Full Code of Shouheng88/Android-notes for AI

master 416fdc3de0ea cached
103 files
825.2 KB
309.9k tokens
1 requests
Download .txt
Showing preview only (1,311K chars total). Download the full file or copy to clipboard to get everything.
Repository: Shouheng88/Android-notes
Branch: master
Commit: 416fdc3de0ea
Files: 103
Total size: 825.2 KB

Directory structure:
gitextract_c1rujubr/

├── API简析/
│   └── LruCache.md
├── Kotlin/
│   └── Kotlin.md
├── README.md
├── 其他/
│   ├── Android知识点随记.md
│   ├── MarkNote版本1的.md
│   ├── MarkNote版本2.md
│   └── 计算机视觉与Android.md
├── 响应式编程/
│   ├── RxJava系列-4:RxJava源码分析.md
│   ├── RxJava系列(1):一篇的比较全面的RxJava2方法总结.md
│   ├── RxJava系列(2):Flowable和背压.md
│   └── RxJava系列(3):用RxJava打造EventBus.md
├── 四大组件/
│   ├── Activity.md
│   ├── Broadcast.md
│   ├── Fragment.md
│   └── Service.md
├── 图片加载/
│   ├── Android相机最佳实践.md
│   ├── Glide系列:Glide主流程源码分析.md
│   ├── Glide系列:Glide的缓存的实现原理.md
│   ├── Glide系列:Glide的配置和使用方式.md
│   └── 图片压缩框架封装.md
├── 工作空间/
│   ├── OOM优化.md
│   ├── Tinker.md
│   ├── URL编码问题.md
│   ├── 文章暂时存放.md
│   ├── 百度定位API.md
│   └── 第三方库整理.md
├── 开发工具/
│   ├── ADB_常见的ADB指令总结.md
│   ├── Gradle_常见的指令和配置总结.md
│   └── Keytool_常用的指令.md
├── 异步编程/
│   ├── Android多线程编程:IntentService和HandlerThread.md
│   └── AsyncTask的使用和源码分析.md
├── 性能优化/
│   ├── Android性能优化-ANR.md
│   ├── Android性能优化-内存优化.md
│   ├── Android性能优化-启动优化.md
│   ├── Android性能优化-布局优化.md
│   ├── Android相机Camera1资料.md
│   ├── Android相机Camera2资料.md
│   └── Android进程保活.md
├── 消息机制/
│   ├── EventBus的源码分析.md
│   ├── 线程通信:Handler、MessageQueue和Looper.md
│   └── 跨进程通信:Binder机制.md
├── 混合开发/
│   └── ReactNative.md
├── 笔试面试/
│   ├── Android高级软件工程师2017.md
│   ├── Android高级面试_10_跨平台开发.md
│   ├── Android高级面试_11_JNINDK.md
│   ├── Android高级面试_12_各种三方库分析.md
│   ├── Android高级面试_12_算法.md
│   ├── Android高级面试_12_项目经验梳理.md
│   ├── Android高级面试_1_Handler相关.md
│   ├── Android高级面试_2_IPC相关.md
│   ├── Android高级面试_3_语言相关.md
│   ├── Android高级面试_4_虚拟机相关.md
│   ├── Android高级面试_5_四大组件、系统源码等.md
│   ├── Android高级面试_6_性能优化.md
│   ├── Android高级面试_7_网络相关.md
│   ├── Android高级面试_8_热修补插件化等.md
│   ├── Android高级面试_9_网络基础.md
│   ├── README.md
│   ├── java/
│   │   ├── ArrayList、LinkedList、Vector.md
│   │   ├── Collection包结构,与Collections的区别.md
│   │   ├── HashMap和ConcurrentHashMap的区别,HashMap的底层源码.md
│   │   ├── HashMap和HashTable的区别.md
│   │   ├── Hashcode的作用.md
│   │   ├── Java1.7与1.8新特性.md
│   │   ├── Java的四种引用,强弱软虚,用到的场景.md
│   │   ├── Map、Set、List、Queue、Stack的特点与用法.md
│   │   ├── Object有哪些公用方法.md
│   │   ├── Override和Overload的含义去区别.md
│   │   ├── Static class 与 non static class的区别.md
│   │   ├── String、StringBuffer与StringBuilder的区别.md
│   │   ├── Switch能否用string做参数.md
│   │   ├── TreeMap、HashMap、LindedHashMap.md
│   │   ├── equals与==的区别.md
│   │   ├── jvm-java 内存模型 以及各个分区具体内容.md
│   │   ├── throw和throws有什么区别.md
│   │   ├── wait()和sleep()的区别.md
│   │   ├── 九种基本数据类型的大小以及他们的封装类.md
│   │   ├── 内存溢出和内存泄露的区别.md
│   │   └── 解析XML的几种方式的原理与特点:DOM、SAX、PULL.md
│   ├── 今日头条Android面试.md
│   └── 初级工程师.md
├── 系统架构/
│   ├── Android应用启动过程.md
│   ├── Android应用安装过程.md
│   ├── Android打包过程.md
│   ├── Android系统启动过程.md
│   ├── Android系统架构.md
│   ├── SurefaceView_and_TextureView.md
│   ├── 控件体系/
│   │   ├── RV.md
│   │   ├── RV各种效果实现.md
│   │   ├── View体系详解:View的工作流程.md
│   │   ├── View体系详解:坐标系、滑动事件和分发机制.md
│   │   ├── View体系详解:自定义控件.md
│   │   └── 动画体系详解.md
│   └── 窗口机制/
│       └── Android的Window管理机制.md
├── 网络访问/
│   ├── OKHttp源码阅读.md
│   └── Retrofit源码阅读.md
└── 高阶技术/
    ├── Android插件化.md
    ├── Dagger从集成到源码.md
    ├── JNI技术总结.md
    ├── 探索Android架构设计.md
    ├── 注解在Android中的应用.md
    ├── 浅谈LiveData的通知过程.md
    └── 浅谈ViewModel生命周期控制.md

================================================
FILE CONTENTS
================================================

================================================
FILE: API简析/LruCache.md
================================================
# Android 内存缓存框架 LruCache 的源码分析

LruCache 是 Android 提供的一种基于内存的缓存框架。LRU 是 **Least Recently Used** 的缩写,即最近最少使用。当一块内存最近很少使用的时候就会被从缓存中移除。在这篇文章中,我们会先简单介绍 LruCache 的使用,然后我们会对它的源码进行分析。

## 1、基本的使用示例

首先,让我们来简单介绍一下如何使用 LruCache 实现内存缓存。下面是 LruCache 的一个使用示例。

这里我们实现的是对 RecyclerView 的列表的截图的功能。因为我们需要将列表的每个项的 Bitmap 存储下来,然后当所有的列表项的 Bitmap 都拿到的时候,将其按照顺序和位置绘制到一个完整的 Bitmap 上面。如果我们不使用 LruCache 的话,当然也能够是实现这个功能——将所有的列表项的 Bitmap 放置到一个 List 中即可。但是那种方式存在缺点:因为是强引用类型,所以当内存不足的时候会导致 OOM。

在下面的方法中,我们先获取了内存的大小的 8 分之一作为缓存空间的大小,用来初始化 LruCache 对象,然后从 RecyclerView 的适配器中取出所有的 ViewHolder 并获取其对应的 Bitmap,然后按照键值对的方式将其放置到 LruCache 中。当所有的列表项的 Bitmap 都拿到之后,我们再创建最终的 Bitmap 并将之前的 Bitmap 依次绘制到最终的 Bitmap 上面:

```java
    public static Bitmap shotRecyclerView(RecyclerView view) {
        RecyclerView.Adapter adapter = view.getAdapter();
        Bitmap bigBitmap = null;
        if (adapter != null) {
            int size = adapter.getItemCount();
            int height = 0;
            Paint paint = new Paint();
            int iHeight = 0;
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

            // 使用内存的 8 分之一作为该缓存框架的缓存空间
            final int cacheSize = maxMemory / 8;
            LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
            for (int i = 0; i < size; i++) {
                RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
                adapter.onBindViewHolder(holder, i);
                holder.itemView.measure(
                        View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
                        View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
                        holder.itemView.getMeasuredHeight());
                holder.itemView.setDrawingCacheEnabled(true);
                holder.itemView.buildDrawingCache();
                Bitmap drawingCache = holder.itemView.getDrawingCache();
                if (drawingCache != null) {
                    bitmaCache.put(String.valueOf(i), drawingCache);
                }
                height += holder.itemView.getMeasuredHeight();
            }

            bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
            Canvas bigCanvas = new Canvas(bigBitmap);
            Drawable lBackground = view.getBackground();
            if (lBackground instanceof ColorDrawable) {
                ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
                int lColor = lColorDrawable.getColor();
                bigCanvas.drawColor(lColor);
            }

            for (int i = 0; i < size; i++) {
                Bitmap bitmap = bitmaCache.get(String.valueOf(i));
                bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
                iHeight += bitmap.getHeight();
                bitmap.recycle();
            }
        }

        return bigBitmap;
    }
```

因此,我们可以总结出 LruCahce 的基本用法如下:

首先,你要声明一个缓存空间的大小,在这里我们用了运行时内存的 8 分之 1 作为缓存空间的大小

```java
    LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
```

**但是应该注意的一个问题是缓存空间的单位的问题**。因为 LruCache 的键值对的值可能是任何类型的,所以你传入的类型的大小如何统计需要自己去指定。后面我们在分析它的源码的时候会指出它的单位的问题。LruCahce 的 API 中也已经提供了计算传入的值的大小的方法。我们只需要在实例化一个 LruCache 的时候覆写该方法即可。而这里我们认为一个 Bitmap 对象所占用的内存的大小不超过 1KB. 

然后,我们可以像普通的 Map 一样调用它的 `put()` 和 `get()` 方法向缓存中插入和从缓存中取出数据:

```java
    bitmaCache.put(String.valueOf(i), drawingCache);
    Bitmap bitmap = bitmaCache.get(String.valueOf(i));
```

## 2、LruCahce 源码分析

### 2.1 分析之前:当我们自己实现一个 LruCache 的时候,我们需要考虑什么

在我们对 LruCache 的源码进行分析之前,我们现来考虑一下当我们自己去实现一个 LruCache 的时候需要考虑哪些东西,以此来带着问题阅读源码。

因为我们需要对数据进行存储,并且又能够根据指定的 id 将数据从缓存中取出,所以我们需要使用哈希表表结构。或者使用两个数组,一个作为键一个作为值,然后使用它们的索引来实现映射也行。但是,后者的效率不如前者高。

此外,我们还要对插入的元素进行排序,因为我们需要移除那些使用频率最小的元素。我们可以使用链表来达到这个目的,每当一个数据被用到的时候,我们可以将其移向链表的头节点。这样当要插入的元素大于缓存的最大空间的时候,我们就将链表末位的元素移除,以在缓存中腾出空间。

综合这两点,我们需要一个既有哈希表功能,又有队列功能的数据结构。在 Java 的集合中,已经为我们提供了 LinkedHashMap 用来实现这个功能。

实际上在 Android 中的 LruCache 也正是使用 LinkedHashMap 来实现的。LinkedHashMap 拓展自 HashMap。如果理解 HashMap 的话,它的源码就不难阅读。LinkedHashMap 仅在 HashMap 的基础之上,又将各个节点放进了一个双向链表中。每次增加和删除一个元素的时候,被操作的元素会被移到到链表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基础之上进行了一层拓展,不过 Android 中的 LruCache 的实现具有一些很巧妙的地方值得我们学习。

### 2.2 LruCache 源代码分析

从上面的分析中我们知道了选择 LinkedHashMap 作为底层数据结构的原因。下面我们分析其中的一些方法。这个类的实现还有许多的细节考虑得非常周到,非常值得我们借鉴和学习。

#### 2.2.1 缓存的最大可用空间

在 LruCache 中有两个字段 size 和 maxSize. maxSize 会在 LruCache 的构造方法中被赋值,用来表示该缓存的最大可用的空间:

```java
    int cacheSize = 4 * 1024 * 1024; // 4MiB,cacheSize 的单位是 KB
    LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    }};
```

这里我们使用 4MB 来设置缓存空间的大小。我们知道 LruCache 的原理是指定了空间的大小之后,如果继续插入元素时,空间超出了指定的大小就会将那些“可以被移除”的元素移除掉,以此来为新的元素腾出空间。那么,因为插入的类型时不确定的,所以具体被插入的对象如何计算大小就应该交给用户来实现。

在上面的代码中,我们直接使用了 Bitmap 的 `getByteCount()` 方法来获取 Bitmap 的大小。同时,我们也注意到在最初的例子中,我们并没有这样去操作。那样的话一个 Bitmap 将会被当作 1KB 来计算。

这里的 sizeOf() 是一个受保护的方法,显然是希望用户自己去实现计算的逻辑。它的默认值是 1,单位和设置缓存大小指定的 maxSize 的单位相同:

```java
    protected int sizeOf(K key, V value) {
        return 1;
    }
```

这里我们还需要提及一下:虽然这个方法交给用户来实现,但是在 LruCache 的源码中,不会直接调用这个方法,而是

```java
    private int safeSizeOf(K key, V value) {
        int result = sizeOf(key, value);
        if (result < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
        return result;
    }
```

所以,这里又增加了一个检查,防止参数错误。其实,这个考虑是非常周到的,试想如果传入了一个非法的参数,导致了意外的错误,那么错误的地方就很难跟踪了。如果我们自己想设计 API 给别人用并且提供给他们自己可以覆写的方法的时候,不妨借鉴一下这个设计。

#### 2.2.2 LruCache 的 get() 方法

下面我们分析它的 get() 方法。它用来从 LruCahce 中根据指定的键来获取对应的值:

```java
    /**
     * 1). 获取指定 key 对应的元素,如果不存在的话就用 craete() 方法创建一个。
     * 2). 当返回一个元素的时候,该元素将被移动到队列的首位;
     * 3). 如果在缓存中不存在又不能创建,就返回n ull
     */
    public final V get(K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
            // 在这里如果返回不为空的话就会将返回的元素移动到队列头部,这是在 LinkedHashMap 中实现的
            mapValue = map.get(key);
            if (mapValue != null) {
                // 缓存命中
                hitCount++;
                return mapValue;
            }
            // 缓存没有命中,可能是因为这个键值对被移除了
            missCount++;
        }

        // 这里的创建是单线程的,在创建的时候指定的 key 可能已经被其他的键值对占用
        V createdValue = create(key);
        if (createdValue == null) {
            return null;
        }

        // 这里设计的目的是防止创建的时候,指定的 key 已经被其他的 value 占用,如果冲突就撤销插入
        synchronized (this) {
            createCount++;
            // 向表中插入一个新的数据的时候会返回该 key 之前对应的值,如果没有的话就返回 null
            mapValue = map.put(key, createdValue);
            if (mapValue != null) {
                // 冲突了,还要撤销之前的插入操作
                map.put(key, mapValue);
            } else {
                size += safeSizeOf(key, createdValue);
            }
        }

        if (mapValue != null) {
            entryRemoved(false, key, createdValue, mapValue);
            return mapValue;
        } else {
            trimToSize(maxSize);
            return createdValue;
        }
    }
```

这里获取值的时候对当前的实例进行了加锁以保证线程安全。当用 map 的 get() 方法获取不到数据的时候用了 `create()` 方法。因为当指定的键值对找不到的时候,可能它本来就不存在,可能是因为缓存不足被移除了,所以,我们需要提供这个方法让用户来处理这种情况,该方法默认返回 null. 如果用户覆写了 `create()` 方法,并且返回的值不为 null,那么我们需要将该值插入到哈希表中。

插入的逻辑也在同步代码块中进行。这是因为,创建的操作可能过长而且是非同步的。当我们再次向指定的 key 插入值的时候,它可能已经存在值了。所以当调用 map 的 put() 的时候如果返回不为 null,就表明对应的 key 已经有对应的值了,就需要撤销插入操作。最后,当 mapValue 非 null,还要调用 `entryRemoved()` 方法。每当一个键值对从哈希表中被移除的时候,这个方法将会被回调一次。

最后调用了 `trimToSize()` 方法,用来保证新的值被插入之后缓存的空间大小不会超过我们指定的值。当发现已经使用的缓存超出最大的缓存大小的时候,“最近最少使用” 的项目将会被从哈希表中移除。

那么如何来判断哪个是 “最近最少使用” 的项目呢?我们先来看下 `trimToSize()` 的方法定义:

```java
    public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize) {
                    break;
                }

                // 获取用来移除的 “最近最少使用” 的项目
                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
```

显然,这里是使用了 LinkedHashMap 的 `eldest()` 方法,这个方法的返回值是:

```java
    public Map.Entry<K, V> eldest() {
        return head;
    }
```

也就是 LinkedHashMap 的头结点。那么为什么要移除头结点呢?这不符合 LRU 的原则啊,这里分明是直接移除了头结点。实际上不是这样,魔力发生在 `get()` 方法中。在 LruCache 的 get() 方法中,我们调用了 LinkedHashMap 的 `get()` 方法,这个方法中又会在拿到值的时候调用下面的方法:

```java
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }
```

这里的逻辑是把 `get()` 方法中返回的结点移动到双向链表的末尾。所以,最近最少使用的结点必然就是头结点了。

## 3、总结

以上是我们对 LruCache 的是使用和源码的总结,这里我们实际上只分析了 `get()` 的过程。因为这个方法才是 LruCache 的核心,它包含了插入值和移动最近使用的项目的过程。至于 `put()` 和 `remove()` 两种方法,它们内部实际上直接调用了 LinkedHashMap 的方法。这里我们不再对它们进行分析。

------
**如果您喜欢我的文章,可以在以下平台关注我:**

- 个人主页:[https://shouheng88.github.io/](https://shouheng88.github.io/)
- 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)
- Github:[https://github.com/Shouheng88](https://github.com/Shouheng88)
- CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)
- 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)


================================================
FILE: Kotlin/Kotlin.md
================================================
# Kotlin 学习笔记-1:基础语法

## 1、初识 Kotlin

### 1.1 基本特性梳理

1. 本质上是**静态类型语言**,编译期确定类型,**但无需明确指定变量类型**;
2. 对**可空类型的支持**,可以在编译期发现空指针;
3. 支持**函数式编程**,虽然 Java 8 以后都支持了;
4. 类文件的后缀名式 `.kt`,编译之后还是生成 class 文件,只是编译器使用的是 `kotlinc`(对应于 javac),执行 class 的时候还是使用 java;
5. 可以使用转换器将 Java 转换成 kotlin;
6. Kotlin 标准库给 Java 库做了封装,我们可以简化原生 Java 库的调用;

### 1.2 类文件结构

在 Kotlin 中文件名称和文件的内容没有关系(在 Java 中文件名和类名相同),并且文件内部定义的是函数还是类都没关系。比如,下面是定义在目录 `me/shouheng/demo1/FirstDemo.kt` 中的类和函数,这里类和函数处于文件的同一层次。另外,一个文件中还可以定义多个类和多个函数,都是允许的。

```kotlin
package me.shouheng                                 // 包的声明应处于源文件顶部

class Person (age : Int, name : String)             // 声明了一个类
class Person2 (val age : Int, val name : String)    // 声明了一个类
```

类和函数的真实包名是由文件中的 **package** 关键字指定的,与文件结构没有必然的关系。当然,我们建议按照 Java 的规则使其对应起来,因为这样维护起来更好、逻辑更清晰些。

### 1.3 定义函数

```kotlin
fun doSomething(person: Person) : Int {             // 定义了一个函数
    // 在字符串中使用 “$+变量名” 的格式进行占位,相当于 "My name is" + persion + "!"
    println("My name is $person!")
}

fun sum(a: Int, b: Int) = a + b
```

1. 函数的定义使用关键字 `fun`,覆写函数的话就在函数名前面加上 `override fun`。
2. 变量和返回值的类型被放在冒号后面,如果返回无意义的类型,可以使用 `Unit`,也可以省略。
3. **字符串模板**:在字符串中使用 `$+变量名` 的格式进行占位(这叫字符串模板),如果希望使用美元符号,前面加上反斜杠即可。
4. 也可以将表达式作为函数体、返回值类型自动推断。
5. 如果需要把一个字符串当作正则表达式,需要显式调用字符串的 `toRegex()` 方法才行。
6. 三重引号中的字符不会做任何转义,即 `"""$"""` 可以直接当作美元。

### 1.4 定义变量

```kotlin
fun test(args : Array<String>) {
    val a = Person(10, "Ming")
    var b: Person
    b = Person(11, "Xing")
    doSomething(a)
}
```

1. **数组**:没有专门用来声明数组的,全部都是类。可以像下面这样声明数组 `Array<String>`。另外,可以按照 `args[0]` 的方式获取数组元素。
2. **声明变量有 `var` 和 `val` 两中方式**:系统可以自动推断类型,`var` 声明的变量可以二次赋值,而 `val` 不行,后者相当于 `final` 的。
3. 虽然 `var` 类型的变量可以二次赋值,但是两次赋值的类型必须相同。
4. 可以在声明变量的时候使用冒号指定变量的类型,像上面的 b 一样(大部分情况下可以省略,因为编译器可以自动推断)。
5. 初始化一个类的时候不需要 `new` 关键字(抛出异常的时候自然也一样)。

### 1.5 使用循环

```kotlin               
    // 循环 Map
    val map = HashMap<Char, String>()
    for (c in 'A'..'F') {           // 循环字符串
        map[c] = Integer.toHexString(c.toInt())
    }
    for ((k, v) in map) { // 输出结果是 <A,41> <B,42> <C,43> <D,44> <E,45> <F,46>
        print("<$k,$v> ")
    }

    // 循环列表
    val items = listOf("apple", "banana", "kiwifruit")
    for (index in items.indices) {
        println("item at $index is ${items[index]}")
    }

    // while 循环
    var index = 0
    while (index < items.size) {    // 使用 while 循环
        println("item at $index is ${items[index]}")
        index++
    }
```

总结,

1. Kotlin 的 for 循环与 Java 稍有不同,它跟 js 等更相似,即使用 `in` 关键字。
2. 遍历 map 的时候使用上述方式,以键值对的形式遍历即可。
3. 要按照索引的方式进行遍历,需要先使用列表的 `indices` 得到索引列表。
4. while 循环和 Java 中的使用方式基本一致,包括 `while` 和 `do...while` 两种形式。

### 1.6 when

类似于 Java 中的 switch,但是它的每个条件中默认加入了 break. 另外,它还有一个比较好的地方是,它会检查枚举是否都包含进去了,如果没全部包含,它会提示你全包含或者加入 else 语句。

```kotlin
fun multiple(city: City2) = when(city) {
    City2.BEIGING -> {
        2*10000
        2+2
    }
    City2.SHANGHAI,City2.GUANGZHOU->3*10000
    else -> 5
}

fun describe(obj: Any): String =
    when (obj) {
        1          -> "One"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }
```

另外,从示例 2 中可以看出,

1. 注意每个条件之后需要加上 `->` 才行哦!
2. 当多个类型的逻辑相同的时候,可以把它们放在 when 的一个条件里,然后用逗号分隔开。
3. when 比 switch 的功能更加强大,它还可以使用不同类型的判断条件。(参考示例 2)

### 1.7 控制语句

#### 1.7.1 if 语句

在 Kotlin 中,if是一个表达式,即它会返回一个值。 因此就不需要三元运算符 `? : `,比如 `val max = if (a > b) a else b`。

#### 1.7.2 返回与跳转

Kotlin 中返回与跳转语句也是 return、break 和 continue 三种。它们的基本使用方式与 Java 相同。此外,Kotlin 中还支持标签。标签的格式为标识符后跟 @ 符号,例如:`abc@`、`fooBar@` 都是有效的标签。我们可以使用标签进行流程的控制(用的比较少)。

### 1.8 异常处理

`try..catch` 语句的基本结构如下,和 Java 基本相似,只是 catch 中声明变量的方式,下面的函数会当小于 0 时返回 -1,否则返回 1. 另外,kotlin 中不分受检异常和非受检异常,不会强制你捕获异常。

```kotlin
fun tryTest(i : Int) = try {
    if (i < 0) throw IllegalArgumentException("< 0")
    else 1
} catch (e : Exception) {
    -1
}
```

## 2、类与对象

### 2.1 类声明

Kotlin 中使用关键字 `class` 声明类。类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的;如果一个类没有类体,可以省略花括号。

```kotlin
class MyClass { /*...*/ }
class Empty
```

### 2.2 构造方法

在 Kotlin 中的一个类可以有一个`主构造函数`以及一个或多个`次构造函数`。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 `constructor` 关键字。

```kotlin
class Person constructor(firstName: String) { ... } // 主构造函数
class Person(firstName: String) { ... }             // 省略主构造函数
class Person(val firstName: String) { ... } 
class DontCreateMe private constructor () { ... }   // 将构造函数设置成私有的
```

注意上述声明方式中的 2 和 3 的区别,后者声明之后有一个局部变量 firstName,而前者没有声明任何变量。可以通过 private 关键字将构造函数设置成私有的。

类也可以声明前缀有 constructor 的次构造函数。果类有一个主构造函数,每个次构造函数需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:

```kotlin
// 声明了一个次构造函数
class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}
// 有主构造函数时,次构造函数的声明方式
class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}
```

### 2.3 初始化代码块

Kotlin 中也有初始化代码块,非静态初始化代码块使用 `init` 关键字即可。

```kotlin
class Person {
    init { //
        // ...
    }
}
```

初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。

静态代码块与静态变量定义的方式一致,略显繁琐,后续说明。

### 2.4 函数

函数是 Kotlin 中非常重要的概念,Kotlin 提供了许多便利的函数。

```kotlin
// 默认参数
fun MyFun(a: String = "a", b: String) {
    println("$a $b")
}

// 为 String 增加函数
fun String.lastChar() : Char = this[length - 1]

// 为 String 增加属性
val String.lastChar: Char
    get() = get(length - 1) 

// 可变数量的参数
fun varFun(vararg args: String) { 
    for (arg in args) println(arg)
}

fun main(args: Array<String>) {
    MyFun(a = "x", b = "y")   // 指定参数名称:输出 x y,允许指定参数的名称
    MyFun(b = "y")            // 指定参数名称:输出 a y,使用默认的参数
    val args = arrayOf("A", "B", "C")
    varFun(*args)             // 使用伸展操作符调用可变数量参数的函数
}
```

1. 允许在调用方法的时候指定参数的名称,并且指定了一个参数之后,后面的参数都要指定名称;
2. 允许为函数的参数指定 `缺省参数`,比如上面的 a 默认是 `a`;
3. 可以为别人的函数添加函数和属性,但是 `拓展函数无法访问私有的或者受保护的成员`。本质上拓展函数将调用它的实例当作第一个参数,这是本质的实现原理,很多问题可以依靠这个理解。拓展函数无法被继承,原因很简单,就是因为它们只相当于调用了一个静态方法而把实例当作参数实现的
4. 可变数量参数函数调用的时候可以使用伸展(spread)操作符(就是在数组前面加 `*`)。缺省参数定义的时候需要使用 `vararg`(也许是因为 `..` 被当作其他用途了),当传入数组的时候的需要解包,也就是数组前面加 `*`。
5. 把函数提升到与类同一层次,这样它就成 `静态函数` 了,把字段提升到与类同一层次,这样它就成 `静态字段` 了。
6. 可以在函数内部定义`局部函数`,并且局部函数可以访问外部函数(即闭包)的局部变量。
7. 导入函数的时候可以使用 `as` 重命名导入以简化使用。

### 2.5 属性

#### 2.5.1 声明属性

声明类的属性有 var 和 val 两种方式。声明一个属性的格式是,

```kotlin
var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]
```

示例程序,

```kotlin
class Person{
    var grade: Int = 0
        get() = field + 1
        set(value) {
            field = value + 1
        }
    var age: Int = 0
        private set     // 修改默认访问权限
}
```

Kotlin 中会将类的局部变量的访问权默认为 pulic 的,所以外部可以直接通过实例获取字段和赋值。可以通过上述方式来修改它的默认方法权限。

可以通过 `get()` 和 `set()` 来重写 getter 和 setter 方法。一般情况下,使用默认的 `get()` 和 `set()` 默认逻辑即可,这也是 Java 规范。如果想增加新的逻辑,可以增加一个新的方法。注意,在覆写的时候,如果要修改属性的值,需要通过 field 来完成。field 标识符只能用在属性的访问器内,也被称为幕后字段。

#### 2.5.2 编译期常量 const

```kotlin
// 定义在类顶层
const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

// 定义在类内部,可以用来为类添加静态常量
class MyClass {
    companion object {
        const val EXTRA_LAUNCH_TYPE = "__extra_launch_type"
    } // 外部访问方式是:MyClass.EXTRA_LAUNCH_TYPE
}
```

使用 `const` 修饰符标记为编译期常量。 这些属性需要满足以下要求:

1. 位于`顶层`或者是 `object` 声明或 `companion object` 的一个成员;
2. 以 String 或原生类型值初始化;
3. 没有自定义 getter。

#### 2.5.3 延迟初始化 lateinit 属性与变量

```kotlin
lateinit var subject: TestSubject
```

一般地,属性声明为非空类型必须在构造函数中初始化。当无法在构造器中对属性初始化时,可以用 `lateinit` 修饰该属性。该修饰符只能用于在类体中的属性,而自 Kotlin 1.2 起,也用于顶层属性与局部变量。该属性或变量必须为`非空`类型,并且是`非原生类型`。

在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。自 1.2 起,可以该属性的引用上使用 `.isInitialized` 检测一个 lateinit var 是否已初始化。

### 2.6 嵌套类与内部类

#### 2.6.1 内部类

下面是 Kotlin 中内部类的使用示例。在这个例子中,声明的内部类类似于 Java 中的非静态内部类,因此进行实例化的时候需要先获取到外部类的实例。

```kotlin
class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
    inner class Inner {
        fun foo() = bar // 可以访问外部类变量
    }
}

val demo = Outer.Nested().foo() // == 2
```

类可以标记为 `inner` 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用。使用 `inner` 修饰的类属于内部类,没有使用的属于嵌套类。所以,上面的 Nested 属于嵌套类,Inner 属于内部类。但是注意嵌套类和内部类的区别:嵌套类不是内部类,不包含对外部类的引用。所以,比如 Android 中常见的内存泄漏的问题就可以避免了。

#### 2.6.2 匿名内部类

匿名内部类也是我们开发过程中比较常用的定义方式,比如设置点击事件的回调的时候。匿名类的定义又分成下面两种方式:

```kotlin
// 定义一个类
open class A(x: Int) {
    public open val y: Int = x
}

// 使用匿名内部类
window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { …… }
    override fun mouseEntered(e: MouseEvent) { …… }
})

// 对函数式接口使用匿名内部类
val listener = ActionListener { println("clicked") }

// 匿名内部类有多个超类的情况
val val ab: A = object : A(1), MouseAdapter {
    override val y = 15
}

// 不适用任何类创建匿名类实例
val adHoc = object {
    var x: Int = 0
    var y: Int = 0
}
```

第一种方式适用于类中包含多个方法的情形,如上面的 MouseAdapter 的匿名类;另一种方式适用于函数式接口,即只有一个方法的接口,如 ActionListener。

如果一个类的超类型有一个构造函数,则必须传递适当的构造函数参数给它。多个超类型可以由跟在冒号后面的逗号分隔的列表指定,如 ab 的定义。

如果不想要明确创建哪种类型,而只是想创建一个匿名类实例,可以按按上面的 `adHoc` 那样定义。

#### 2.6.3 对象表达式

上面是 object 定义匿名类的几个示例,除此之外,它还可以用来定义单例类,

```kotlin
object DataProviderManager {
    // 单例的方法
    fun registerDataProvider(provider: DataProvider) {
        // ……
    }

    // 单例类的属性
    val allDataProviders: Collection<DataProvider>
        get() = // ……
}

// 调用单例类的方法
DataProviderManager.registerDataProvider(……)
```

这种形式定义的单例在初始化的时候是线程安全的,它调用的时候有点类似于 Java 中静态类的方法和属性的调用。这些对象也可以有父类,它的实现方式与普通的类的继承并无二致。

注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。

#### 2.6.4 伴生对象

类内部的对象声明可以用 companion 关键字标记的对象是伴生对象,它的使用效果类似于 Java 中的静态字段和静态方法。伴生对象也是可以实现基类和接口的。比如,

```kotlin
class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

// 调用的方式是:MyClass.create()
```

### 2.7 继承

在 Kotlin 中所有类都有一个共同的超类 Any,类似于 Java 中的 Object,但是两者不同。

Kotlin 中的声明默认都是 `public final` 的,即公共且无法继承,如果希望一个类可以被继承,可以使用 open 关键字进行修饰。覆写函数的时候需要使用 override 关键字进行修饰,并且是必须的。属性的覆盖与函数的覆盖类似,都是使用 override 进行修饰。

```kotlin
// 基类,使用 open 关键字修饰
open class Base(p: Int)

// 继承的时候调用基类的构造器
class Derived(p: Int) : Base(p)

// 当基类有多个构造器的时候
class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
```

如果派生类有一个主构造函数,其基类型可以(并且必须) 用基类的主构造函数参数就地初始化。

如果派生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数。

另外,

1. 当在派生类的函数中调用父类函数的时候使用 `super.函数名` 即可,与 Java 一致。
2. 如果实现了多个接口,想要调某个父接口的实现,需要按照 `super<接口>.函数名` 的形式调用。
3. `abstract` 关键字的用法和 Java 一样,它同时具有 open 的语义。
4. **可见性修饰符**:总共有四个,即 private、 protected、 internal 和 public。修饰符 `internal` 表示模块内可见;`protected` 表示子类可见;`private` 表示类内可见,并且子类可见并不代表模块内可见,两个之间没有关系;public 表示没有任何限制,并且是默认级别。
5. 非静态内部类可以使用 `this@外部类名称` 访问外部类的方法和变量。

### 2.8 接口

Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含实现:                       

```kotlin
interface IClickable {
    fun onClick()
    fun defaulFun() {  // 默认函数,不需要任何声明
        println("I'm defaulFun().")
    }
    fun defaulFun2() {
        println("I'm defaulFun2().")
    }
}
```

### 2.9 数据类

专门用来存储数据的类,在普通类的基础之上使用 data 关键字修饰。系统会自动为我们的数据类生成:equals()、hashCode()、toString()、componentN() 和 copy() 函数的实现。

```kotlin
data class User(val name: String, val age: Int)
```

数据类要求:

1. 主构造函数需要至少有一个参数
2. 主构造函数的所有参数需要标记为 val 或 var
3. 数据类不能是抽象、开放、密封或者内部的
4. (在1.1之前)数据类只能实现接口

上面的 `copy()` 函数类似于 Java 中的 `clone()` 函数,用来实现函数的克隆。                                                                                            
### 2.10 密封类

密封类有点类似于枚举,要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。密封类不允许有非-private 构造函数(其构造函数默认为 private)。

```kotlin
sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
```

理解上,密封类的作用是类似于枚举,但是对类的位置进行了限制。这是为了让运用于 when 的子类能够更容易被发现。

### 2.11 枚举类

声明枚举类的时候需要使用 enum 关键字,也可以给枚举增加一些属性,其定义方式基本同 Java. 

```kotlin
enum class City {
    BEIGING, SHANGHAI, GUANGZHOU
}
enum class City2(level:Int) {
    BEIGING(1), SHANGHAI(2), GUANGZHOU(3)
}
```

## 3、高阶特性

### 3.1 Lambda 表达式

#### 3.1.1 Lambda 表达式的基本示例

Lambda 表达式的格式是:`{ x: Int, y: Int -> x + y }`。它的使用比较简单,通常用来定义函数式接口。如果变量的含义明确,它还可以进一步简化,比如 `{ it * it}` 也是可以的。

#### 3.1.2 集合与 Lambda

以下面的程序为例,我们可以在集合中使用 Lambda 表达式。在 Java 8 中,我们可以使用 Stream 进行编程,而 Android 中要求 API 24 以上才能使用 Stream,所以 Kotlin 可以帮助我们解决这个遗憾。

```kotlin
listOf(1,2,3,4).filter { it > 2 }.map { it.toString() }.all { it.length > 2 }
```

它支持的操作符包括:`filter`, `map`, `all`, `any`, `count` 和 `find`, `groupBy`, `flatMap` 和 `flatten`。它们的用法和效果与 Stream 或者 RxJava 中的操作符的含义一致。

### 3.1.3 with 与 apply

`with` 表示以某个类作为开始,对其进行操作,最后返回。`apply` 对应于 with,表示对某个实例进行某种操作;(省去了声明一个实例的过程,仅此而已,但是新添加一个语法……)

它们的效果有点类似于在 Java 中的这种写法。

```java
new LinedList<String>{
    {
        add("A");
        add("B");
    }
}
```

也就是可以为声明的对象增加一些操作,但是这些过程都被包含在了 `with` 和 `apply` 中。参考下面的程序:

```kotlin
// with
fun getString() : String = with(LinkedList<Int>()) {
    for (i in 1..10) {
        this.add(i) // 这里的 this 就是上面传入的列表
    }
    this.toString()
}
// apply
fun getString2() : String = LinkedList<Int>().apply { 
    for (i in 1..10) {
        add(i)
    }
}.toString()
```

### 3.2 区间

与区间相关的几个操作符是 `..`、`in`、`!in`、`until`、`downTo` 以及 `until`。其含义如下,Kotlin 中的区间默认是闭区间的:

```kotlin
    val nums = 1..10
    // 输出结果是 1..10
    println(nums)                   
 
    // 输出结果是 12345678910
    for (num in nums) {             
        print(num)
    }                  
 
    // 输出结果是 1086,10 递减到 5,步进 2
    for (i in 10 downTo 5 step 2) { 
        print(i)
    }                   
 
    // 输出结果是 12345,1 递增到 6,步进 1
    for (i in 1 until 6) {          
        print(i)
    }    
```

### 3.3 集合

Kotlin 中的集合比 Java 中的集合,增加了可变和不可变的概念。不可变集合的好处在于它的线程安全性(估计这个又是从 Guava 中借鉴来的概念)。在创建集合的时候,我们无需按照 Java 中使用 new 的方式来创建。在使用的时候还是要注意区分。下面我们来列举些这些集合,

![不可变集合的创建方法](res/QQ截图20190303112453.png)

![可变集合的创建方法](res/QQ截图20190303112506.png)

Kotlin 中的不可变集合的一个好处是,它本身就不会提供插入和删除的方法,所以无需担心因为该方法没有实现而出现的运行时异常。

上面也说过,Kotlin 中的集合支持 Stream 的一些操作,除了上面的那些,它还支持许多其他的操作,这里就不一一列举了。

### 3.4 类型系统

Kotlin 对空类型的处理比较好:默认所有的参数都是非空的,除非显式声明其可以为空。而 Java 中默认全部都是可空的。这可以有效帮助我们减少程序中的 NPE. 

```kotlin
fun testFun1(param : String) {
    print(param.length)
}

// 如果一个类是可空的那么必须显式声明,所以下面的程序编译器提示错误
fun testFun2(param : String?) {
//    print(param.length)
}

fun main(args : Array<String>) {
//    testFun1(null) // 编译器提示错误
    testFun2(null)
    val str : String? = null
    println(str?.length)            // null
    println(str ?: "B")             // B
    val b = "AA".let { it + "A" }   // AAA
    println(b)
}
```

1. 使用 `?` 在类型的后面则说明这个变量是可空的。
2. 安全调用运算符 `?.`,以 `a?.method()` 为例,当 a 不为 null 则整个表达式的结果是 `a.method()` 否则是 null;
3. Elvis 运算符 `?:`,以 `a ?: "A"` 为例,当 a 不为 null 则整个表达式的结果是 a,否则是 "A";
4. 安全转换运算符 `as?`,以 `foo as? Type` 为例,当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例,否则返回 null;
5. 非空断言 `!!`,用在某个变量后面表示断言其非空,如 `a!!`;
6. `let` 表示对调用 let 的实例进行某种运算,如 `val b = "AA".let { it + "A" }` 返回 "AAA"。如果使用 let 的某个对象是可空的,那么只有当该对象非空的时候才会执行 let。
7. Kotlin 中进行类型之间的转换的时候必须显式进行,需要调用 `toXX()` 方法;
8. `Any` 和 `Any?` 分别是所有非空和空类型的超类;
9. `Unit` 相当于 Java 中的 void,返回 Unit 就相当于返回 void;

## 4、协程

常规的线程使用时,上下文切换会带来额外的性能开销。线程适用于 CPU 密集型的程序,而协程适合 Android 这种 IO 密集型的程序。从执行效果上面看,协程和线程达到的效果基本一致。它们的区别主要有以下几点:

1. 协程不需要进行同步控制;
2. 可以开大量的协程,但是线程数量是有限的,不然会影响程序的运行时性能;
3. 使用 GlobalScope 启动的协程像守护线程,当程序中的所有线程都结束的时候,整个程序结束,没有执行完毕的协程不会继续执行;

协程配置等相关信息:[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)

**挂起函数**:使用 suspend 修饰的函数,挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数能挂起协程。挂起函数挂起协程时,不会阻塞协程所在的线程,挂起函数执行完成后会恢复协程。所以,挂起函数只能在协程中或其他挂起函数中调用。

**CoroutineScope 和 CoroutineContext**:CoroutineScope 时协程本身,包含了 CoroutineContext。CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。

**CoroutineDispatcher**:协程调度器,决定协程所在的线程或线程池。指定协程运行于特定的一个线程、一个线程池或者不指定任何线程。有三种标准实现 Dispatchers.Default、Dispatchers.IO,Dispatchers.Main和Dispatchers.Unconfined,Unconfined 就是不指定线程。

**构建协程**:`CoroutineScope.launch {}` 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。`runBlocking {}`:创建一个新的协程同时阻塞当前线程,直到协程结束。`withContext {}` 不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。`async {}` 在后台创建一个新协程,跟 `CoroutineScope.launch {}` 的区别在于它有返回值。



================================================
FILE: README.md
================================================
# Android [DEPRECATED]

## 1、目录

### 基础开发

- 基础回顾
    - [Android 基础回顾:Activity 基础](四大组件/Activity.md)
    - [Android 基础回顾:Fragment 基础](四大组件/Fragment.md)
    - [Android 基础回顾:Service 基础](四大组件/Service.md)
    - [Android 基础回顾:Broadcast 基础](四大组件/Broadcast.md)

- 开发语言
    - [Java 注解在 Android 中的应用](注解和依赖注入/注解在Android中的应用.md)
    - [Kotlin 基础知识梳理](Kotlin/Kotlin.md)
    - [在 Android 中使用 JNI 的总结](高阶技术/JNI技术总结.md)

- 架构设计
    - [Android 应用架构设计探索:MVC、MVP、MVVM和组件化](结构设计/探索Android架构设计.md)
    - [浅谈 ViewModel 的生命周期控制](高阶技术/浅谈ViewModel生命周期控制.md)
    - [浅谈 LiveData 的通知机制](高阶技术/浅谈LiveData的通知过程.md)

- 性能优化
    - [ANR](性能优化/Android性能优化-ANR.md)
    - [布局优化](性能优化/Android性能优化-布局优化.md)
    - [进程保活](性能优化/Android进程保活.md)
    - [启动优化](性能优化/Android性能优化-启动优化.md)
    - [内存优化](性能优化/Android性能优化-内存优化.md)

- 开发环境
    - [常见的 ADB 指令总结](开发工具/ADB_常见的ADB指令总结.md)
    - [常见的 Gradle 指令和配置总结](开发工具/Gradle_常见的指令和配置总结.md)
    - [常见的 Keytool 指令总结](开发工具/Keytool_常用的指令.md)

### 系统源码

- 核心流程
    - [Android 系统架构](系统架构/Android系统架构.md)
    - [Android 系统启动流程源码分析](系统架构/Android系统启动过程.md)
    - [Android 应用打包过程](系统架构/Android打包过程.md)
    - [Android 应用安装过程](系统架构/Android应用安装过程.md)

- 消息机制
    - [Android 消息机制:Handler、MessageQueue 和 Looper](消息机制/线程通信:Handler、MessageQueue和Looper.md.md)
    - [Android IPC 机制:Binder 机制](消息机制/跨进程通信:Binder机制.md) 

- 异步编程
    - [AsyncTask 的使用和源码分析](异步编程/AsyncTask源码分析.md)
    - [Android 多线程编程:IntentService 和 HandlerThread](异步编程/Android多线程编程:IntentService和HandlerThread.md)

- 窗口机制
    - [Android 的窗口管理机制](系统架构/窗口机制/Android的Window管理机制.md)(编辑中)

- 控件体系
    - [View 体系详解:View的工作流程](系统架构/控件体系/View体系详解:View的工作流程.md)
    - [View 体系详解:坐标系、滑动事件和分发机制](系统架构/控件体系/View体系详解:坐标系、滑动事件和分发机制.md)
    - [Android 动画体系详解](系统架构/控件体系/动画体系详解.md)
    - [SurfaceView 与 TextureView 的区别](系统架构/SurefaceView_and_TextureView.md)

- 部分 API 源码
    - [LruCache 的使用和源码分析](API简析/LruCache.md)

### 三方库源码

- 网络框架
    - [网络框架 OkHttp 源码解析](网络访问/OKHttp源码阅读.md)
    - [网络框架 Retrofit 源码解析](网络访问/Retrofit源码阅读.md)

- 图片加载框架
    - [Glide 系列-1:预热、Glide 的常用配置方式及其原理](图片加载/Glide系列:Glide的配置和使用方式.md)
    - [Glide 系列-2:主流程源码分析](图片加载/Glide系列:Glide主流程源码分析.md)
    - [Glide 系列-3:Glide 缓存的实现原理](图片加载/Glide系列:Glide的缓存的实现原理.md)

- RxJava
    - [RxJava2 系列-1:一篇的比较全面的 RxJava2 方法总结](响应式编程/RxJava2系列·_一篇的比较全面的RxJava2方法总结.md)
    - [RxJava2 系列-2:Flowable 和背压](响应式编程/Flowable和背压.md)
    - [RxJava2 系列-3:使用 Subject](响应式编程/用RxJava打造EventBus.md)
    - [RxJava2 系列-4:RxJava 源码分析](响应式编程/RxJava系列-4:RxJava源码分析.md)

- 其他框架
    - [消息机制 EventBus 源码解析](消息机制/EventBus的源码分析.md)
    - [Dagger 从集成到源码带你理解依赖注入框架](高阶技术/Dagger从集成到源码.md)

### Java 相关

- 并发编程
    - [Java 并发编程:ThreadLocal 的使用及其源码实现](https://blog.csdn.net/github_35186068/article/details/83858944)

- 设计模式
    - [观察者模式](https://blog.csdn.net/github_35186068/article/details/83754026)

- 虚拟机
    - [内存管理](https://juejin.im/post/5b475e976fb9a04fa8671a45)
    - [虚拟机执行子系统](https://juejin.im/post/5b4a1fb7e51d4519213fd374)
    - [虚拟机内存模型与高效并发](https://juejin.im/post/5b4f48e75188251b1b448aa0)

- 三方库
    - [时间库 JodaTime](https://blog.csdn.net/github_35186068/article/details/83754146)

### UI 相关

- [自定义控件](系统架构/控件体系/View体系详解:自定义控件.md)(编辑中)

### 编程基础

- 数据库
    - [MySQL 基础知识(全)](https://juejin.im/post/5a12d62bf265da431d3c4a01)

### 面试题

> 通过面试题梳理知识点细节

- [Android高级面试_1_Handler相关](笔试面试/Android高级面试_1_Handler相关.md)
- [Android高级面试_2_IPC相关](笔试面试/Android高级面试_2_IPC相关.md)
- [Android高级面试_3_语言相关](笔试面试/Android高级面试_3_语言相关.md)
- [Android高级面试_4_虚拟机相关](笔试面试/Android高级面试_4_虚拟机相关.md)
- [Android高级面试_5_四大组件、系统源码等](笔试面试/Android高级面试_5_四大组件、系统源码等.md)
- [Android高级面试_6_性能优化](笔试面试/Android高级面试_6_性能优化.md)
- [Android高级面试_7_三方库相关](笔试面试/Android高级面试_7_三方库相关.md)
- [Android高级面试_8_热修补插件化等](笔试面试/Android高级面试_8_热修补插件化等.md)
- [Android高级面试_9_网络基础](笔试面试/Android高级面试_9_网络基础.md)
- [Android高级面试_10_跨平台开发](笔试面试/Android高级面试_10_跨平台开发.md)
- [Android高级面试_11_JNINDK](笔试面试/Android高级面试_11_JNINDK.md)
- [Android高级面试_12_项目经验梳理](笔试面试/Android高级面试_12_项目经验梳理.md)
- [Android 中高级工程师面试题总结](笔试面试/Android高级软件工程师2017.md)

### 其他

- [马克笔记—Android 端开源的 Markdown 笔记应用](其他/MarkNote版本1的.md)
- [承上启下:Markdown 笔记应用 MarkNote 的重构之路](其他/MarkNote版本2.md)

## 2、资源整理




================================================
FILE: 其他/Android知识点随记.md
================================================

## 异常处理

对于未捕获的异常,借助 Thread 的静态方法来进行处理

```java
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
         defaultUncaughtExceptionHandler = eh;
     }
```

## multidex

这是因为安装应用时,有一步是使用 DexOpt 对 Dex 进行优化。这个过程会生成一个 ODex 文件,执行 ODex 的效率会比直接执行 Dex 文件的效率要高很多。在早期的 Android 系统中,DexOpt 把每一个类的方法 id 检索起来,存在一个链表结构里面。但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536 个。尽管在新版本的 Android 系统中,修复了 DexOpt 的这个问题,但是我们仍然需要对低版本的 Android 系统做兼容。

为了解决方法数超限的问题,需要启用 multidex 将该 dex文 件拆成多个。

## 动态布局

就是指服务端使用 API 下发数据信息,然后客户端根据下发的信息进行动态布局。

另一层含义可能是在代码中进行动态布局而不是使用 XML 的方式。






================================================
FILE: 其他/MarkNote版本1的.md
================================================
# 马克笔记—Android 端开源的 Markdown 笔记应用

![App 导引](https://github.com/Shouheng88/MarkNote/blob/master/resources/images/app.png?raw=true)

> 马克笔记是运行在Android设备上面的一款开源的Markdown笔记,它的功能开发得已经比较完善,已经能够满足大部分用户的需求。现在将其开源到Github上面,用来交流和学习。当然,更希望你能够参与到项目的开发当中,帮助马克笔记变得更加有用。

## 1、关于马克笔记

马克笔记是一款开源的Markdown笔记应用,它的界面设计采用了Google最新的Material Design风格。该笔记现在的功能已经比较完善,能够满足用户大多数场景的需求。开源该软件的目的是希望与更多的人交流和学习,同时也希望能够有人参与到项目的开发中,一起帮助马克笔记,让它变得更加有用。

你可以通过加入[Google+社区](https://plus.google.com/u/1/communities/102252970668657211916)来关注该软件开发的最新动态,并且可以参与Beta测试。

马克笔记现在已经发布到了[酷安网](https://www.coolapk.com/apk/178276)上面,也欢迎你下载和使用该软件。另外,笔者还开发了一款清单应用[多功能清单](https://www.coolapk.com/apk/185660),感兴趣的同学也可以了解一下。

## 2、应用展示图

<a href="#app">这里</a>是该应用的一些截图通过Photoshop调整之后得到的展示图,通过展示图,你大概可以了解一下该软件的主要功能和开发状态。在接下来的行文中,我会向你更详细地介绍它使用到的一些技术以及现在开发完成的一些功能和特性。

## 3、功能和特性

我把该软件当前已经支持的功能列了一个清单:

|编号|功能|
|:-:|:-|
|1|基本的**添加、修改、归档、放进垃圾箱、彻底删除**操作|
|2|基本的Markdown语法,外加**MathJax**等高级特性|
|3|特色的**时间线**功能,通过类似于AOP的操作记录用户的操作信息|
|4|多种形式的媒体数据,包括**文件、视频、音频、图片、手写和位置信息**等|
|5|**多主题**,支持**夜间主题**,并且有多种可选的**主题色和强调色**|
|6|多彩的**图表**用于统计用户的数据信息|
|7|三种形式的**桌面小控件**,并且可以为每个笔记添加快捷方式|
|8|允许你为笔记指定多个多彩的标签|
|9|使用“树结构”模拟文件夹操作,支持**多层文件夹**,并可以进行层级的搜索|
|10|允许将笔记**导出为PDF、TXT、MD格式的文本、HTML和图片**|
|11|使用**应用独立锁**,加强数据安全|
|12|允许用户**备份数据到外部存储空间和OneDrive**|
|13|图片**自动压缩**,节省本地的数据存储空间|

将来希望开发和完善的功能:

|编号|功能描述|
|:-:|:-|
|1|数据同步,本地的文件管理容易导致多平台的不一致,增加同步服务,能够实现多平台操作|
|2|文件服务器,用于获取图片和文件的链接|
|3|富文本编辑,即时的编辑预览|
|4|允许添加闹钟,并且复选框可以编辑|
|5|添加地图来展示用户的位置信息的变更|

你可以从[更新日志](app/src/main/res/raw/changelog.xml)中获取到软件的更新信息。

## 4、依赖和用到的一些技术

马克笔记用到了MVVM的设计模式,还用到了DataBinding等一系列技术。下面的表格中列出了用到的具体的依赖和简要的描述。在此,还要感谢这些开源项目的作者:

|编号|依赖|描述|
|:-:|:-|:-|
|1|[arch.lifecycle]()|使用ViewModel+LiveData实现Model和View的解耦|
|2|[Stetho](https://github.com/facebook/stetho)|Facebook开源的安卓调试框架|
|3|[Fabric]()|错误跟踪,用户数据收集|
|4|[RxBinding](https://github.com/JakeWharton/RxBinding)||
|5|[RxJava](https://github.com/ReactiveX/RxJava)||
|6|[RxAndroid](https://github.com/ReactiveX/RxAndroid)||
|7|[OkHttp](https://github.com/square/okhttp)||
|8|[Retrofit](https://github.com/square/retrofit)||
|9|[Glide](https://github.com/bumptech/glide)||
|10|[BRVAH](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|非常好用的Recycler适配器|
|11|[Gson](https://github.com/google/gson)||
|12|[Joda-Time](https://github.com/JodaOrg/joda-time)|Java时间库|
|13|[Apache IO](http://commons.apache.org/io/)|文件操作库|
|14|[Material dialogs](https://github.com/afollestad/material-dialogs)||
|15|[PhotoView](https://github.com/chrisbanes/PhotoView)||
|16|[Hello charts](https://github.com/lecho/hellocharts-android)||
|17|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)||
|18|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)||
|19|[CircleImageView](https://github.com/hdodenhof/CircleImageView)||
|20|[Changeloglib](https://github.com/gabrielemariotti/changeloglib)|日志信息|
|21|[PinLockView](https://github.com/aritraroy/PinLockView)|锁控件|
|22|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)|底部弹出的对话框|
|23|[Luban](https://github.com/Curzibn/Luban)|图片压缩|
|24|[Flexmark](https://github.com/vsch/flexmark-java)|基于Java的Markdown文本解析|
|25|[PrettyTime](https://github.com/ocpsoft/prettytime)|时间格式美化|


特别需要说明的一点是,马克笔记是在开发了一段时间之后重新引入的ViewModel,因为作者本人水平有限,或者对ViewModel理解不够深入,设计难免有不足的地方,还请批评指正。

### 数据库操作

对于数据库部分,笔者自己设计了一套数据的访问逻辑,这里使用到了模板和单例等设计模式。它的好处在于,当你想要向程序中添加一个数据库实体的时候,只需要很少的配置即可,可以省去很多的样板代码。而且,由于该项目的一些特殊需求,比如要记录统计信息等,所以就自己设计了一下。当然,可能性能上仍然有许多值得提升的地方,但笔者认为仍不失为一个简单的学习材料。

### Markdown解析

对于Markdown解析,可以使用js在webview里面解析,也可以像本项目一样在程序种用java进行解析。笔者认为使用Flexmark在java种解析的好处是更方便地对解析的功能进行拓展。如该软件中的MathJax的解析就是在Flexmark的基础上进行的拓展。

## 5、参与项目

正如一开始提及的那样,马克笔记仍然有许多不足,我希望可以有更多的人帮助马克笔记继续完善它的功能。当然,这并不勉强。如果你希望对该项目贡献代码,你可以fork该项目,并向该项目提交请求。你可以在[waffle.io](https://waffle.io/Shouheng88/NotePal)上面跟踪issue的开发状态。或者,你发现了该软件中存在的一些问题,你可以在issue中向开发者报告。如果有其他的需求,可以直接通过[邮箱](mailto:shouheng2015@gmail.com)邮件开发者。

## 6、项目地址

因为这篇文章是从Github的Readme文件中拷贝出来的,所以忘记加上Github地址了,抱歉。现在补上:[Github](https://github.com/Shouheng88/MarkNote)

================================================
FILE: 其他/MarkNote版本2.md
================================================
# 承上启下:Markdown 笔记应用 MarkNote 的重构之路

## 1、关于项目

**MarkNote** 是一款 Android 端的笔记应用,它支持非常多的 Markdown 基础语法,还包括了 MathJax, Html 等各种特性。此外,你还可以从相机或者相册中选择图象并将其添加到自己的笔记中。这很酷!因为你可以将自己的游记或者其他图片拍摄下来并将其作为自己笔记的一部分。这也是笔者开发这款软件的目的——希望 MarkNote 能够成为一款帮助用户记录自己生活的笔记应用。

下面是我自己制作的一张部分功能预览图。这里仅仅列举了其中的部分页面,当然,你可以在酷安网或者 Google Play Store 上面获取到这个应用程序,并进一步了解它的全部功能,也可以在 Github 上得到最新版的应用的全部源代码。

 ![预览图](https://camo.githubusercontent.com/bea0cac9b6a352211c658024615419a785bc7fe6/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a553137333943454b3259667241726a376651546230672e6a706567)

项目相关的**链接**:

1. [酷安网下载链接:https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276)
2. [Google Play Store 下载:https://play.google.com/store/apps/details?id=me.shouheng.notepal](https://play.google.com/store/apps/details?id=me.shouheng.notepal)
3. [Github 项目链接:https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote)

**最后**,之所以把这次重构称为 “承上启下” 的一个很重要的原因是:这次重构代码其实是为了后续功能的开发铺路。在未来,我会为这个应用增加更多有趣的功能。如果你对该项目感兴趣的话,可以 **Star** 或者 **Fork** 该项目,并为项目贡献代码。我们欢迎任何的、即使很小的贡献 :)

## 2、关于重构

在之前的版本中,MarkNote 在功能、界面和代码方面都存在一些不足,所以,前些日子我又专门抽了些时间对这些不足的地方进行了一些优化,时间大概从 11 月中旬直到 12 月中旬。这次重构也进行了大量的代码优化。经过这次重构,项目增加了大概 100 多次 commit. 下面我们列举一下本次重构所涉及的部分,其实也是这段时间以来学习到的东西的一些总结。

### 2.1 项目结构优化

#### 2.1.1 包结构优化

首先,在之前笔者已经对项目的整个结构做了一次调整,主要是将项目中各个模块的位置进行了调整。这部分内容主要是项目中的 Gradle 配置和项目文件的路径的修改。在 `settings.gradle` 里面,我按照下面的方式指定了依赖的各个模块的路径:

    include ':app', ':commons', ':data', ':pinlockview', ':fingerprint'
    project(':commons').projectDir = new File('../commons')
    project(':data').projectDir = new File('../data')
    project(':pinlockview').projectDir = new File('../pinlockview')
    project(':fingerprint').projectDir = new File('../fingerprint')

这种方式最大的好处就是,项目中的 `app`, `commons`, `data` 等模块的文件路径处于相同的层次中,即:

    --MarkNote
         |----client
         |----commons
         |----data
         ....

这个调整当然是为了组件化开发做准备啦,当然这样的结构相比于将各个模块全部放置在 `client` 下面清晰得多。

其次,我将项目中已经比较成熟的部分打包成了 `aar`,并直接引用该包,而不是继续将其作为一个依赖的形式。这样又进一步简化了项目的结构。

最后是项目中的功能模块的拆分。在之前的项目中,Markdown 编辑器和解析、渲染相关的代码都被我放置在项目所引用的一个模块中。而这次,我直接将这个部分拆成了一个单独的项目并将其开源到了 Github. 

![EasyMark](res/easymark.png)

这么做的主要目的是:

1. 将核心的功能模块从项目中独立出来单独开发,以实现更多的功能并提升该部分的性能;
2. 开源,希望能够帮助想实现一个 Markdown 笔记的开发者快速集成这个功能;
3. 开源,希望能够有开发者参与进行以提升这部分的功能。

关于 Markdown 处理的部分被开源到了 Github,其地址是:https://github.com/Shouheng88/EasyMark ,该项目中同时还包含了一个非常好用的编辑器菜单控件,感兴趣的同学可以关注一下这个项目。

#### 2.1.2 MVVM 调整

在该项目中,我们一直使用的是最新的 MVVM 设计模式,只是可惜的是在之前的版本中,笔者对 MVVM 的理解不够深入,所以导致程序的结构更像是 MVP. 本次,我们对这个部分做了优化,使其更符合 MVVM 设计原则。

以笔记列表界面为例,当我们获取了对应于 Fragment 的 ViewModel 之后,我们统一在 `addSubscriptions()` 方法中对其通知进行订阅:

        viewModel.getMutableLiveData().observe(this, resources -> {
            assert resources != null;
            switch (resources.status) {
                case SUCCESS:
                    adapter.setNewData(resources.data);
                    getBinding().ivEmpty.showEmptyIcon();
                    break;
                case LOADING:
                    getBinding().ivEmpty.showProgressBar();
                    break;
                case FAILED:
                    ToastUtils.makeToast(R.string.text_failed);
                    getBinding().ivEmpty.showEmptyIcon();
                    break;
            }
        });

这里返回的 resources,是封装的 `Resource` 的实例,是用来向观察者传递程序执行结果的包装类。然后,我们会使用 ViewModel 的 `fetchMultiItems()` 方法来根据之前传入的页面的状态信息拉取笔记记录:

    public Disposable fetchMultiItems() {
        if (mutableLiveData != null) {
            mutableLiveData.setValue(Resource.loading(null));
        }
        return Observable.create((ObservableOnSubscribe<List<NotesAdapter.MultiItem>>) emitter -> {
            List<NotesAdapter.MultiItem> multiItems = new LinkedList<>();
            List list;
            if (category != null) {
                switch (status) {
                    case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(category);break;
                    case TRASHED: list = TrashHelper.getNotebooksAndNotes(category);break;
                    default: list = NotebookHelper.getNotesAndNotebooks(category);
                }
            } else {
                switch (status) {
                    case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(notebook);break;
                    case TRASHED: list = TrashHelper.getNotebooksAndNotes(notebook);break;
                    default: list = NotebookHelper.getNotesAndNotebooks(notebook);
                }
            }
            for (Object obj : list) {
                if (obj instanceof Note) {
                    multiItems.add(new NotesAdapter.MultiItem((Note) obj));
                } else if (obj instanceof Notebook) {
                    multiItems.add(new NotesAdapter.MultiItem((Notebook) obj));
                }
            }
            emitter.onNext(multiItems);
        }).observeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(multiItems -> {
            if (mutableLiveData != null) {
                mutableLiveData.setValue(Resource.success(multiItems));
            }
        });
    }

从上面也可以看出,我们将从数据库中获取到数据的许多逻辑放在了 ViewModel 中,并且每当想要拉取数据的时候调用一下 `fetchMultiItems()` 方法即可。这样,我们可以大大地减少 View 层的代码量。 View 层的逻辑也因此变得清晰得多。

### 2.2 界面优化:更纯粹的质感设计

记得在 Material Design 刚推出的时候,笔者和许多其他开发者一样兴奋。不过,在实际的开发过程中我却总是感觉不得要领,总觉少了一些什么。不过,经过前段时间的学习,我对在应用中实现质感设计有了更多的认识。

#### 2.2.1 Toolbar 的阴影效果

在之前的版本中,为了实现工具栏下面的阴影效果,我使用了在 Toolbar 下面增加一个高度为 `5dp` 的控件并为设置一个渐变背景的实现方式。这种实现方式可以完美兼容 Android 系统的各个版本。但是,这种实现的效果没有系统自带的显得那么自然。在新的版本中,我使用了下面的方式来实现阴影的效果:

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".activity.SearchActivity">

        <me.shouheng.commons.widget.theme.SupportAppBarLayout
            android:id="@+id/bar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="?attr/colorPrimary">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"/>

        </me.shouheng.commons.widget.theme.SupportAppBarLayout>

    ...

这里的 `SupportAppBarLayout` 继承自支持包的 `AppBarLayout`,主要用来实现日夜间主题的兼容。这样 Toolbar 下面就会带有一个漂亮的阴影,但是在比较低版本的手机上面是没有效果的,所以,为了兼容低版本的手机还要使用之前的那种使用控件填充的方式。(在新版本中暂时没有做这个处理)

#### 2.2.2 日夜间主题兼容

在之前的项目中,支持 20 多种主题颜色和强调色,不过最近随着 Google 在自己的项目中逐渐采用纯白色的设计,我也抛弃了之前的逻辑。现在整个项目中只支持三种主题:

1. 白色的主题 + 蓝色的强调色
2. 白色的主题 + 粉红的强调色
3. 黑色的主题 + 蓝色的强调色

![主题](res/themes.jpg)

对于主题的支持,我依然延续了之前的实现方式——通过重建 Activity 来实现主题的切换。同时,为了达到某些控件随着主题自适应调整的目的,我定义了一些自定义控件,并在其中根据当前的设置选择使用的颜色。而对于其他可以直接使用项目中的强调色或者主题色的部分,我们可以直接使用当前的主题的值,比如下面的 Toolbar 的背景颜色会使用当前主题中的 `主题色`:

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"/>

#### 2.2.3 启动页优化

之前的版本中在第一次打开程序的时候会有一个启动页来展示程序的功能,新版本中直接移除了这个功能。取而代之的是使用启动页来进行优化,首秀定义一个主题。这个主题只应用于第一次打开的 Activity。

    <style name="AppTheme.Branded" parent="LightThemeBlue">
        <item name="colorPrimaryDark">#00a0e9</item>
        <item name="android:windowBackground">@drawable/branded_background</item>
    </style>

这里,我们将界面的背景更换成我们自己的项目的图标,因为项目图标中使用的颜色与状态栏的颜色不一致,所以,这里又重写了 `colorPrimaryDark` 属性以将状态栏的颜色和启动页的颜色设置成相同的效果:

    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item>
            <color android:color="#00a0e9"/>
        </item>
        <item>
            <bitmap
                android:src="@drawable/mn"
                android:tileMode="disabled"
                android:gravity="center"/>
        </item>
    </layer-list>

这种实现方式的效果是,在程序打开的时候不会存在白屏。之前的白屏会被我们指定的启动页替换掉(因为这个启动页是该 Activity 的窗口的背景)。当然,当页面打开完毕之后你还要在程序中将启动页背景替换掉。这样优化之后程序打开的时候显得更加自然、流畅。

#### 2.2.4 动画优化

因为时间的原因,在当前的版本中,我并没有加入太多的动画,而只是对程序中的一些地方增加了动画的效果。

在笔记的列表中,我使用了下面的动画效果。这样当打开列表界面的时候各个条目会存在自底向上的进入动画。

    private int lastPosition = -1;

    @Override
    protected void convert(BaseViewHolder helper, MultiItem item) {
        // ... 
        /* Animations */
        if (PalmUtils.isLollipop()) {
            setAnimation(helper.itemView, helper.getAdapterPosition());
        } else {
            if (helper.getAdapterPosition() > 10) {
                setAnimation(helper.itemView, helper.getAdapterPosition());
            }
        }
    }

    private void setAnimation(View viewToAnimate, int position) {
        if (position > lastPosition) {
            Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.anim_slide_in_bottom);
            viewToAnimate.startAnimation(animation);
            lastPosition = position;
        }
    }

不过,这种方式实现的并不是最理想的效果,因为当打开页面的时候,多条记录会以一个整体的形式进入到页面中。这也是以后的一个优化的地方。

### 2.3 使用 RxJava 重构

在之前的项目中,当进行异步的操作的时候,需要定义一个 `AsyncTask`. 这种实现方式存在一个明显的问题,当需要执行的异步任务比较多,又无法进行复用的时候,你需要定义大量的 `AsyncTask`。另外,在各个页面之间进行数据传递的时候,如果单纯地使用 `onActivityResult()` 或者进行接口回调(Fragment 和 Activity 之间)会使得代码繁琐、难以阅读。针对这些问题,我们可以使用 RxJava 来进行很好的优化。

首先是异步操作的问题,我们可以使用 RxJava 来实现线程的切换。以下面的这段代码为例,它被用来实现保存`快速笔记`的结果到文件系统和数据库中。在这段代码中,我们使用了 RxJava 的 `create()` 方法,并在其中进行逻辑的处理,然后使用 `subscribeOn()` 方法指定处理的线程是 IO 线程,并使用 `observeOn()` 方法指定最终处理的结果在主线程中进行处理:
 
    public Disposable saveQuickNote(@NonNull Note note, QuickNote quickNote, @Nullable Attachment attachment) {
        return Observable.create((ObservableOnSubscribe<Note>) emitter -> {
            /* Prepare note content. */
            String content = quickNote.getContent();
            if (attachment != null) {
                attachment.setModelCode(note.getCode());
                attachment.setModelType(ModelType.NOTE);
                AttachmentsStore.getInstance().saveModel(attachment);
                if (Constants.MIME_TYPE_IMAGE.equalsIgnoreCase(attachment.getMineType())
                        || Constants.MIME_TYPE_SKETCH.equalsIgnoreCase(attachment.getMineType())) {
                    content = content + "![](" + quickNote.getPicture() + ")";
                } else {
                    content = content + "[](" + quickNote.getPicture() + ")";
                }
            }
            note.setContent(content);
            note.setTitle(NoteManager.getTitle(quickNote.getContent(), quickNote.getContent()));
            note.setPreviewImage(quickNote.getPicture());
            note.setPreviewContent(NoteManager.getPreview(note.getContent()));

            /* Save note to the file system. */
            String extension = UserPreferences.getInstance().getNoteFileExtension();
            File noteFile = FileManager.createNewAttachmentFile(PalmApp.getContext(), extension);
            try {
                Attachment atFile = ModelFactory.getAttachment();
                FileUtils.writeStringToFile(noteFile, note.getContent(), Constants.NOTE_FILE_ENCODING);
                atFile.setUri(FileManager.getUriFromFile(PalmApp.getContext(), noteFile));
                atFile.setSize(FileUtils.sizeOf(noteFile));
                atFile.setPath(noteFile.getPath());
                atFile.setName(noteFile.getName());
                atFile.setModelType(ModelType.NOTE);
                atFile.setModelCode(note.getCode());
                AttachmentsStore.getInstance().saveModel(atFile);
                note.setContentCode(atFile.getCode());
            } catch (IOException e) {
                emitter.onError(e);
            }

            /* Save note. */
            NotesStore.getInstance().saveModel(note);

            emitter.onNext(note);
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(note1 -> {
            if (saveNoteLiveData != null) {
                saveNoteLiveData.setValue(Resource.success(note1));
            }
        });
    }

另外是界面之间的结果传递的问题。对于 `onActivityResult()` 的执行结果,我们使用自定义的 `RxBus` 来传递信息,它的作用类似于 `EventBus`。然后,我们为此而封装了一个 `RxMessage` 对象来包装返回的结果。但是在程序中,我们尽量来简化和减少这种代码,因为过多的全局消息会让代码调试变得更加困难。我们希望代码逻辑更加简单、清晰。

RxJava 除了能够完成线程切换的任务之外,对代码的可读性的提升效果也是非常明显的。另外,它还非常适用于局部的优化,比如,我们可以很轻易地改变自己的代码来将某个耗时逻辑放在异步线程中执行来提升界面的响应速度。

### 2.4 增加新功能

#### 2.4.1 桌面快捷方式

桌面快捷方式并不是所有的 Android 桌面都支持的,我们在程序中有两个地方使用它。如下图所示,第一种方式是在笔记内部点击创建快捷方式的时候在桌面创建应用的快捷方式,我们可以通过点击快捷方式来快速打开笔记;第二种方式是长按应用图标的时候弹出一个菜单选项。

![快捷方式](res/shortcuts.jpg)

首先,第一种实现方式是在 7.0 之后加入的,之前我们也是可以创建快捷方式的,只是实现的方式与现在的方式不同而已。如下面这段代码所示,当 7.0 之后,我们使用 ShortcutManager 来创建快捷方式。之前,我们可以使用 "com.android.launcher.action.INSTALL_SHORTCUT" 这个 ACTION 并指定参数来创建快捷方式:

    public static void createShortcut(Context context, @NonNull Note note) {
        Context mContext = context.getApplicationContext();
        Intent shortcutIntent = new Intent(mContext, MainActivity.class);
        shortcutIntent.putExtra(SHORTCUT_EXTRA_NOTE_CODE, note.getCode());
        shortcutIntent.setAction(SHORTCUT_ACTION_VIEW_NOTE);

        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
            ShortcutManager mShortcutManager = context.getSystemService(ShortcutManager.class);
            if (mShortcutManager != null && VERSION.SDK_INT >= VERSION_CODES.O) {
                if (mShortcutManager.isRequestPinShortcutSupported()) {
                    ShortcutInfo pinShortcutInfo = new Builder(context, String.valueOf(note.getCode()))
                            .setShortLabel(note.getTitle())
                            .setLongLabel(note.getTitle())
                            .setIntent(shortcutIntent)
                            .setIcon(Icon.createWithResource(context, R.drawable.ic_launcher_round))
                            .build();

                    Intent pinnedShortcutCallbackIntent = mShortcutManager.createShortcutResultIntent(pinShortcutInfo);

                    PendingIntent successCallback = PendingIntent.getBroadcast(context, /* request code */ 0,
                            pinnedShortcutCallbackIntent, /* flags */ 0);

                    mShortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender());
                }
            } else {
                createShortcutOld(context, shortcutIntent, note);
            }
        } else {
            createShortcutOld(context, shortcutIntent, note);
        }
    }

    private static void createShortcutOld(Context context, Intent shortcutIntent, Note note) {
        Intent addIntent = new Intent();
        addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
        addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, note.getTitle());
        addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
                Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher_round));
        addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
        context.sendBroadcast(addIntent);
    }

对于第二种实现方式,我们可以在 Manifest 文件中进行注册,并为其指定 ACTION 和启动类来实现各个选项被点击之后发送的事件。然后,我们在指定的 Activity 中对各个 ACTION 进行处理即可,具体可以参考源代码。另外,这里的快速创建笔记还是比较有意思的,可以打开一个背景透明的 Activity 并在其中弹出一个自定义对话框来快速编辑笔记。可以帮助我们快速地记录自己的笔记。

#### 2.4.2 指纹解锁 

当然,这部分功能,我们直接使用了一个开源的三方库。毕竟人家为还为各个系统的指纹解锁的支持做了处理,所以这里我们直接奉行拿来主义了。这个项目的地址是:https://github.com/uccmawei/FingerprintIdentify. 

#### 2.4.3 打开网页的各种问题

打开网页当然不难实现,我们使用一个自定义的 WebView 即可实现。不过,在这个项目的重构版本中,我们采用了一个开源的库 AgentWeb,它可以满足我们非常多场景的应用。

另外,因为在我们的新的重构版本中,将支持包和 targetApi 都提升到了 28,所以出现了一个问题:使用 `http` 的网页无法打开。为了解决这个问题,我们需要在 Manifest 文件中指定网络配置文件的地址:

    android:networkSecurityConfig="@xml/network_security_config"

然后,在该配置文件中指定我们可以访问的 http 白名单:

    <?xml version="1.0" encoding="utf-8"?>
    <network-security-config>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="true">mikecrm.com</domain>
            <domain includeSubdomains="true">m.weibo.cn</domain>
        </domain-config>
    </network-security-config>

在这里我们还发现了一个其他的问题:我们打开网页的时候设置的 Weibo 的链接是 https 的,但是因为我们在移动设备上面使用,所以又被重定向到了 `http://m.weibo.cn`,导致我们的网页无法打开。解决的方式即按照上面那样,将重定向之后的地址添加到白名单之中即可。

#### 2.4.4 其他

1. 在新的版本中,为了帮助我们进一步优化程序,我们使用了友盟进行埋点。
2. 不注册支付宝和微信支付账号进行打赏;
3. 分享相关的逻辑等;
4. 其他:新版本中我们还增加了许多其他的逻辑,如果你感兴趣的话可以查看下代码。

## 3、总结

上面我们介绍了项目的一些内容和新版本重构时加入的新功能等。这些新加入的东西也算是这段时间以来学习成果的一个小集合。当然,因为毕竟业余时间有限,代码中可能仍然存在一些不足和设计不良的地方,如果你发现了这些不愉快的问题,可以在 Github 上面为项目提 issue,很乐意与你沟通和学习!

最后,重申一下项目相关的链接:

1. [酷安网下载链接:https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276)
2. [Google Play Store 下载:https://play.google.com/store/apps/details?id=me.shouheng.notepal](https://play.google.com/store/apps/details?id=me.shouheng.notepal)
3. [Github 项目链接:https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote)

------
**如果您喜欢我的文章,可以在以下平台关注我:**

- 博客:[https://shouheng88.github.io/](https://shouheng88.github.io/)
- 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)
- Github:[https://github.com/Shouheng88](https://github.com/Shouheng88)
- CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)
- 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)

更多文章:[Gihub: Android-notes](https://github.com/Shouheng88/Android-notes)


================================================
FILE: 其他/计算机视觉与Android.md
================================================
# Android 与计算机视觉

不管你是否从事计算机视觉相关的工作,了解这方面的内容总是好的,因为即使你现在的工作与 AI 无关,采用一些开放的 API 仍然有可能让你的应用做得更好。比如,百度开发平台就提供了许多 AI 相关的 API,像当下比较受欢迎的“白描”等应用,其实就是使用了百度的 API。所以,你也可以考虑一下能否借助一些语音和文字识别等功能来赋能自己的应用。

因为我们所做的计算机视觉的东西更多的是对图片进行处理,这就涉及到 OpenCV 和 Tensorflow 在 Android 端的应用,以及相机和 Android 端的其他图片处理逻辑。这不可避免地要用到 JNI 和 NDK 当中的一些内容。因此,在本篇文章中,我们想要讨论的内容主要包括以下几个方面的:

1. **Android 端图片压缩库封装**
2. **Android 端相机库封装和性能优化**
3. **JNI 和 NDK 调用,以及 CMake 在 Android 中的应用**
4. **OpenCV 在 Android 中的集成和应用**
5. **Tensorflow 在 Android 端的集成和应用**

其实之前的文章中我们也提到过一些今天我们想讨论的内容。所以在这里相关的技术底层的知识能带过的就直接带过。我们会给出相关的技术文章的链接,如果感兴趣的可以到指定的文章下面查看更具体的知识。

## 1、Android 端图片压缩库封装

为什么要做图像压缩呢?因为太大的图片上传速度比较慢,会影响程序的用户体验;而过分的压缩图片会导致程序识别出来的效率比较低。识别的效率每提高 1 个百分点,标注团队可能就要多标注几万张图片。经过测试发现,把图片的短边控制在 1100 左右是最合适的,那么此时我们就需要制定一个自己的压缩策略。

这个在之前的文章中我们已经讨论过,并且对 Android 端 Bitmap 相关的压缩的知识都做了介绍。您可以到下面的文章下面了解下我们是如何对图片压缩库进行封装的,以及 Android 中图片压缩的底层原理:

[开源一个 Android 图片压缩框架](https://juejin.im/post/5c87d01f6fb9a049b7813784)

当然,上面的文章在介绍的这方面的东西的时候,基于的是我们库的第一个版本,那个版本可以满足基本的功能。在后来的版本中,我们又对自己的库做了完善,增加了更多的 feature。这里我们主要介绍下新的框架相关的 API 以及后来我们如何做了兼容性的设计,以在第一个版本的基础之上进行了功能性的拓展。

在实际的使用过程中,我们发现更多的时候你需要对 Bitmap 进行处理而不是 File. 在这个时候,第一个版本的库就应用不上了。想了想,我们希望能够对自己的库进行拓展以支持更多的应用场景。这包括:

1. **在当前线程中直接获取压缩的 Bitmap 而不是通过 Callback 或者 RxJava 的形式传递结果**:因为我们有一部分代码本身就是在 RxJava 中异步执行的,回调或者使用 RxJava 会影响我们程序的逻辑结构。
2. **直接使用 Bitmap 或者 `byte[]` 作为参数进行压缩而不是先写入到 File 中,然后对文件进行读取和压缩**:这个是存在具体的应用场景,比如当你从相机当中获取数据的时候,实际获取到的是 `byte[]`,在连续拍摄的情况下,不断写入到文件再读取并进行压缩非常影响程序性能。
3. **支持直接返回 Bitmap 而不是只能返回 File 类型**:有时候我们需要对程序的局部做优化,比如图片处理结果的预览,此时,如果我们返回的是 File 的话,一样会影响我们程序的性能和逻辑结构。

最初,我们希望能够像 Glide 那样支持对自定义的数据结构进行压缩,然后自定义图片获取的逻辑,然而考虑到时间和兼容的问题,直接放弃了这个想法,转而采用更加简单、直接的方式:

1. **对入参这一块,直接使用重载函数接受不同的参数类型**;
2. **压缩的过程可以直接使用 `get()` 方法把压缩的中间结果返回给调用者**;
3. **增加 `asBitmap()` 方法,转换输出参数类型为 Bitmap 而不是 File 类型**。

因此,在后来的版本中,你可以像下面这样直接获取到 Bitmap 的结果:

```java
    Bitmap result = Compress.with(this, bytes)
            .setQuality(80)
            .strategy(Strategies.compressor())
            .setScaleMode(ScaleMode.SCALE_SMALLER) // 指定放缩的边
            .setMaxHeight(1100f)
            .setMaxWidth(1100f)
            .asBitmap()  // 调用这个方法表示期望的返回类型是 Bitmap 而不是 File
            .asFlowable()
            .subscribe(b -> {
                // do somthing
            })
```

这里关于 `asBitmap()` 方法的设计可以简单说明一下。

![Compressor 设计图](res/Compressor.png)

> 图片链接:[https://www.processon.com/view/link/5cdfb769e4b00528648784b7](https://www.processon.com/view/link/5cdfb769e4b00528648784b7])

在第一个版本中,我们采用的上图中第一个图的设计。在这里两种压缩策略均继承自 AbstractStrategy,其实上述的 `strategy()` 方法,你可以把它理解成**拐了一个弯**,也就是它返回的具体的策略,你下面能调用的方法都局限在具体的策略中。

在后来的设计中要在 `asBitmap()` 方法处返回一个具体的构建者继续按照返回 Bitmap 的逻辑进行处理。此时我们直接返回的是第二张图中的 BitmapBuilder 对象,而 Abstrategy 则依然按照返回 File 类型的逻辑走。这样我们可以轻易地在原来的基础上,通过**拐一个弯**的形式把后续的构建逻辑转移到了 BitmapBuilder 对象上面。同时,为了达到代码复用的目的,我们引入了泛型的 `RequestBuilder<T>`。这样 AbstractStrategy 和 BitmapBuilder 只需要实现它,并指定各自的资源类型即可。又因为按照之前的逻辑,我们一直在构建的都是 AbstractStrategy 对象,因此,我们只需要把 AbstractStrategy 作为参数传入到具体的 RequestBuilder 里面就可以从它上面直接获取 Bitmap 了。(Bitmap 是在之间串联的“通用货币”。)这样我们既复用了大量的代码又在兼容原始版本的基础上进行了功能的拓展,妙极!

## 2、Android 相机库封装和性能优化

对于一个 ToC 的应用来说,用户体验直观重要。按照我们的业务场景,如果使用拍照识别的效率比人工操作的效率还要低的话,那么人工智能似乎就没有存在的必要了。毕竟我们的目标是提升别人的工作效率,所以在相机这块就必须做到快速响应。

在项目的初期我们使用的是 Google 开源的 [CameraView](https://github.com/google/cameraview). 然而在实际的应用过程中,我们逐渐发现这个库存在大量不好的设计,影响了我们程序的应用性能:

![相机启动 TraceView 分析](https://user-gold-cdn.xitu.io/2019/4/23/16a4ac1ac0d8a697?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

> 配图是我们使用 TraceView 对程序执行过程进行的性能分析

1. **没必要的数据结构构建,影响相机启动速率**:首先,当从相机的属性当中读取相机支持的尺寸的时候它会使用这些参数构建一个尺寸的宽高比到尺寸列表的哈希表结构。然后具体的运算的时候从这个哈希表中读取尺寸再进行计算。这样设计很不好!因为当需要计算尺寸的时候遍历一遍尺寸列表可能并不会占用太多的时间,并且构建的哈希表结构使用并不频繁,而在相机启动阶段进行不必要的计算反而影响了相机的启动速率。

2. **打开相机的操作在主线程当中执行,影响界面响应速率**:前提是界面能够快速响应用户,即使打开的是一个黑色的等待界面也比按下没有响应更容易接受。通过 TraceView 我们发现相机 `open()` 的过程大概占用了相机启动速率的 25%。因此把这个方法的调用放在主线程中是不太合适的。

3. **相机不支持视频拍摄和预览**:这个库是不支持相机的视频拍摄和预览的。毕竟作为计算机视觉的一部分的实时处理也是很重要的一部分。就算当前的项目中没有这方面的功能,我们也应该考虑对这方面的功能进行支持。(这方面的内容基本就是 OpenGL + Camera)

于是乎,我们自己开发的一款相机库就诞生了。当然当初开发的一个原因也是希望能够支持 OpenGL。只是时间太有限,暂时还没有太多时间去关注这些问题:

![相机库设计 UML 建模图](https://user-gold-cdn.xitu.io/2019/4/23/16a4aae65580a62c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

> 图片链接:[https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049](https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049)

关于这个库,我只是把它所有的逻辑实现了一遍,并且在我的手机上面调试没有什么问题。如果具体应用到生存环境当中还需要更多的测试和验证。关于 Android 相机开发的知识,主要覆盖 Camera1 和 Camera2 两块内容。一个方法的实现逻辑看懂了,其他方法的实现与之类似,具体的内容可以参考项目的源码。因为本人当前时间和精力有限,所以暂时无法详细讲解相机 API 的使用。

我们可以简单概括一下这份设计图当中的一些内容:

1. **三种主要设计模式**:

    1. **门面模式**:考虑到兼容 Camera1 和 Camera2 的问题,我们需要对外提供统一的 API 调用,所以,我们考虑了使用门面模式来做一个统一的封装。这里定义了 Camera1Manager 和 Camera2Manager 两个实现,分别对应于两种不同的相机 API. 它们统一继承自 CameraManager 这个门面接口。这种设计模式的好处是对外是统一的,这样结合具体的工厂+策略模式,我们可以让用户在 Camera1 和 Camera2 之间自由选择。

    2. **策略模式+工厂模式**:因为考虑到各种不同应用场景的兼容,我们希望能够用户提供最大的自由度。所以,我们采用了策略的方式,对外提供了接口给用户来计算最终想要得到的相机尺寸等参数。所以这里我们定义了一个名为 `ConfigurationProvider` 的类,它是一个单例的类,除了负责获取相机参数的计算策略,同时肩负着内存缓存的责任。这样对于很多参数的计算,包括预览尺寸、照片尺寸、视频尺寸等可以让用户自由来指定具体的大小。

2. **三个主要的优化点**:

    1. **内存缓存优化**:实际上作为相机属性的相机所支持尺寸等信息是不变的,使用内存缓存缓存这些数据之后下次就无需再次获取并进行处理,这样可以在下次相机启动的时候显著提升程序响应的速率。

    2. **延迟初始化,不使用不计算**:为了提升程序的响应速率,我们甚至对数字的计算也进行了优化,当然这个优化点可能效果没有那么明显,但是如果你愿意精益求精的话,这也可以当作一个优化点。目前程序里面还是使用了浮点数进行计算,在早期对于作为哈希表映射的键的字段,我们甚至直接使用移位预算。当然这种优化的效果还要取决于整体的数据量,并且数据量越大的时候优化效果越明显。

    3. **异步线程优化**:在早期的版本中,我们使用的是私有锁进行线程优化。因为要把线程的 `open()` 和设置参数放在两个线程当中进行,因此不可避免地要遇到线程安全问题。而所谓的私有锁其实就类似于 `Collections.syncXXX()` 所返回的同步容器。其实就是对容器的每个方法进行加锁。这样虽然可以解决问题,但是程序的结构不好看。所以,后来的版本中,我们直接使用 HandlerThread 来进行异步的调用。所谓的 HandlerThread,顾名思义,就是 Handler 和 Thread 的结合。

(更多的内容可以参考:[Android 相机库开发实践](https://juejin.im/post/5cbf2667f265da037875980a))

## 3、JNI 和 NDK 调用,以及 CMake 在 Android 中的应用 

在之前我们想在在 Java 或者 Android 中调用 C++ 代码是比较复杂的。这需要进行动态或者静态的注册。对于静态注册的方式,你需要一步一步地进行编译;对于动态注册的方式,你需要把方法一个个地在 native 层进行注册。不过后来有了 CMake 之后一切都变得简单了。当然对于 CMake,如果做过 native 的同学肯定不会陌生。对于一般应用层开发的同学,其实也可以了解下它。因为,有了它之后你可以很容易地把你的一部分实现逻辑放在 native 层里,而 native 层相对于 Java 层比较安全,而且借助 C++ 和 NDK 你可以做出更多有趣的东西。

要在 Android 端使用 CMake 而是很简单的,你需要首先在 AS 里面安装下相关的 SDK 工具:

![CMake 开发环境搭建](https://user-gold-cdn.xitu.io/2019/3/2/1693c6828071d2a6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

然后,你需要在 Gradle 里面做简单的配置:

![CMake gradle 配置](https://user-gold-cdn.xitu.io/2019/3/2/1693c83b5585c77b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

当然,虽然我们这样将其来容易,但是进行配置的时候可能需要很多的相关的专业知识。

这里面会配置到一个 CMake path,它就是指向的 CMake 的配置文件 `CMakeLists.txt`。通常我们程序中要用到的一些三方的 native 库就需要在这个地方进行配置。比如下面的就是之前项目里面的 CMake 的配置。这里面配置了一些 OpenCV 的库以及我们自己的代码所在的位置,并且引用了 NDK 里面的一些相关的库:

```cmake
# 设置要求的 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

# 指定头文件的目录
include_directories(opencv/jni/include
        src/main/cpp/include
        ../../common)

add_library(opencv_calib3d STATIC IMPORTED)
add_library(opencv_core STATIC IMPORTED)
# ....

#if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)
#    add_library(tbb STATIC IMPORTED)
#endif()

set_target_properties(opencv_calib3d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_calib3d.a)
set_target_properties(opencv_core PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_core.a)
set_target_properties(opencv_features2d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_features2d.a)
# ....

add_library(everlens
        SHARED
        src/main/cpp/img_proc.cpp
        src/main/cpp/img_cropper.cpp
        src/main/cpp/android_utils.cpp
        ../../common/EdgeFinder.cpp
        ../../common/ImageFilter.cpp)

find_library(log-lib
        log)

find_library(jnigraphics-lib
        jnigraphics)

if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)
    target_link_libraries(
            my_library
            opencv_stitching
            opencv_features2d
            # ....

            ${log-lib}
            ${jnigraphics-lib})
else()
    target_link_libraries(
            my_library
            opencv_stitching
            opencv_features2d
            # ....

            ${log-lib}
            ${jnigraphics-lib})
endif()
```

对于 CMake 的一些指令,之前也进行过一些总结,并且对指定了官方文档的地址。如果想要了解的话,可以到文章下面了解更多的内容:

[常用的 CMake 指令总结](https://blog.csdn.net/github_35186068/article/details/88639757)

使用 CMake 的好处主要是 AS 支持得比较好:

1. 可以根据 native 层代码到 Java 层代码之间的关系,鼠标左键 + Ctrl 即可直接完成 native 层方法和 Java 层方法之间的跳转;

2. 无需进行繁琐的动态注册和静态注册,只需要在 CMake 和 Gradle 当中进行配置,可以把注意力更多地放在自己的代码的逻辑实现上。

当然,就算使用了 CMake 有时候还是需要了解一些 JNI 中动态注册的内容,因为有时候当你在 native 层中从 Java 层传入的对象上面获取信息的时候还是需要进行动态注册。比如,

```C++
#include <jni.h>
#include <string>
#include <android_utils.h>

// 定义一个结构体及实例 gPointInfo
static struct {
    jclass jClassPoint;
    jmethodID jMethodInit;
    jfieldID jFieldIDX;
    jfieldID jFieldIDY;
} gPointInfo;

// 初始化 Class 信息,注意下映射关系是如何表达的,其实就类似于反编译之后的注释
static void initClassInfo(JNIEnv *env) {
    gPointInfo.jClassPoint = reinterpret_cast<jclass>(env -> NewGlobalRef(env -> FindClass("android/graphics/Point")));
    gPointInfo.jMethodInit = env -> GetMethodID(gPointInfo.jClassPoint, "<init>", "(II)V");
    gPointInfo.jFieldIDX = env -> GetFieldID(gPointInfo.jClassPoint, "x", "I");
    gPointInfo.jFieldIDY = env -> GetFieldID(gPointInfo.jClassPoint, "y", "I");
}

// 动态注册,在这里初始化
extern "C"
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_FALSE;
    }
    initClassInfo(env);
    return JNI_VERSION_1_4;
}
```

(更多的内容可以参考:[在 Android 中使用 JNI 的总结](https://juejin.im/post/5c79f5d0518825347a56275f))

## 4、OpenCV 在 Android 中的集成和应用:图片裁剪以及透视变换

### 4.1 关于 OpenCV 的集成

当然,不引用 OpenCV 的 C++ 库,直接使用别人封装好的 Java 库也是可以的,这取决于具体的应用场景。比如,如果你不需要实现特别复杂的功能,只需要简单的图像处理即可,那么别人包装过的 Java 库已经可以完全满足你的需求。但如果你像我们一样,本身需要包装和编译来自算法同学的 C++ 算法,甚至还需要使用 OpenCV 的拓展库,那么使用 Java 包装后的库可能无法满足你的需求。

下面是 OpenCV 及其拓展库的 Github 地址:

- [OpenCV](https://github.com/opencv/opencv)
- [OpenCV-contrib](https://github.com/opencv/opencv_contrib)

有了这些库你还是无法直接将其应用到程序当中的。因为上述项目得到的是 OpenCV 的源码,主要是源代码以及一些头文件,还需要对它们进行编译然后再应用到自己的项目当中。

[Build OpenCV 3.3 Android SDK on Mac OSX](https://chaoyang.nz/post/build-opencv-android-sdk/)

当然也有一些已经编译完成的 OpenCV 及其拓展库,我们可以在 CMake 中配置之后直接将其引用到我们的项目中:

[opencv3-android-sdk-with-contrib](https://github.com/chaoyangnz/opencv3-android-sdk-with-contrib)

所以最终项目的结构如下:

![OpenCV 集成之后的项目结构](res/QQ截图20190718000949.png)

左边圈出的部分是 OpenCV 及 CMake 的一些配置,右边是封装之后的 Java 方法。

### 4.2 关于 OpenCV 的应用

OpenCV 可以用来处理做很多图片处理的工作,很多工作是使用 Android 原生的 Bitmap 无法完成的。比如,图片不规则裁剪之后的透视变换、灰度化处理等。其实,不论你在 native 层如何对图片进行处理,在 Android 当中,对 native 层的输入和 native 层的输出都是 Bitmap. 而 `OpenCV::Mat` 就像是 native 层图片处理的通用货币。所以,一个完整的图片处理的流程大致是:

1. Step1: Java 层的 Bitmap 转换成 native 层的 Mat;
2. Step2: 使用 Mat 进行图片处理;
3. Step3: 将 native 层的 Mat 转换成 Java 层的 Bitmap 并返回。

将 Java 层的 Bitmap 转换成 native 层的 Mat 你可以使用下面的方法:

```C++
#include <jni.h>
#include <android/bitmap.h>
#include "android_utils.h"

void bitmap_to_mat(JNIEnv *env, jobject &srcBitmap, Mat &srcMat) {
    void *srcPixels = 0;
    AndroidBitmapInfo srcBitmapInfo;
    try {
        // 调用 AndroidBitmap 中的方法获取 bitmap 信息
        AndroidBitmap_getInfo(env, srcBitmap, &srcBitmapInfo);
        AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);
        uint32_t srcHeight = srcBitmapInfo.height;
        uint32_t srcWidth = srcBitmapInfo.width;
        srcMat.create(srcHeight, srcWidth, CV_8UC4);
        // 根据 bitmap 的格式构建不同通道的 Mat
        if (srcBitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            Mat tmp(srcHeight, srcWidth, CV_8UC4, srcPixels);
            tmp.copyTo(srcMat);
            cvtColor(tmp, srcMat, COLOR_RGBA2RGB);
        } else {
            Mat tmp = Mat(srcHeight, srcWidth, CV_8UC2, srcPixels);
            cvtColor(tmp, srcMat, COLOR_BGR5652RGBA);
        }
        AndroidBitmap_unlockPixels(env, srcBitmap);
        return;
    } catch (cv::Exception &e) {
        AndroidBitmap_unlockPixels(env, srcBitmap);
        // 构建一个 Java 层的异常并将其抛出
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, e.what());
        return;
    } catch (...) {
        AndroidBitmap_unlockPixels(env, srcBitmap);
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, "unknown");
        return;
    }
}
```

这里主要是先从 Bitmap 中获取图片具体的信息,这里调用了 NDK 里面的图像相关的一些方法。然后利用得到的图片尺寸信息和颜色信息构建 OpenCV 里面的 Mat. Mat 就类似于 MATLAB 里面的矩阵,它包含了图像的像素等信息,并且也提供了类似于 `eye()`, `zeros()` 等类似的方法用来构建特殊的矩阵。

将 Bitmap 转换成 Mat 之后就是如何使用它们了。下面是一份用来对图片进行裁剪和透视变换的算法:

```C++
// 将 Java 层的顶点转换成 native 层的 Point 对象
static std::vector<Point> pointsToNative(JNIEnv *env, jobjectArray points_) {
    int arrayLength = env->GetArrayLength(points_);
    std::vector<Point> result;
    for(int i = 0; i < arrayLength; i++) {
        jobject point_ = env -> GetObjectArrayElement(points_, i);
        int pX = env -> GetIntField(point_, gPointInfo.jFieldIDX);
        int pY = env -> GetIntField(point_, gPointInfo.jFieldIDY);
        result.push_back(Point(pX, pY));
    }
    return result;
}

// 裁剪并且透视变化
extern "C" JNIEXPORT void JNICALL
Java_xxxx_MyCropper_nativeCrop(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray points_, jobject outBitmap) {
    std::vector<Point> points = pointsToNative(env, points_);
    if (points.size() != 4) {
        return;
    }
    // 取出四个顶点
    Point leftTop = points[0], rightTop = points[1], rightBottom = points[2], leftBottom = points[3];

    // 获取源图和结果图对应的 Mat
    Mat srcBitmapMat, dstBitmapMat;
    bitmap_to_mat(env, srcBitmap, srcBitmapMat);
    AndroidBitmapInfo outBitmapInfo;
    AndroidBitmap_getInfo(env, outBitmap, &outBitmapInfo);
    int newHeight = outBitmapInfo.height, newWidth = outBitmapInfo.width;
    dstBitmapMat = Mat::zeros(newHeight, newWidth, srcBitmapMat.type());

    // 将图片的顶点放进集合当中,用来调用透视的方法
    std::vector<Point2f> srcTriangle, dstTriangle;

    srcTriangle.push_back(Point2f(leftTop.x, leftTop.y));
    srcTriangle.push_back(Point2f(rightTop.x, rightTop.y));
    srcTriangle.push_back(Point2f(leftBottom.x, leftBottom.y));
    srcTriangle.push_back(Point2f(rightBottom.x, rightBottom.y));

    dstTriangle.push_back(Point2f(0, 0));
    dstTriangle.push_back(Point2f(newWidth, 0));
    dstTriangle.push_back(Point2f(0, newHeight));
    dstTriangle.push_back(Point2f(newWidth, newHeight));

    // 获取一个映射的转换矩阵
    Mat transform = getPerspectiveTransform(srcTriangle, dstTriangle);
    warpPerspective(srcBitmapMat, dstBitmapMat, transform, dstBitmapMat.size());

    // 将 Mat 转换成 Bitmap 输出到 Java 层
    mat_to_bitmap(env, dstBitmapMat, outBitmap);
}
```

算法最终的输出结果:

<div align="middle"><img src="res/Screenshot_20190718-003619.jpg" alt="透视变化和图像切割" height="300" /></div>

## 5、Tensorflow 在 Android 端的集成和应用:图片边缘检测

在之前对图片的边缘进行检测的时候,因为发现 OpenCV 算法效果不太理性,所以后来选择使用 TensorFlow 对图片进行边缘检测。这就涉及到在 Android 端集成 Tensorflow Lite。前段时间也看到爱奇艺的 SmartVR 的介绍。借助一些官方的资料,在 Android 端使用 TF 并不难。在 Tensorflow 的开源仓库中已经有一些 Sample 可供参考:

- [Tensorflow github repository](https://github.com/tensorflow/tensorflow)
- [Tensorflow lite offical](https://www.tensorflow.org/lite)

做边缘检测当然有些大材小用的味道,但是对于我们 Android 开发者来说,借这个机会了解如何在 Android 端集成一些 Tensorflow 也可以拓展一下。毕竟这种东西属于当下比较热门的东西,说不定哪天心血来潮自己训练一个模型呢 :)

在 Android 端引入 Tensorflow 并不复杂,只需要添加相关的仓库以及依赖:

```groovy
allprojects {
    repositories {
        jcenter()
        maven { url 'https://google.bintray.com/tensorflow' }
    }
}

dependencies {
    // ...
    // tensorflow
    api 'org.tensorflow:tensorflow-lite:0.0.0-nightly'
}
```

困难的地方在于如何对训练的模型的输入和输出进行处理。因为所谓的模型,你可以将其理解成锻炼出来的一个函数,你只需要按照要求的输入,它就可以按照固定的格式给你返回一个输出。所以,具体的输入和输出是什么样的还要取决于锻炼模型的同学。

在我们之前开发的时候,最初是训练模型的同学使用 Python 代码调用的模型。使用 Python 虽然代码简洁,但是对于客户端开发简直就是噩梦。因为像 [NumPy](https://numpy.org/) 和 [Pillow](https://pillow.readthedocs.io/en/stable/) 这种函数库,一行代码的任务,你可能要“翻译”很久。后来,我们使用的是 `C++ + OpenCV` 的形式。对于 iOS 开发,因为他们可以使用 Object-C 与 C++ 混编,所以比较轻松。对于 Android 开发则需要做一些处理。

下面是加载模型以及在调用 Tensorflow 之前在 Java 层所做的一些处理:

```java
public class TFManager {

    private static TFManager instance;

    private static final float IMAGE_MEAN = 128.0f;
    private static final float IMAGE_STD = 128.0f;

    private Interpreter interpreter;
    private int[] intValues;
    private ByteBuffer imgData;
    private int inputSize = 256;

    public static TFManager create(Context context) { // DCL
        if (instance == null) {
            synchronized (TFManager.class) {
                if (instance == null) {
                    instance = new TFManager(context);
                }
            }
        }
        return instance;
    }

    // 从 Assets 中加载模型,用来初始化 TF
    private static MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename) throws IOException {
        AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename);
        FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
        FileChannel fileChannel = inputStream.getChannel();
        long startOffset = fileDescriptor.getStartOffset();
        long declaredLength = fileDescriptor.getDeclaredLength();
        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
    }

    // 初始化 TF
    private TFManager(Context context) {
        try {
            interpreter = new Interpreter(loadModelFile(context.getAssets(), "Model.tflite"));
            interpreter.setNumThreads(1);
            interpreter.resizeInput(0, new int[]{1, 256, 256, 3});
            intValues = new int[inputSize * inputSize];
            imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);
            imgData.order(ByteOrder.nativeOrder());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 边缘识别
    public EdgePoint[] recognize(Bitmap bitmap) {
        long timeStart = System.currentTimeMillis();
        imgData.rewind();
        Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true);
        scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());
        for (int i = 0; i < inputSize; ++i) {
            for (int j = 0; j < inputSize; ++j) {
                int pixelValue = intValues[i * inputSize + j];
                // 取 RBG 做归一化处理,结果在 -1 到 1 之间
                imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // R
                imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // G
                imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // B
            }
        }
        LogUtils.d("----------TFManager prepare imgData cost : " + (System.currentTimeMillis() - timeStart));

        timeStart = System.currentTimeMillis();
        Map<Integer, Object> outputMap = new HashMap<>();
        outputMap.put(0, new float[1][256][256][5]);

        Object[] inputArray = {imgData};
        // 调用 TF 进行识别
        interpreter.runForMultipleInputsOutputs(inputArray, outputMap);

        // 对识别的结果进行处理,主要是对图片的像素进行处理
        float[][][][] arr = (float[][][][]) outputMap.get(0);
        int[][] colors = new int[5][256 * 256];
        for (int i=0; i<5; i++) {
            for (int j=0; j<256; j++) {
                for (int k=0; k<256; k++) {
                    colors[i][j*256 + k] = (int) (arr[0][j][k][i] * 256);
                }
            }
        }
        LogUtils.d("----------TFManager handle TF result cost : " + (System.currentTimeMillis() - timeStart));

        timeStart = System.currentTimeMillis();
        // 将得到的图片像素按照固定的格式交给 native 层继续进行边缘识别
        EdgePoint[] points = ImgProc.findEdges(bitmap, colors);
        LogUtils.d("----------TFManager" + Arrays.toString(points));
        LogUtils.d("----------TFManager find edges cost : " + (System.currentTimeMillis() - timeStart));
        return points;
    }
}
```

这里程序的主要执行流程是:

1. 从 Assets 的模型文件中获取打开输入流,然后从输入流中打开一个管道,这里用到了 NIO 中的一些类。然后,从管道中获取一个字节缓存区。文件读写的时候管道直接与缓冲区进行交互。除了作为一个缓存区,这个缓存区还具有内存映射的功能。类似于 mmap 吧,主要是为了提升文件读写的效率。

2. 初始化并配置 Tensorflow,上面的一些参数用来设置线程数量等信息,比较简单。后面等一些参数主要用来按照模型等要求对 TF 进行调整。比如,我们使用模型来判断图片的顶点的时候使用的是只包含 RGB 三个纬度的 256 * 256 的图片。所以,这里使用了下面几行代码来进行设置:

```java
// inputSize = 256;
interpreter.resizeInput(0, new int[]{1, 256, 256, 3});
intValues = new int[inputSize * inputSize];
// 256 * 256 的图片,3 个纬度,四张图
imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);
```

3. 对要识别对图片进行处理。这里需要先对图片进行放缩,将其控制到 256 * 256 的大小。然后,使用 Bitmap 的方法获取图片的像素。下面的几行代码是对图片对 RBG 三种色彩提取,并分别对其进行归一化处理。处理之后对结果统一写入到 imgData 当中,作为模型对输入。

4. 按照模型对输出对文件的格式构建一个 Java 对象作为模型的输出参数。调用模型的方法进行识别。

5. 对模型对输出结果进行处理。根据我们上述定义对模型输出 `new float[1][256][256][5]`,这里实际对含义是 256 * 256 的 5 张图片。因为模型输出的数据并不是原始的像素信息,所以需要乘以 256 来得到真正的图片的像素。最后就是使用这些像素以及 Bitmap 的方法来得到最终的 Bitmap.

上面调用完了模型但是整个流程还没有结束。因为只是调用模型得到了五张识别之后的图片。这五张图片就是只留下了图片边缘的边框,所以想要得到图片的顶点还需要继续对这五张图进行处理。这部分需要一些算法,虽然在 Java 层去判断也是可以的,但是在 native 层,借助 OpenCV 的一些库可以使整个过程更加简单。因此,这里又要涉及一个 JNI 调用:

```C++
extern "C"
JNIEXPORT void JNICALL
Java_com_xxx_ImgProc_nativeFindEdges(JNIEnv *env, jclass type, jintArray mask1_, jintArray mask2_,
                                                        jintArray mask3_, jintArray mask4_, jintArray mask5_,
                                                        jobject origin, jobjectArray points) {
    // 从 Java 中传入的数组元素
    jint *mask1 = env->GetIntArrayElements(mask1_, NULL);
    jint *mask2 = env->GetIntArrayElements(mask2_, NULL);
    jint *mask3 = env->GetIntArrayElements(mask3_, NULL);
    jint *mask4 = env->GetIntArrayElements(mask4_, NULL);
    jint *mask5 = env->GetIntArrayElements(mask5_, NULL);

    // 原图转换成 Native 层的 mat
    Mat originMat;
    bitmap_to_mat(env, origin, originMat);

    // 构建一个集合
    std::vector<jint*> jints;
    jints.push_back(mask1);
    jints.push_back(mask2);
    jints.push_back(mask3);
    jints.push_back(mask4);
    jints.push_back(mask5);

    // 从像素点中得到对应的 Mat 并将其放到一个集合当中
    std::vector<cv::Mat> masks;
    for (int k = 0; k < 5; ++k) {
        Mat mask(256, 256, CV_8UC1);
        for (int i = 0; i < 256; ++i) {
            for (int j = 0; j < 256; ++j) {
                mask.at<uint8_t>(i, j) = (char)(*(jints[k] + i * 256 + j));
            }
        }
        masks.push_back(mask);
    }

    try {
        // 调用算法进行边缘检测
        EdgeFinder finder = ImageEngine::EdgeFinder(
                originMat, masks[0], masks[1], masks[2], masks[3], masks[4]);
        vector<cv::Point2d> retPoints = finder.FindBorderCrossPoint();
        // 将得到的“点”转换成 Java 层的对象
        jclass class_point = env->FindClass("com/xxx/EdgePoint");
        jmethodID method_point = env->GetMethodID(class_point, "<init>", "(FF)V");
        // 将顶点组成一个 Java 数组返回
        for (int i = 0; i < 4; ++i) {
            jobject point = env->NewObject(class_point, method_point, retPoints[i].x, retPoints[i].y);
            env->SetObjectArrayElement(points, i, point);
        }
    }  catch (cv::Exception &e) {
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, e.what());
        return;
    } catch (...) {
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, "unknown");
        return;
    }

    // 释放资源
    env->ReleaseIntArrayElements(mask1_, mask1, 0);
    env->ReleaseIntArrayElements(mask2_, mask2, 0);
    env->ReleaseIntArrayElements(mask3_, mask3, 0);
    env->ReleaseIntArrayElements(mask4_, mask4, 0);
    env->ReleaseIntArrayElements(mask5_, mask5, 0);
}
```

这里的主要逻辑是把之前得到的像素以及原始图片的 Bitmap 统一传入到 native 层,然后这些像素中得到 OpenCV 对应的 Mat,再一起作为算法的参数调用算法来得到顶点信息。最后把得到的顶点信息映射成 Java 层的类,并将其放在数组中返回即可。

从上面的流程中也可以看出,整个调用流程实际上进行了多次的 JNI 调用:

1. 调用 Bitmap 的方法本身就是一次 JNI 调用,调用了 Android 底层的 Skia 的库来实现图片处理;
2. TF-Lite 本身就是对 native 层方法的一个封装,调用其方法也涉及到 JNI 调用;
3. 最后是对模型识别的结果进行处理,这也涉及 JNI 调用。

JNI 调用的时候需要进行额外的转换操作,需要在函数开始的时候把 Java 层的对象转换成 native 层的对象,在算法调用完毕之后再将 native 层的对象转换成 Java 层的对象。这是我们以后可以优化的一个地方。

## 总结

以上就是计算机视觉在 Android 中的应用,主要涉及 JNI 的一些内容,以及 OpenCV 和 Tensorflow 的一些应用。再这之前介绍了图片压缩和相机库的一些封装,如果你的程序中需要一些图片处理的功能的话,我想这些东西肯定是对你有用的 :D

-------

关注作者获取更多知识:

- 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)
- Github:[https://github.com/Shouheng88](https://github.com/Shouheng88)
- CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)
- 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)
- 博客:[https://shouheng88.github.io/](https://shouheng88.github.io/)

更多知识请参考 [Github, Android-notes](https://github.com/Shouheng88/Android-notes)。


================================================
FILE: 响应式编程/RxJava系列-4:RxJava源码分析.md
================================================
# RxJava 系列-4:RxJava 源码分析

在之前的文章中我们介绍了 RxJava 2 的常用的 API 的方法总结、背压的概念以及 RxJava 2 在项目中的实际应用。在本节中,我们将要对 RxJava 2 的源码进行分析。下面是之前文章的一些链接,如果对 RxJava 2 的使用比较感兴趣,你可以通过下面的文章进行学习:

- [RxJava2 系列-1:一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd)
- [RxJava2 系列-2:背压和 Flowable](https://juejin.im/post/5b759b9cf265da283719d187)
- [RxJava2 系列-3:使用 Subject](https://juejin.im/post/5b801dfa51882542cb409905)

下面我们就从 RxJava 2 的一个简单的示例来分析下 RxJava 2 是的主流程、设计模式以及 RxJava 2 是如何实现线程切换的。

## 1、RxJava 的主流程源码分析

下面是 RxJava 的一个非常典型的使用示例,在该示例中,我们在 IO 线程中执行业务逻辑,在主线程中对执行的结果进行后续的处理。

```java
    Disposable disposable = Observable.create(new ObservableOnSubscribe<Object>() {
        @Override
        public void subscribe(ObservableEmitter<Object> emitter) throws Exception {
            // 在这里执行业务逻辑
            emitter.onNext(new Object());
        }
    }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Object>() {
        @Override
        public void accept(Object o) throws Exception {
            // 在主线程中进行后续的处理
        }
    });
    disposable.dispose();
```

我们将这段程序分成四个阶段来进行分析:1). 调用 `create()` 方法的执行过程;2). 调用 `subscribeOn(Schedulers.io())` 和 `observeOn(AndroidSchedulers.mainThread())` 实现线程切换的过程;3). 使用 `subscribe()` 进行订阅的工程;4). 调用 `dispose()` 方法取消订阅的过程。

下面先来看第一个阶段的执行过程。

### 1.1 create() 和 subscribe() 方法的执行过程

下面是调用了 `create()` 方法之后的执行过程,在下面的代码中,我们省略了 null 的检测相关的逻辑。在当前的小节中,我们假设没有指定线程切换相关的逻辑。也就是调用了 `create()` 之后,紧接着调用了 `subscribe()` 方法。

对于 RxJavaPlugins 的静态方法,比如 `onAssembly()` 等,暂时我们先不考虑它的用途。你可以将其看作直接将传入的参数的值返回。比如下面的 `create()` 方法将返回 `ObservableCreate` 的实例。

```java
    public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {
        // 看作直接返回了 new ObservableCreate<T>(source) 即可
        return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));
    }

    public final class ObservableCreate<T> extends Observable<T> {
        final ObservableOnSubscribe<T> source;

        public ObservableCreate(ObservableOnSubscribe<T> source) {
            this.source = source;
        }

        @Override
        protected void subscribeActual(Observer<? super T> observer) {
            // 对传入的观察者进行包装
            CreateEmitter<T> parent = new CreateEmitter<T>(observer);
            // 调用观察者的订阅回调方法
            observer.onSubscribe(parent);
            try {
                // 真正执行订阅的地方
                source.subscribe(parent);
            } catch (Throwable ex) {
                Exceptions.throwIfFatal(ex);
                parent.onError(ex);
            }
        }

        static final class CreateEmitter<T> extends AtomicReference<Disposable> 
            implements ObservableEmitter<T>, Disposable {
            final Observer<? super T> observer;

            CreateEmitter(Observer<? super T> observer) {
                this.observer = observer;
            }

            @Override
            public void onNext(T t) {
                if (t == null) {
                    onError(new NullPointerException("onNext ..."));
                    return;
                }
                if (!isDisposed()) {
                    // 调用传入的观察者的 onNext() 方法
                    observer.onNext(t);
                }
            }
        
            @Override
            public void dispose() {
                // 取消订阅
                DisposableHelper.dispose(this);
            }
            // ...
        }
        // ...
    }
```

上面是第一个阶段的执行过程。这里我们省去了一些代码,只保留了比较具有代表性的一些方法。也许你现在还对这部分代码看得云里雾里,没关系,看了下面的内容你会慢慢理解。

接下来我们看下当调用了 `subscribe()` 方法之后的处理。

```java
    public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,
            Action onComplete, Consumer<? super Disposable> onSubscribe) {
        // 将三种类型的观察者回调统一包装到 LambdaObserver 方法中
        LambdaObserver<T> ls = new LambdaObserver<T>(onNext, onError, onComplete, onSubscribe);
        subscribe(ls);
        return ls;
    }

    public final void subscribe(Observer<? super T> observer) {
        try {
            // 看作直接返回 observer 即可
            observer = RxJavaPlugins.onSubscribe(this, observer);
            // 调用了 subscribeActual() 方法
            subscribeActual(observer);
        } catch (NullPointerException e) {
            throw e;
        } catch (Throwable e) {
            Exceptions.throwIfFatal(e);
            RxJavaPlugins.onError(e);
            throw new NullPointerException("Actually not, but can't throw other exceptions due to RS");
        }
    }
```
上面的这些方法都定义在 Observable 中,区别只在于调用的对象。所以,为了更清晰地分析这个过程,我们使用大写字母来进行分析:

首先,整体的执行过程是,

```java
D = Observalbe.create(S).subscribe(X,Y,Z);
```

它可以被拆解成下面的两个步骤来分析(下面是一份伪代码,只是按照时间的调用顺序来排序的):

```java
A = Observable.create(S);
D = A.subscribe(X,Y,Z);
```

然后,调用 A 的 `subscribe()` 方法的时候,实际上会调用到 Observable 的 `subscribe()` 方法(就是上面的代码)。所以,按照调用的过程,上面的伪代码将变成下面这个样子,

```java
A = Observable.create(S)
O = LambdaObserver(X,Y,Z)
D = A.subscribe(O)
A.subscribeActual(O)
```

于是我们可以得知,当调用了 `subscribe()` 方法的时候,实际上调用了 A 的 `subscribeActual()` 方法,并将 B 作为参数传入。B 是 LambdaObserver,由我们调用 `subscribe()` 的时候传入的三个参数组成。那么 A 呢?回到之前的 `create()` 代码中,我们得知它就是 `ObservableCreate` 的实例。这里会调用到它的 `subscribeActual()` 方法。按照字母表示的方式,该方法将会成为下面这个样子,

```java
    @Override
    protected void subscribeActual(O) {
        P = new CreateEmitter<T>(O);
        O.onSubscribe(P);
        S.subscribe(P);
    }
```

这里的 S 是由 ObservableCreate 的构造方法传入的,也就是我们在 `create()` 方法中传入的对象。首先,这里会将 O 作为构造方法的参数传入到 `CreateEmitter` 实例中。然后,回调 O 的 `onSubscribe()` 方法并将 P 传出。这是我们常用的 RxJava 的回调方法之一。第三步中,我们调用了 S 的 `subscribe()` 方法并将 P 传出。所以,当我们按照示例代码的方式调用下面这行代码的时候,

```java
    emitter.onNext(new Object());
```

实际上是调用了这里的 P 的方法。那么,我们来看 P 的 `onNext()` 方法,

```java
    @Override
    public void onNext(T t) {
        O.onNext(t);
    }
```

它通过调用 O 的 `onNext()` 方法实现。所以,到头来,其实还是回调了我们的在 `subscribe()` 方法中传入的 Consumer 的方法。这样就通过回调的方式把我们发送的值,传递给了我们的观察方法。

### 1.2 `dispose()` 方法的执行过程

上面分析了 `create()` 和 `subscribe()` 方法的主流程。那么 `dispose()` 方法呢?

按照上面给出的代码,它的定义如下。也就是通过 `DisposableHelper` 的 `dispose()` 方法来最终完成取消订阅。

```java
    @Override
    public void dispose() {
        DisposableHelper.dispose(this);
    }
```

`DisposableHelper` 的 `dispose()` 方法的定义如下。按照上面的分析,`dispose()` 的时候传入的 this 就是 CreateEmitter. 并且它是继承了 `AtomicReference<Disposable>` 的。

```java
    public static boolean dispose(AtomicReference<Disposable> field) {
        Disposable current = field.get();
        Disposable d = DISPOSED;
        if (current != d) {
            current = field.getAndSet(d);
            if (current != d) {
                if (current != null) {
                    current.dispose();
                }
                return true;
            }
        }
        return false;
    }
```

对 AtomicReference,相比大家都不陌生,它是一个原子类型的引用。这里正式通过对该原子类型引用的赋值来完成取消订阅的——通过一个原子操作将其设置为 DISPOSED. 
            
### 1.3 RxJava 执行过程的总结

上面我们总结了 RxJava 的 Observable 从 `create()` 到 `subscribe()` 到 `dispose()` 方法的执行过程。虽然,我们依靠自己的逻辑能够把整个流程梳理下来,但是这太笨拙了。除了掌握了整个流程,我想我们更应该分析下它使用的设计思想。

一开始,当我们分析到上面的流程的时候,我也是云里雾里,但是当我继续分析了 `subscribeOn()` 的时候才恍然大悟——它整体的设计使用的设计模式和 Java 中的流是一致的。在真正分析 `subscribeOn()` 之前,我们先来看下它的代码,

```java
    public final Observable<T> subscribeOn(Scheduler scheduler) {
        return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));
    }

    public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {
        final Scheduler scheduler;

        public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {
            super(source);
            this.scheduler = scheduler;
        }

        @Override
        public void subscribeActual(final Observer<? super T> s) {
            final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);

            s.onSubscribe(parent);

            parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
        }
        // ...
    }
```

对比一下 `subscribeOn()` 方法和 `create()` 方法,我们可以很容易地发现,它们的逻辑几乎是一致的。都是传入了一个 ObservableSource 之后对其进行包装,然后在 `subscribeActual()` 方法中,得到一个 parent,然后调用 `onSubscribe()` 继而进行后续处理……也就是它和 Java 的 IO 体系一样,都使用了**装饰者设计模式**。

在 Java 的 IO 体系中,我们经常可见下面的代码。

```java
    InputStream is = new FileInputStream(fileToCopy);
    BufferedInputStream bis = new BufferedInputStream(is, 15 * 1024);
    DataInputStream dis = new DataInputStream(bis);
```

这里的 FileInputStream 是节点流,用来打开磁盘上面的输入流。后续的 BufferedInputStream 和 DataInputStream 都用来对节点流进行修饰。它们各自只需要完成自己的功能,前者主要负责缓存以提升读取速率,后者用来将得到的流转换成我们需要的数据类型。如果我们由其他的需求只需要在这个链的基础上实现一个自定义的装饰器即可。

回想一下我们在实际的开发过程中是不是经常使用链式来调用一大串,中间的各个环节分别来实现自己的功能,比如转换、过滤、统计等等。使用了装饰者模式之后,链的每个环节只需要实现自己的功能,使用者可以根据自己的需求在链上面增加环节。所以,类似于转换、过滤、统计等等,每个类的责任变得单一了,从整个调用链上面解耦出来。真是不得不佩服 RxJava 的这种设计!

知道了 RxJava 的整体使用的是装饰者设计模式,我们理解其它的一些特性来就容易得多。按照装饰者设计模式的思路,RxJava 的包装过程和调用 `subscribe()` 方法之后的回调过程将如下所示:

![RxJava 的包装和回调的过程](res/RxJava.png)

所以,为什么 RxJava 为人诟病其调用栈太长,就是因为当我们使用一个个的装饰器套起来的时候,导致整个调用的栈变得很长。

另外,捎带说一下所谓的线程切换的问题。假如我们在上述调用过程中的 4 处使用了 `subscribeOn()` 方法,并指定处理的线程为 A;在 5 处同样调用该方法,但是指定的线程为 B,那么之前的 1~3 的过程会被包装成一个对象,放在 4 指定的线程中执行;然后 4 又被包装成一个对象放在 5 所在的线程。因此,如果我们在 2 中获取当前线程,那么肯定得到的是 4 所在的线程。也就是当使用两个 `subscribeOn()` 的时候,通常会被认为只有第一个有效的原因。其实两个都有效,只是 A 是在 B 中执行的,而 1~3 又是在 A 中执行的。所以,所谓的线程切换到奥秘啊,就是依靠这层包裹的关系实现的。一个线程里面把任务执行完了,自然就切换到另一个线程里了。(`subscribeOn()` 和 `observeOn()` 实现线程的时候稍有区别,详情看下文。)

### 1.4 RxJava 的线程切换的执行过程

上面我们也提到过 `subscribeOn()` 和 `observeOn()` 实现线程切换的方式有所不同。所以,在下面的文章中,我们分成两种情况来分别对其进行分析。

当调用 `subscribeOn()` 方法的时候,上流传入的 Observable 将会被进一步装饰成 ObservableSubscribeOn 对象。按照我们上面的分析,当最终调用 `subscribe()` 方法的时候,将会沿着装饰器构成的链,直到 ObservableSubscribeOn 的 `subscribeActual()` 方法中。下面就是该方法的定义,

```java
    @Override
    public void subscribeActual(final Observer<? super T> s) {
        final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);
        s.onSubscribe(parent);
        parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
    }
```

除了上面分析的内容,这里多了一个 scheduler,它就是我们调用  `subscribeOn()` 方法时指定的线程。这里会直接调用它的 `scheduleDirect()` 方法将任务添加到线程池当中执行。这里传入的是 SubscribeTask 对象,它实现了 `Runnable` 接口,并且会在覆写的 `run()` 方法中调用传入的 parent 的 `subscribe()` 方法。因此,它可以被放入任何线程池当中执行,并且当被执行的时候会调用传入的 Observable 的 `subscribe()` 方法来让上流的任务在该线程池当中执行。

下面是 RxJava 中异步任务执行的流程图,

![RxJava 任务调度](res/RxJava_Scheduler.png)

这里的传入的 Schduler 是一个顶层的类,当我们调用 `Schedulers.io()` 等方法的时候,会获取其实现类的实例,比如 IOScheduler. 上面调用 `scheduleDirect()` 方法之后会先使用 Scheduler 的模板方法 `createWorker()` 中获取到一个 Worker. 这个类用来对 RxJava 的任务进行管理。它会进一步调用自己的 `schedule()` 方法来进一步安排任务的执行。图中的 Worker 也是一个抽象类,上面用到的 NewThreadWorker 是它的一个实现。NewThreadWorker 中维护了一个线程池,当调用了它的 `scheduler()` 方法的时候,它就会进一步把该任务放进线程池当中执行。因此,我们的异步任务就在该线程池当中被执行了。

然后,我们再来看下 `observeOn()` 方法是如何进行任务调度的。

当我们调用 `observeOn()` 方法的时候,该任务会被包装成 ObservableObserveOn 的实例。同样,我们来看它的 `subscribeActual()` 方法,

```java
    @Override
    protected void subscribeActual(Observer<? super T> observer) {
        if (scheduler instanceof TrampolineScheduler) {
            source.subscribe(observer);
        } else {
            Scheduler.Worker w = scheduler.createWorker();
            source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));
        }
    }
```

它会直接调用 Scheduler 的模板方法得到 Worker,然后将 Worker 和传入的 Observer 一起包装到 ObserveOnObserver 中。它会被继续向上传递到 ObservableCreate 中,然后它的 `onNext()` 等方法将会被顶层的类触发。接下来,我们就看下 ObserveOnObserver 的定义,这里我们仍然只以 `onNext()` 为例,其方法源码如下,

```java
    @Override
    public void onNext(T t) {
        if (done) {
            return;
        }
        if (sourceMode != QueueDisposable.ASYNC) {
            queue.offer(t);
        }
        schedule();
    }

    void schedule() {
        if (getAndIncrement() == 0) {
            worker.schedule(this);
        }
    }
```

因此,可以看出,在 `observeOn()` 方法中也是通过将任务放进某个 Worker 中执行来实现的,只是具体的线程将取决于 Scheduler 和 Worker 的具体实现。

而 Android 中的将任务放进主线程当中去执行就是通过向主线程的 Handler 发送消息来实现的。如果按照 `subscribeOn()` 的解释,那么当 A 线程启动 B 线程执行任务,那么 B 执行完自然就到了 A 了。那么为什么 Android 中还需要向主线程中发送消息呢?我们使用下面的图来解释。

![RxJava 线程切换](res/RxJava_Switch2.png)

`subscribeOn()` 是一个向上回调的过程,当 A 线程启动 B 线程执行任务,那么 B 执行完自然就到了 A 了,没有问题。但 `observeOn()` 是一个向下调用的过程,从上面的代码中也可以看出,它直接在线程池当中调用 `onNext()` 的时候会沿着回调相反的路线从上往下执行,因此 `observeOn()` 之后所有的逻辑在它指定的线程中执行。

## 2、总结

在本篇文章中,我们总结了 RxJava 2 的源码。虽然 RxJava 的功能非常强大,但是其核心的实现却仅仅依赖两个设计模式,一个是观察者模式,另一个是装饰器模式。它采用了类似于 Java 的流的设计,每个装饰器负责自己一种任务,这复合单一责任原则;各个装饰器之间相互协作,来完成复杂的功能。从上面的源码分析过程中我们也可以看出,RxJava 的缺点也是非常明显的,大量的自定义类,在完成一个功能的时候各装饰器之间不断包装,导致调用的栈非常长。至于线程的切换,它依赖于自己的装饰器模式,因为一个装饰器可以决定其上游的 Observable 在哪些线程当中执行;两个装饰器处于不同的线程的时候,从一个线程中执行完毕自然进入到另一个线程中执行就完成了线程切换的过程。

以上就是 RxJava 的源码分析,如有疑问,欢迎评论区交流:)



================================================
FILE: 响应式编程/RxJava系列(1):一篇的比较全面的RxJava2方法总结.md
================================================
# RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结

看了许多讲解RxJava的文章,有些文章讲解的内容是基于第一个版本的,有些文章的讲解是通过比较常用的一些API和基础的概念进行讲解的。
但是每次看到RxJava的类中的几十个方法的时候,总是感觉心里没底。所以,我打算自己去专门写篇文章来从API的角度系统地梳理一下RxJava的各种方法和用法。

## 1、RxJava 基本

### 1.1 RxJava 简介

RxJava是一个在Java VM上使用可观测的序列来组成异步的、基于事件的程序的库。

虽然,在Android中,我们可以使用AsyncTask来完成异步任务操作,但是当任务的梳理比较多的时候,我们要为每个任务定义一个AsyncTask就变得非常繁琐。
RxJava能帮助我们在实现异步执行的前提下保持代码的清晰。
它的原理就是创建一个`Observable`来完成异步任务,组合使用各种不同的链式操作,来实现各种复杂的操作,最终将任务的执行结果发射给`Observer`进行处理。
当然,RxJava不仅适用于Android,也适用于服务端等各种场景。

我们总结以下RxJava的用途:

1. 简化异步程序的流程;
2. 使用近似于Java8的流的操作进行编程:因为想要在Android中使用Java8的流编程有诸多的限制,所以我们可以使用RxJava来实现这个目的。

在使用RxJava之前,我们需要先在自己的项目中添加如下的依赖:

    compile 'io.reactivex.rxjava2:rxjava:2.2.0'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.2'

这里我们使用的是RxJava2,它与RxJava的第一个版本有些许不同。在本文中,我们所有的关于RxJava的示例都将基于RxJava2. 

注:如果想了解关于Java8的流编程的内容的内容,可以参考我之前写过的文章[五分钟学习Java8的流编程](https://juejin.im/post/5b07f4536fb9a07ac90da4e5)。

### 1.2 概要

下面是RxJava的一个基本的用例,这里我们定义了一个`Observable`,然后在它内部使用`emitter`发射了一些数据和信息(其实就相当于调用了被观察对象内部的方法,通知所有的观察者)。
然后,我们用`Consumer`接口的实例作为`subscribe()`方法的参数来观察发射的结果。(这里的接口的方法都已经被使用Lambda简化过,应该学着适应它。)

    Observable<Integer> observable = Observable.create(emitter -> {
        emitter.onNext(1);
        emitter.onNext(2);
        emitter.onNext(3);
    });
    observable.subscribe(System.out::println);

这样,我们就完成了一个基本的RxJava的示例。从上面的例子中,你或许没法看出`Observable`中隐藏的流的概念,看下面的例子:

    Observable.range(0, 10).map(String::valueOf).forEach(System.out::println);

这里我们先用`Observable.range()`方法产生一个序列,然后用`map`方法将该整数序列映射成一个字符序列,最后将得到的序列输出来。从上面看出,这种操作和Java8里面的Stream编程很像。但是两者之间是有区别的:

1. 所谓的“推”和“拉”的区别:Stream中是通过从流中读取数据来实现链式操作,而RxJava除了Stream中的功能之外,还可以通过“发射”数据,来实现通知的功能,即RxJava在Stream之上又多了一个观察者的功能。
2. Java8中的Stream可以通过`parall()`来实现并行,即基于分治算法将任务分解并计算得到结果之后将结果合并起来;而RxJava只能通过`subscribeOn()`方法将所有的操作切换到某个线程中去。
3. Stream只能被消费一次,但是`Observable`可以被多次进行订阅;

RxJava除了为我们提供了`Observable`之外,在新的RxJava中还提供了适用于其他场景的基础类,它们之间的功能和主要区别如下:

1. `Flowable`: 多个流,响应式流和背压
2. `Observable`: 多个流,无背压
3. `Single`: 只有一个元素或者错误的流
4. `Completable`: 没有任何元素,只有一个完成和错误信号的流
5. `Maybe`: 没有任何元素或者只有一个元素或者只有一个错误的流

除了上面的几个基础类之外,还有一个`Disposable`。当我们监听某个流的时候,就能获取到一个`Disposable`对象。它提供了两个方法,一个是`isDisposed`,可以被用来判断是否停止了观察指定的流;另一个是`dispose`方法,用来放弃观察指定的流,我们可以使用它在任意的时刻停止观察操作。

### 1.3 总结

上面我们介绍了了关于RxJava的基本的概念和使用方式,在下面的文章中我们会按照以上定义的顺序从API的角度来讲解以下RxJava各个模块的使用方法。

## 2、RxJava 的使用

### 2.1 Observable

从上面的文章中我们可以得知,`Observable`和后面3种操作功能近似,区别在于`Flowable`加入了背压的概念,`Observable`的大部分方法也适用于其他3个操作和`Flowable`。
因此,我们这里先从`Observable`开始梳理,然后我们再专门对`Flowable`和背压的进行介绍。

`Observable`为我们提供了一些静态的构造方法来创建一个`Observable`对象,还有许多链式的方法来完成各种复杂的功能。
这里我们按照功能将它的这些方法分成各个类别并依次进行相关的说明。

#### 2.1.1 创建操作

1.interval & intervalRange

下面的操作可以每个3秒的时间发送一个整数,整数从0开始:

    Observable.interval(3, TimeUnit.SECONDS).subscribe(System.out::println);

如果想要设置从指定的数字开始也是可以的,实际上`interval`提供了许多重载方法供我们是使用。下面我们连同与之功能相近的`intervalRange`方法也一同给出:

1. `public static Observable<Long> interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler)`
2. `public static Observable<Long> interval(long period, TimeUnit unit, Scheduler scheduler)`
3. `public static Observable<Long> intervalRange(long start, long count, long initialDelay, long period, TimeUnit unit, Scheduler scheduler)`

这里的`initialDelay`参数用来指示开始发射第一个整数的之前要停顿的时间,时间的单位与`peroid`一样,都是通过`unit`参数来指定的;`period`参数用来表示每个发射之间停顿多少时间;`unit`表示时间的单位,是`TimeUnit`类型的;`scheduler`参数指定数据发射和等待时所在的线程。

`intervalRange`方法可以用来将发射的整数序列限制在一个范围之内,这里的`start`用来表示发射的数据的起始值,`count`表示总共要发射几个数字,其他参数与上面的`interval`方法一致。

2.range & rangeLong

下面的操作可以产生一个从5开始的连续10个整数构成的序列:

    Observable.range(5, 10).subscribe(i -> System.out.println("1: " + i));

该方法需要传入两个参数,与之有相同功能的方法还有`rangeLong`:

1. `public static Observable<Integer> range(final int start, final int count)`
2. `public static Observable<Long> rangeLong(long start, long count)`

这里的两个参数`start`用来指定用于生成的序列的开始值,`count`用来指示要生成的序列总共包含多少个数字,上面的两个方法的主要区别在于一个是用来生成int型整数的,一个是用来生成long型整数的。

3.create

`create`方法用于从头开始创建一个`Observable`,像下面显示的那样,你需要使用`create`方法并传一个发射器作为参数,在该发射器内部调用`onNext`、`onComplete`和`onError`方法就可以将数据发送给监听者。

    Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {
        observableEmitter.onNext(1);
        observableEmitter.onNext(2);
        observableEmitter.onComplete();
    }).subscribe(System.out::println);

4.defer

`defer`直到有观察者订阅时才创建Observable,并且为每个观察者创建一个新的Observable。`defer`操作符会一直等待直到有观察者订阅它,然后它使用Observable工厂方法生成一个Observable。比如下面的代码两个订阅输出的结果是不一致的:

    Observable<Long> observable = Observable.defer((Callable<ObservableSource<Long>>) () -> Observable.just(System.currentTimeMillis()));
    observable.subscribe(System.out::print);
    System.out.println();
    observable.subscribe(System.out::print);

下面是该方法的定义,它接受一个Callable对象,可以在该对象中返回一个Observable的实例:

`public static <T> Observable<T> defer(Callable<? extends ObservableSource<? extends T>> supplier)`

5.empty & never & error

1. `public static <T> Observable<T> empty()`:创建一个不发射任何数据但是正常终止的Observable;
2. `public static <T> Observable<T> never()`:创建一个不发射数据也不终止的Observable;
3. `public static <T> Observable<T> error(Throwable exception)`:创建一个不发射数据以一个错误终止的Observable,它有几个重载版本,这里给出其中的一个。

测试代码:

    Observable.empty().subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete"));
    Observable.never().subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete"));
    Observable.error(new Exception()).subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete"));

输出结果:`completeerror`

6.from 系列

`from`系列的方法用来从指定的数据源中获取一个Observable:

1. `public static <T> Observable<T> fromArray(T... items)`:从数组中获取;
2. `public static <T> Observable<T> fromCallable(Callable<? extends T> supplier)`:从Callable中获取;
3. `public static <T> Observable<T> fromFuture(Future<? extends T> future)`:从Future中获取,有多个重载版本,可以用来指定线程和超时等信息;
4. `public static <T> Observable<T> fromIterable(Iterable<? extends T> source)`:从Iterable中获取;
5. `public static <T> Observable<T> fromPublisher(Publisher<? extends T> publisher)`:从Publisher中获取。

7.just 系列

just系列的方法的一个参数的版本为下面的形式:`public static <T> Observable<T> just(T item)`,它还有许多个重载的版本,区别在于接受的参数的个数不同,最少1个,最多10个。

8.repeat

该方法用来表示指定的序列要发射多少次,下面的方法会将该序列无限次进行发送:

    Observable.range(5, 10).repeat().subscribe(i -> System.out.println(i));

`repeat`方法有以下几个相似方法:

1. `public final Observable<T> repeat()`
2. `public final Observable<T> repeat(long times)`
3. `public final Observable<T> repeatUntil(BooleanSupplier stop)`
4. `public final Observable<T> repeatWhen(Function<? super Observable<Object>, ? extends ObservableSource<?>> handler)`

第1个无参的方法会无限次地发送指定的序列(实际上内部调用了第2个方法并传入了Long.MAX_VALUE),第2个方法会将指定的序列重复发射指定的次数;第3个方法会在满足指定的要求的时候停止重复发送,否则会一直发送。

9.timer

timer操作符创建一个在给定的时间段之后返回一个特殊值的Observable,它在延迟一段给定的时间后发射一个简单的数字0。比如下面的程序会在500毫秒之后输出一个数字`0`。

    Observable.timer(500, TimeUnit.MILLISECONDS).subscribe(System.out::print);

下面是该方法及其重载方法的定义,重载方法还可以指定一个调度器:

1. `public static Observable<Long> timer(long delay, TimeUnit unit)`
2. `public static Observable<Long> timer(long delay, TimeUnit unit, Scheduler scheduler)`

#### 2.1.2 变换操作

1.map & cast

1. `map`操作符对原始Observable发射的每一项数据应用一个你选择的函数,然后返回一个发射这些结果的Observable。默认不在任何特定的调度器上执行。
2. `cast`操作符将原始Observable发射的每一项数据都强制转换为一个指定的类型(多态),然后再发射数据,它是map的一个特殊版本:

下面的第一段代码用于将生成的整数序列转换成一个字符串序列之后并输出;第二段代码用于将Date类型转换成Object类型并进行输出,这里如果前面的Class无法转换成第二个Class就会出现异常:

    Observable.range(1, 5).map(String::valueOf).subscribe(System.out::println);
    Observable.just(new Date()).cast(Object.class).subscribe(System.out::print);

这两个方法的定义如下:

1. `public final <R> Observable<R> map(Function<? super T, ? extends R> mapper)`
2. `public final <U> Observable<U> cast(Class<U> clazz)`

这里的`mapper`函数接受两个泛型,一个表示原始的数据类型,一个表示要转换之后的数据类型,转换的逻辑写在该接口实现的方法中即可。

2.flatMap & contactMap

`flatMap`将一个发送事件的上游Observable变换为多个发送事件的Observables,然后将它们发射的事件合并后放进一个单独的Observable里。需要注意的是, flatMap并不保证事件的顺序,也就是说转换之后的Observables的顺序不必与转换之前的序列的顺序一致。比如下面的代码用于将一个序列构成的整数转换成多个单个的`Observable`,然后组成一个`OBservable`,并被订阅。下面输出的结果仍将是一个字符串数字序列,只是顺序不一定是增序的。

    Observable.range(1, 5)
            .flatMap((Function<Integer, ObservableSource<String>>) i -> Observable.just(String.valueOf(i)))
            .subscribe(System.out::println);

与`flatMap`对应的方法是`contactMap`,后者能够保证最终输出的顺序与上游发送的顺序一致。下面是这两个方法的定义:

1. `public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper)`
2. `public final <R> Observable<R> concatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper)`

`flatMap`的重载方法数量过多,它们在数据源方面略有不同,有的支持错误等可选参数,具体可以参考源代码。

3.flatMapIterable

`flatMapIterable`可以用来将上流的任意一个元素转换成一个`Iterable`对象,然后我们可以对其进行消费。在下面的代码中,我们先生成一个整数的序列,然后将每个整数映射成一个`Iterable<string>`类型,最后,我们对其进行订阅和消费:

    Observable.range(1, 5)
            .flatMapIterable((Function<Integer, Iterable<String>>) integer -> Collections.singletonList(String.valueOf(integer)))
            .subscribe(s -> System.out.println("flatMapIterable : " + s));

下面是该方法及其重载方法的定义:

1. `public final <U> Observable<U> flatMapIterable(Function<? super T, ? extends Iterable<? extends U>> mapper)`
2. `public final <U, V> Observable<V> flatMapIterable(Function<? super T, ? extends Iterable<? extends U>> mapper, BiFunction<? super T, ? super U, ? extends V> resultSelector)`

4.buffer

该方法用于将整个流进行分组。以下面的程序为例,我们会先生成一个7个整数构成的流,然后使用`buffer`之后,这些整数会被3个作为一组进行输出,所以当我们订阅了`buffer`转换之后的`Observable`之后得到的是一个列表构成的`OBservable`:

    Observable.range(1, 7).buffer(3)
            .subscribe(integers -> System.out.println(Arrays.toString(integers.toArray())));

下面是这个方法及其重载方法的定义,它的重载方法太多,这里我们只给出其中的两个,其他的可以参考RxJava的源码。这里的buffer应该理解为一个缓冲区,当缓冲区满了或者剩余的数据不够一个缓冲区的时候就将数据发射出去。

1. `public final Observable<List<T>> buffer(int count)`
2. `public final Observable<List<T>> buffer(int count, int skip)`
3. ...

5.groupBy

`groupBy`用于分组元素,它可以被用来根据指定的条件将元素分成若干组。它将得到一个`Observable<GroupedObservable<T, M>>`类型的`Observable`。如下面的程序所示,这里我们使用`concat`方法先将两个`Observable`拼接成一个`Observable`,然后对其元素进行分组。这里我们的分组依据是整数的值,这样我们将得到一个`Observable<GroupedObservable<Integer, Integer>>`类型的`Observable`。然后,我们再将得到的序列拼接成一个并进行订阅输出:

    Observable<GroupedObservable<Integer, Integer>> observable = Observable.concat(
            Observable.range(1,4), Observable.range(1,6)).groupBy(integer -> integer);
    Observable.concat(observable).subscribe(integer -> System.out.println("groupBy : " + integer));

该方法有多个重载版本,这里我们用到的一个的定义是:

`public final <K> Observable<GroupedObservable<K, T>> groupBy(Function<? super T, ? extends K> keySelector)`

6.scan

`scan`操作符对原始Observable发射的第一项数据应用一个函数,然后将那个函数的结果作为自己的第一项数据发射。它将函数的结果同第二项数据一起填充给这个函数来产生它自己的第二项数据。它持续进行这个过程来产生剩余的数据序列。这个操作符在某些情况下被叫做accumulator。

以下面的程序为例,该程序的输结果是`2 6 24 120 720`,可以看出这里的计算规则是,我们把传入到`scan`中的函数记为`f`,序列记为`x`,生成的序列记为`y`,那么这里的计算公式是`y(0)=x(0); y(i)=f(y(i-1), x(i)), i>0`:

    Observable.range(2, 5).scan((i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + " "));

除了上面的这种形式,`scan`方法还有一个重载的版本,我们可以使用这个版本的方法来在生成序列的时候指定一个初始值。以下面的程序为例,它的输出结果是`3 6 18 72 360 2160 `,可以看出它的输出比上面的形式多了1个,这是因为当指定了初始值之后,生成的第一个数字就是那个初始值,剩下的按照我们上面的规则进行的。所以,用同样的函数语言来描述的话,那么它就应该是下面的这种形式:`y(0)=initialValue; y(i)=f(y(i-1), x(i)), i>0`。

    Observable.range(2, 5).scan(3, (i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + " "));

以上方法的定义是:

1. `public final Observable<T> scan(BiFunction<T, T, T> accumulator)`
2. `public final <R> Observable<R> scan(R initialValue, BiFunction<R, ? super T, R> accumulator)`

7.window

`window`Window和Buffer类似,但不是发射来自原始Observable的数据包,它发射的是Observable,这些Observables中的每一个都发射原始Observable数据的一个子集,最后发射一个onCompleted通知。

以下面的程序为例,这里我们首先生成了一个由10个数字组成的整数序列,然后使用`window`函数将它们每3个作为一组,每组会返回一个对应的Observable对象。
这里我们对该返回的结果进行订阅并进行消费,因为10个数字,所以会被分成4个组,每个对应一个Observable:

    Observable.range(1, 10).window(3).subscribe(
            observable -> observable.subscribe(integer -> System.out.println(observable.hashCode() + " : " + integer)));

除了对数据包进行分组,我们还可以根据时间来对发射的数据进行分组。该方法有多个重载的版本,这里我们给出其中的比较具有代表性的几个:

1. `public final Observable<Observable<T>> window(long count)`
2. `public final Observable<Observable<T>> window(long timespan, long timeskip, TimeUnit unit)`
3. `public final <B> Observable<Observable<T>> window(ObservableSource<B> boundary)`
4. `public final <B> Observable<Observable<T>> window(Callable<? extends ObservableSource<B>> boundary)`

#### 2.1.3 过滤操作

1.filter

`filter`用来根据指定的规则对源进行过滤,比如下面的程序用来过滤整数1到10中所有大于5的数字:

    Observable.range(1,10).filter(i -> i > 5).subscribe(System.out::println);

下面是该方法的定义:

1. `public final Observable<T> filter(Predicate<? super T> predicate)`

2.elementAt & firstElement & lastElement

`elementAt`用来获取源中指定位置的数据,它有几个重载方法,这里我们介绍一下最简单的一个方法的用法。下面是`elementAt`的一个示例,它将获取源数据中索引为1的元素并交给观察者订阅。下面的程序将输出`1`

    Observable.range(1, 10).elementAt(0).subscribe(System.out::print);

这里我们给出`elementAt`及其相关的方法的定义,它们的使用相似。注意一下这里的返回类型:

1. `public final Maybe<T> elementAt(long index)`
2. `public final Single<T> elementAt(long index, T defaultItem)`
3. `public final Single<T> elementAtOrError(long index)`

除了获取指定索引的元素的方法之外,RxJava中还有可以用来直接获取第一个和最后一个元素的方法,这里我们直接给出方法的定义:

1. `public final Maybe<T> firstElement()`
2. `public final Single<T> first(T defaultItem)`
3. `public final Single<T> firstOrError()`
4. `public final Maybe<T> lastElement()`
5. `public final Single<T> last(T defaultItem)`
6. `public final Single<T> lastOrError()`

3.distinct & distinctUntilChanged

`distinct`用来对源中的数据进行过滤,以下面的程序为例,这里会把重复的数字7过滤掉:

    Observable.just(1,2,3,4,5,6,7,7).distinct().subscribe(System.out::print);

与之类似的还有`distinctUntilChanged`方法,与`distinct`不同的是,它只当相邻的两个元素相同的时候才会将它们过滤掉。比如下面的程序会过滤掉其中的2和5,所以最终的输出结果是`12345676`:

    Observable.just(1,2,2,3,4,5,5,6,7,6).distinctUntilChanged().subscribe(System.out::print);

该方法也有几个功能相似的方法,这里给出它们的定义如下:

1. `public final Observable<T> distinct()`
2. `public final <K> Observable<T> distinct(Function<? super T, K> keySelector)`
3. `public final <K> Observable<T> distinct(Function<? super T, K> keySelector, Callable<? extends Collection<? super K>> collectionSupplier)`
4. `public final Observable<T> distinctUntilChanged()`
5. `public final <K> Observable<T> distinctUntilChanged(Function<? super T, K> keySelector)`
6. `public final Observable<T> distinctUntilChanged(BiPredicate<? super T, ? super T> comparer)`

4.skip & skipLast & skipUntil & skipWhile

`skip`方法用于过滤掉数据的前n项,比如下面的程序将会过滤掉前2项,因此输出结果是`345`:

    Observable.range(1, 5).skip(2).subscribe(System.out::print);

与`skip`方法对应的是`take`方法,它用来表示只选择数据源的前n项,该方法的示例就不给出了。这里,我们说一下与之类功能类似的重载方法。`skip`还有一个重载方法接受两个参数,用来表示跳过指定的时间,也就是在指定的时间之后才开始进行订阅和消费。下面的程序会在3秒之后才开始不断地输出数字:

    Observable.range(1,5).repeat().skip(3, TimeUnit.SECONDS).subscribe(System.out::print);

与`skip`功能相反的方法的还有`skipLast`,它用来表示过滤掉后面的几项,以及最后的一段时间不进行发射等。比如下面的方法,我们会在程序开始之前进行计时,然后会不断重复输出数字,直到5秒之后结束。然后,我们用`skipLast`方法表示最后的2秒不再进行发射。所以下面的程序会先不断输出数字3秒,3秒结束后停止输出,并在2秒之后结束程序:

    long current = System.currentTimeMillis();
    Observable.range(1,5)
            .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5))
            .skipLast(2, TimeUnit.SECONDS).subscribe(System.out::print);

与上面的这些方法类似的还有一些,这里我们不再一一列举。因为这些方法的重载方法比较多,下面我们给出其中的具有代表性的一部分:

1. `public final Observable<T> skip(long count)`
2. `public final Observable<T> skip(long time, TimeUnit unit, Scheduler scheduler)`
3. `public final Observable<T> skipLast(int count)`
4. `public final Observable<T> skipLast(long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)`
5. `public final <U> Observable<T> skipUntil(ObservableSource<U> other)`
6. `public final Observable<T> skipWhile(Predicate<? super T> predicate)`

5.take & takeLast & takeUntil & takeWhile

与`skip`方法对应的是`take`方法,它表示按照某种规则进行选择操作。我们以下面的程序为例,这里第一段程序表示只发射序列中的前2个数据:

    Observable.range(1, 5).take(2).subscribe(System.out::print);

下面的程序表示只选择最后2秒中输出的数据:

    long current = System.currentTimeMillis();
    Observable.range(1,5)
            .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5))
            .takeLast(2, TimeUnit.SECONDS).subscribe(System.out::print);

下面是以上相关的方法的定义,同样的,我们只选择其中比较有代表性的几个:

1. `public final Observable<T> take(long count)`
2. `public final Observable<T> takeLast(long count, long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)`
3. `public final <U> Observable<T> takeUntil(ObservableSource<U> other)`
4. `public final Observable<T> takeUntil(Predicate<? super T> stopPredicate)`
5. `public final Observable<T> takeWhile(Predicate<? super T> predicate)`

6.ignoreElements

该方法用来过滤所有源Observable产生的结果,只会把Observable的onComplete和onError事件通知给订阅者。下面是该方法的定义:

1. `public final Completable ignoreElements()`

7.throttleFirst & throttleLast & throttleLatest & throttleWithTimeout

这些方法用来对输出的数据进行限制,它们是通过时间的”窗口“来进行限制的,你可以理解成按照指定的参数对时间进行分片,然后根据各个方法的要求选择第一个、最后一个、最近的等进行发射。下面是`throttleLast`方法的用法示例,它会输出每个500毫秒之间的数字中最后一个数字:

    Observable.interval(80, TimeUnit.MILLISECONDS)
            .throttleLast(500, TimeUnit.MILLISECONDS)
            .subscribe(i -> System.out.print(i + " "));

其他的几个方法的功能大致列举如下:

1. `throttleFirst`只会发射指定的Observable在指定的事件范围内发射出来的第一个数据;
2. `throttleLast`只会发射指定的Observable在指定的事件范围内发射出来的最后一个数据;
3. `throttleLatest`用来发射距离指定的时间分片最近的那个数据;
5. `throttleWithTimeout`仅在过了一段指定的时间还没发射数据时才发射一个数据,如果在一个时间片达到之前,发射的数据之后又紧跟着发射了一个数据,那么这个时间片之内之前发射的数据会被丢掉,该方法底层是使用`debounce`方法实现的。如果数据发射的频率总是快过这里的`timeout`参数指定的时间,那么将不会再发射出数据来。

下面是这些方法及其重载方法的定义(选择其中一部分):

1. `public final Observable<T> throttleFirst(long skipDuration, TimeUnit unit, Scheduler scheduler)`
2. `public final Observable<T> throttleLast(long intervalDuration, TimeUnit unit, Scheduler scheduler)`
3. `public final Observable<T> throttleLatest(long timeout, TimeUnit unit, Scheduler scheduler, boolean emitLast)`
4. `public final Observable<T> throttleWithTimeout(long timeout, TimeUnit unit, Scheduler scheduler)`

8.debounce 

`debounce`也是用来限制发射频率过快的,它仅在过了一段指定的时间还没发射数据时才发射一个数据。我们通过下面的图来说明这个问题:

![debounce](https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.png)

这里红、绿、蓝三个球发射出来的原因都是因为当反射了这个球之后的一定的时间内没有其他的球发射出来,这个时间是我们可以通过参数来指定的。

该方法的用法与`throttle`之类的方法类似,上面也说过`throttle`那些方法底层用了`debounce`实现,所以,这里我们不再为该方法专门编写相关的测试代码。

9.sample

实际上`throttleLast`的实现中内部调用的就是`sample`。

#### 2.1.4 组合操作

1.startWith & startWithArray

`startWith`方法可以用来在指定的数据源的之前插入几个数据,它的功能类似的方法有`startWithArray`,另外还有几个重载方法。这里我们给出一个基本的用法示例,下面的程序会在原始的数字流1-5的前面加上0,所以最终的输出结果是`012345`:

    Observable.range(1,5).startWith(0).subscribe(System.out::print);

下面是`startWith`及其几个功能相关的方法的定义:

1. `public final Observable<T> startWith(Iterable<? extends T> items)`
2. `public final Observable<T> startWith(ObservableSource<? extends T> other)`
3. `public final Observable<T> startWith(T item)`
4. `public final Observable<T> startWithArray(T... items)`

2.merge & mergeArray

`merge`可以让多个数据源的数据合并起来进行发射,当然它可能会让`merge`之后的数据交错发射。下面是一个示例,这个例子中,我们使用`merge`方法将两个`Observable`合并到了一起进行监听:

    Observable.merge(Observable.range(1,5), Observable.range(6,5)).subscribe(System.out::print);

鉴于`merge`方法及其功能类似的方法太多,我们这里挑选几个比较有代表性的方法,具体的可以查看RxJava的源代码:

1. `public static <T> Observable<T> merge(Iterable<? extends ObservableSource<? extends T>> sources)`
2. `public static <T> Observable<T> mergeArray(ObservableSource<? extends T>... sources)`
3. `public static <T> Observable<T> mergeDelayError(Iterable<? extends ObservableSource<? extends T>> sources)`
4. `public static <T> Observable<T> mergeArrayDelayError(ObservableSource<? extends T>... sources)`

这里的`mergeError`方法与`merge`方法的表现一致,只是在处理由`onError`触发的错误的时候有所不同。`mergeError`方法会等待所有的数据发射完毕之后才把错误发射出来,即使多个错误被触发,该方法也只会发射出一个错误信息。而如果使用`merger`方法,那么当有错误被触发的时候,该错误会直接被抛出来,并结束发射操作。下面是该方法的一个使用的示例,这里我们主线程停顿4秒,然后所有`merge`的Observable中的一个会在线程开始的第2秒的时候触发一个错误,该错误最终会在所有的数据发射完毕之后被发射出来:

    Observable.mergeDelayError(Observable.range(1,5),
            Observable.range(1,5).repeat(2),
            Observable.create((ObservableOnSubscribe<String>) observableEmitter -> {
                Thread.sleep(2000);
                observableEmitter.onError(new Exception("error"));
            })
    ).subscribe(System.out::print, System.out::print);
    Thread.sleep(4000);

3.concat & concatArray & concatEager

该方法也是用来将多个Observable拼接起来,但是它会严格按照传入的Observable的顺序进行发射,一个Observable没有发射完毕之前不会发射另一个Observable里面的数据。下面是一个程序示例,这里传入了两个Observable,会按照顺序输出`12345678910`:

    Observable.concat(Observable.range(1, 5), Observable.range(6, 5)).subscribe(System.out::print);

下面是该方法的定义,鉴于该方法及其重载方法太多,这里我们选择几个比较有代表性的说明:

1. `public static <T> Observable<T> concat(Iterable<? extends ObservableSource<? extends T>> sources)`
2. `public static <T> Observable<T> concatDelayError(Iterable<? extends ObservableSource<? extends T>> sources)`
3. `public static <T> Observable<T> concatArray(ObservableSource<? extends T>... sources)`
4. `public static <T> Observable<T> concatArrayDelayError(ObservableSource<? extends T>... sources)`
5. `public static <T> Observable<T> concatEager(ObservableSource<? extends ObservableSource<? extends T>> sources)`
6. `public static <T> Observable<T> concatArrayEager(ObservableSource<? extends T>... sources)`

对于`concat`方法,我们之前已经介绍过它的用法;这里的`conactArray`的功能与之类似;对于`concatEager`方法,当一个观察者订阅了它的结果,那么就相当于订阅了它拼接的所有`ObservableSource`,并且会先缓存这些ObservableSource发射的数据,然后再按照顺序将它们发射出来。而对于这里的`concatDelayError`方法的作用和前面的`mergeDelayError`类似,只有当所有的数据都发射完毕才会处理异常。

4.zip & zipArray & zipIterable

`zip`操作用来将多个数据项进行合并,可以通过一个函数指定这些数据项的合并规则。比如下面的程序的输出结果是`6 14 24 36 50 `,显然这里的合并的规则是相同索引的两个数据的乘积。不过仔细看下这里的输出结果,可以看出,如果一个数据项指定的位置没有对应的值的时候,它是不会参与这个变换过程的:

    Observable.zip(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2)
            .subscribe(i -> System.out.print(i + " "));

`zip`方法有多个重载的版本,同时也有功能近似的方法,这里我们挑选有代表性的几个进行说明:

1. `public static <T, R> Observable<R> zip(Iterable<? extends ObservableSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper)`
2. `ublic static <T, R> Observable<R> zipArray(Function<? super Object[], ? extends R> zipper, boolean delayError, int bufferSize, ObservableSource... sources)`
3. `public static <T, R> Observable<R> zipIterable(Iterable<? extends ObservableSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper, boolean delayError, int bufferSize)`

实际上上面几个方法的用法和功能基本类似,区别在于传入的`ObservableSource`的参数的形式。

5.combineLastest

与`zip`操作类似,但是这个操作的输出结果与`zip`截然不同,以下面的程序为例,它的输出结果是`36 42 48 54 60`:

    Observable.combineLatest(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2)
            .subscribe(i -> System.out.print(i + " "));

利用下面的这张图可以比较容易来说明这个问题:

![combineLastest](https://github.com/Shouheng88/Awesome-Android/blob/master/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/res/combineLatest.png?raw=true)

上图中的上面的两条横线代表用于拼接的两个数据项,下面的一条横线是拼接之后的结果。`combineLatest`的作用是拼接最新发射的两个数据。下面我们用上图的过程来说明该方法是如何执行的:开始第一条只有1的时候无法拼接,;当第二条出现A的时候,此时最新的数据是1和A,故组合成一个1A;第二个数据项发射了B,此时最新的数据是1和B,故组合成1B;第一条横线发射了2,此时最新的数据是2和B,因此得到了2B,依次类推。然后再回到我们上面的问题,第一个数据项连续发射了5个数据的时候,第二个数据项一个都没有发射出来,因此没有任何输出;然后第二个数据项开始发射数据,当第二个数据项发射了6的时候,此时最新的数据组合是6和6,故得36;然后,第二个数据项发射了7,此时最新的数据组合是6和7,故得42,依次类推。

该方法也有对应的`combineLatestDelayError`方法,用途也是只有当所有的数据都发射完毕的时候才去处理错误逻辑。

#### 2.1.5 辅助操作

1.delay

`delay`方法用于在发射数据之前停顿指定的时间,比如下面的程序会在真正地发射数据之前停顿1秒:

    Observable.range(1, 5).delay(1000, TimeUnit.MILLISECONDS).subscribe(System.out::print);
    Thread.sleep(1500);

同样`delay`方法也有几个重载的方法,可以供我们用来指定触发的线程等信息,这里给出其中的两个,其他的可以参考源码和文档:

1. `public final Observable<T> delay(long delay, TimeUnit unit)`
2. `public final Observable<T> delay(long delay, TimeUnit unit, Scheduler scheduler)`

2.do系列

RxJava中还有一系列的方法可以供我们使用,它们共同的特点是都是以`do`开头,下面我们列举一下这些方法并简要说明一下它们各自的用途:

1. `public final Observable<T> doAfterNext(Consumer<? super T> onAfterNext)`,会在`onNext`方法之后触发;
2. `public final Observable<T> doAfterTerminate(Action onFinally)`,会在Observable终止之后触发;
3. `public final Observable<T> doFinally(Action onFinally)`,当`onComplete`或者`onError`的时候触发;
4. `public final Observable<T> doOnDispose(Action onDispose)`,当被dispose的时候触发;
5. `public final Observable<T> doOnComplete(Action onComplete)`,当complete的时候触发;
6. `public final Observable<T> doOnEach(final Observer<? super T> observer)`,当每个`onNext`调用的时候触发;
7. `public final Observable<T> doOnError(Consumer<? super Throwable> onError)`,当调用`onError`的时候触发;
8. `public final Observable<T> doOnLifecycle(final Consumer<? super Disposable> onSubscribe, final Action onDispose)`
9. `public final Observable<T> doOnNext(Consumer<? super T> onNext)`,,会在`onNext`的时候触发;
9. `public final Observable<T> doOnSubscribe(Consumer<? super Disposable> onSubscribe)`,会在订阅的时候触发;
10. `public final Observable<T> doOnTerminate(final Action onTerminate)`,当终止之前触发。

这些方法可以看作是对操作执行过程的一个监听,当指定的操作被触发的时候会同时触发这些监听方法:

    Observable.range(1, 5)
            .doOnEach(integerNotification -> System.out.println("Each : " + integerNotification.getValue()))
            .doOnComplete(() -> System.out.println("complete"))
            .doFinally(() -> System.out.println("finally"))
            .doAfterNext(i -> System.out.println("after next : " + i))
            .doOnSubscribe(disposable -> System.out.println("subscribe"))
            .doOnTerminate(() -> System.out.println("terminal"))
            .subscribe(i -> System.out.println("subscribe : " + i));

3.subscribeOn & observeOn

`subscribeOn`用于指定Observable自身运行的线程,`observeOn`用于指定发射数据所处的线程,比如Android中的异步任务需要用`observeOn`指定发射数据所在的线程是非主线程,然后执行完毕之后将结果发送给主线程,就需要用`subscribeOn`来指定。比如下面的程序,我们用这两个方法来指定所在的线程:

     Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {
        System.out.println(Thread.currentThread());
        observableEmitter.onNext(0);
    }).observeOn(Schedulers.newThread()).subscribeOn(Schedulers.computation())
            .subscribe(integer -> System.out.println(Thread.currentThread()));

最终的输出结果如下所示:

    Thread[RxComputationThreadPool-1,5,main]
    Thread[RxNewThreadScheduler-1,5,main]

4.timeout

用来设置一个超时时间,如果指定的时间之内没有任何数据被发射出来,那么就会执行我们指定的数据项。如下面的程序所示,我们先为设置了一个间隔200毫秒的数字产生器,开始发射数据之前要停顿1秒钟,因为我们设置的超时时间是500毫秒,因而在第500毫秒的时候会执行我们传入的数据项:

    Observable.interval(1000, 200, TimeUnit.MILLISECONDS)
            .timeout(500, TimeUnit.MILLISECONDS, Observable.rangeLong(1, 5))
            .subscribe(System.out::print);
    Thread.sleep(2000);

`timeout`方法有多个重载方法,可以为其指定线程等参数,可以参考源码或者文档了解详情。

#### 2.1.6 错误处理操作符

错误处理操作符主要用来提供给Observable,用来对错误信息做统一的处理,常用的两个是`catch`和`retry`。

1.catch

catch操作会拦截原始的Observable的`onError`通知,将它替换为其他数据项或者数据序列,让产生的Observable能够正常终止或者根本不终止。在RxJava中该操作有3终类型:

1. `onErrorReturn`:这种操作会在onError触发的时候返回一个特殊的项替换错误,并调用观察者的onCompleted方法,而不会将错误传递给观察者;
2. `onErrorResumeNext`:会在onError触发的时候发射备用的数据项给观察者;
3. `onExceptionResumeNext`:如果onError触发的时候onError收到的Throwable不是Exception,它会将错误传递给观察者的onError方法,不会使用备用的Observable。

下面是`onErrorReturn`和`onErrorResumeNext`的程序示例,这里第一段代码会在出现错误的时候输出`666`,而第二段会在出现错误的时候发射数字`12345`:

        Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {
            observableEmitter.onError(null);
            observableEmitter.onNext(0);
        }).onErrorReturn(throwable -> 666).subscribe(System.out::print);

        Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {
            observableEmitter.onError(null);
            observableEmitter.onNext(0);
        }).onErrorResumeNext(Observable.range(1,5)).subscribe(System.out::print);

2.retry

`retry`使用了一种错误重试机制,它可以在出现错误的时候进行重试,我们可以通过参数指定重试机制的条件。以下面的程序为例,这里我们设置了当出现错误的时候会进行2次重试,因此,第一次的时候出现错误会调用`onNext`,重试2次又会调用2次`onNext`,第二次重试的时候因为重试又出现了错误,因此此时会触发`onError`方法。也就是说,下面这段代码会触发`onNext`3次,触发`onError()`1次:

        Observable.create(((ObservableOnSubscribe<Integer>) emitter -> {
            emitter.onNext(0);
            emitter.onError(new Throwable("Error1"));
            emitter.onError(new Throwable("Error2"));
        })).retry(2).subscribe(i -> System.out.println("onNext : " + i), error -> System.out.print("onError : " + error));

`retry`有几个重载的方法和功能相近的方法,下面是这些方法的定义(选取部分):

1. `public final Observable<T> retry()`:会进行无限次地重试;
2. `public final Observable<T> retry(BiPredicate<? super Integer, ? super Throwable> predicate)`
3. `public final Observable<T> retry(long times)`:指定重试次数;
4. `public final Observable<T> retry(long times, Predicate<? super Throwable> predicate) `
5. `public final Observable<T> retryUntil(final BooleanSupplier stop)`
6. `public final Observable<T> retryWhen(Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler)`

#### 2.1.7 条件操作符和布尔操作符

1.all & any

1. `all`用来判断指定的数据项是否全部满足指定的要求,这里的“要求”可以使用一个函数来指定;
2. `any`用来判断指定的Observable是否存在满足指定要求的数据项。

在下面的程序中,我们用该函数来判断指定的数据项是否全部满足大于5的要求,显然是不满足的,因此下面的程序将会输出`false`:

    Observable.range(5, 5).all(i -> i>5).subscribe(System.out::println); // false
    Observable.range(5, 5).any(i -> i>5).subscribe(System.out::println); // true

以下是该方法的定义:

1. `public final Single<Boolean> all(Predicate<? super T> predicate)`
2. `public final Single<Boolean> any(Predicate<? super T> predicate)`

2.contains & isEmpty

这两个方法分别用来判断数据项中是否包含我们指定的数据项,已经判断数据项是否为空:

    Observable.range(5, 5).contains(4).subscribe(System.out::println); // false
    Observable.range(5, 5).isEmpty().subscribe(System.out::println); // false

以下是这两个方法的定义:

1. `public final Single<Boolean> isEmpty()`
2. `public final Single<Boolean> contains(final Object element)`

3.sequenceEqual

`sequenceEqual`用来判断两个Observable发射出的序列是否是相等的。比如下面的方法用来判断两个序列是否相等:

    Observable.sequenceEqual(Observable.range(1,5), Observable.range(1, 5)).subscribe(System.out::println);

4.amb

`amb`作用的两个或多个Observable,但是只会发射最先发射数据的那个Observable的全部数据:

    Observable.amb(Arrays.asList(Observable.range(1, 5), Observable.range(6, 5))).subscribe(System.out::print)

该方法及其功能近似的方法的定义,这里前两个是静态的方法,第二个属于实例方法:

1. `public static <T> Observable<T> amb(Iterable<? extends ObservableSource<? extends T>> sources)`
2. `public static <T> Observable<T> ambArray(ObservableSource<? extends T>... sources)`
3. `public final Observable<T> ambWith(ObservableSource<? extends T> other)`

5.defaultIfEmpty

`defaultIfEmpty`用来当指定的序列为空的时候指定一个用于发射的值。下面的程序中,我们直接调用发射器的`onComplete`方法,因此序列是空的,结果输出一个整数`6`:

    Observable.create((ObservableOnSubscribe<Integer>) Emitter::onComplete).defaultIfEmpty(6).subscribe(System.out::print);

下面是该方法的定义:

1. `public final Observable<T> defaultIfEmpty(T defaultItem)`

#### 2.1.8 转换操作符

1.toList & toSortedList

`toList`和`toSortedList`用于将序列转换成列表,后者相对于前者增加了排序的功能:

    Observable.range(1, 5).toList().subscribe(System.out::println);
    Observable.range(1, 5).toSortedList(Comparator.comparingInt(o -> -o)).subscribe(System.out::println);

下面是它们的定义,它们有多个重载版本,这里选择其中的两个进行说明:

1. `public final Single<List<T>> toList()`
2. `public final Single<List<T>> toSortedList(final Comparator<? super T> comparator)`

注意一下,这里的返回结果是`Single`类型的,不过这并不妨碍我们继续使用链式操作,因为`Single`的方法和`Observable`基本一致。
另外还要注意这里的`Single`中的参数是一个`List<T>`,也就是说,它把整个序列转换成了一个列表对象。因此,上面的两个示例程序的输出是:

    [1, 2, 3, 4, 5]
    [5, 4, 3, 2, 1]

2.toMap & toMultimap

`toMap`用于将发射的数据转换成另一个类型的值,它的转换过程是针对每一个数据项的。以下面的代码为例,它会将原始的序列中的每个数字转换成对应的十六进制。但是,`toMap`转换的结果不一定是按照原始的序列的发射的顺序来的:

    Observable.range(8, 10).toMap(Integer::toHexString).subscribe(System.out::print);

与`toMap`近似的是`toMultimap`方法,它可以将原始序列的每个数据项转换成一个集合类型:

    Observable.range(8, 10).toMultimap(Integer::toHexString).subscribe(System.out::print);

上面的两段程序的输出结果是:

    {11=17, a=10, b=11, c=12, d=13, e=14, f=15, 8=8, 9=9, 10=16}
    {11=[17], a=[10], b=[11], c=[12], d=[13], e=[14], f=[15], 8=[8], 9=[9], 10=[16]}

上面的两个方法的定义是(多个重载,选择部分):

1. `public final <K> Single<Map<K, T>> toMap(final Function<? super T, ? extends K> keySelector)`
2. `public final <K> Single<Map<K, Collection<T>>> toMultimap(Function<? super T, ? extends K> keySelector)`

3.toFlowable

该方法用于将一个Observable转换成Flowable类型,下面是该方法的定义,显然这个方法使用了策略模式,这里面涉及背压相关的内容,我们后续再详细介绍。

    public final Flowable<T> toFlowable(BackpressureStrategy strategy)

4.to

相比于上面的方法,`to`方法的限制更加得宽泛,你可以将指定的Observable转换成任意你想要的类型(如果你可以做到的话),下面是一个示例代码,用来将指定的整数序列转换成另一个整数类型的Observable,只不过这里的每个数据项都是原来的列表中的数据总数的值:

    Observable.range(1, 5).to(Observable::count).subscribe(System.out::println);

下面是该方法的定义:

`public final <R> R to(Function<? super Observable<T>, R> converter)`

### 2.2 线程控制

之前有提到过RxJava的线程控制是通过`subscribeOn`和`observeOn`两个方法来完成的。
这里我们梳理一下RxJava提供的几种线程调度器以及RxAndroid为Android提供的调度器的使用场景和区别等。

1. `Schedulers.io()`:代表适用于io操作的调度器,增长或缩减来自适应的线程池,通常用于网络、读写文件等io密集型的操作。重点需要注意的是线程池是无限制的,大量的I/O调度操作将创建许多个线程并占用内存。
2. `Schedulers.computation()`:计算工作默认的调度器,代表CPU计算密集型的操作,与I/O操作无关。它也是许多RxJava方法,比如`buffer()`,`debounce()`,`delay()`,`interval()`,`sample()`,`skip()`,的默认调度器。
3. `Schedulers.newThread()`:代表一个常规的新线程。
4. `Schedulers.immediate()`:这个调度器允许你立即在当前线程执行你指定的工作。它是`timeout()`,`timeInterval()`以及`timestamp()`方法默认的调度器。
5. `Schedulers.trampoline()`:当我们想在当前线程执行一个任务时,并不是立即,我们可以用`trampoline()`将它入队。这个调度器将会处理它的队列并且按序运行队列中每一个任务。它是`repeat()`和`retry()`方法默认的调度器。

以及RxAndroid提供的线程调度器:

`AndroidSchedulers.mainThread()`用来指代Android的主线程

### 2.3 总结

上面的这些操作也基本适用于`Flowable`、`Single`、`Completable`和`Maybe`。

我们花费了很多的时间和精力来梳理了这些方法,按照上面的内容,使用RxJava实现一些基本的或者高级的操作都不是什么问题。

但是,Observable更适用于处理一些数据规模较小的问题,当数据规模比较多的时候可能会出现`MissingBackpressureException`异常。
因此,我们还需要了解背压和`Flowable`的相关内容才能更好地理解和应用RxJava.


================================================
FILE: 响应式编程/RxJava系列(2):Flowable和背压.md
================================================
# RxJava2 系列 (2):背压和Flowable

背压(Back Pressure)的概念最初并不是在响应式编程中提出的,它最初用在流体力学中,指的是后端的压力,
通常用于描述系统排出的流体在出口处或二次侧受到的与流动方向相反的压力。

在响应式编程中,我们可以将产生信息的部分叫做上游或者叫生产者,处理产生的信息的部分叫做下游或者消费者。
试想如果在异步的环境中,生产者的生产速度大于消费者的消费速度的时候,明显会出现生产过剩的情景,这时候就需要消费者对多余的数据进行缓存,
但如果生产的信息数量过多,以至于超出缓存大小,就会出现缓存溢出,甚至可能造成内存耗尽。

我们可以制定一个数据丢失的规则,来丢失那些“可以丢失的数据”,以减轻缓存的压力。
在之前我们介绍了一些方法,比如`throttleXXX`、`debounce`、`sample`等,都是用来解决在生产速度过快的情况下的数据过滤的,它们指定了数据取舍的规则。
而在`Flowable`,我们可以通过`onBackpressureXXX`一系列的方法来制定当数据生产过快情况下的数据取舍的规则,

我们可以把这种处理方式理解成背压,所谓背压,在Rx中就是通过一种下游用来控制上游事件发射频率的机制(就像流体在出口受到了阻力一样)。
所以,如何理解背压呢?笔者认为,在力学中它是一种现象,在Rx中它是一种机制。

在这篇文章中,我们会先介绍背压的相关内容,然后我们再介绍一下`onBackpressureXXX`系列的方法。

关于RxJava2的基础使用和方法梳理可以参考:[RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd)

说明:以下文章部分翻译自RxJava官方文档[Backpressure (2.0)](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0))。

## 1、背压机制

如果将生产和消费整体看作一个管道,生成看作上游,消费看作下游;
那么当异步的应用场景下,当生产者生产过快而消费者消费很慢的时候,可以通过背压来告知上游减慢生成的速度。

通常在进行异步的操作的时候会通过缓存来存储发射出的数据。在早期的RxJava中,这些缓存是无界的。
这意味着当需要缓存的数据非常多的时候,它们可能会占用非常多的存储空间,并有可能因为虚拟机不断GC而导致程序执行过慢,甚至直接抛出OOM。
在最新的RxJava中,大多数的异步操作内部都存在一个有界的缓存,当超出这个缓存的时候就会抛出`MissingBackpressureException`异常并结束整个序列。

然而,某些情况下的表现会有所不同,它们不会抛出`MissingBackpressureException`异常。比如下面的`range`操作:

    private static void compute(int i) throws InterruptedException {
        Thread.sleep(500);
        System.out.println("computing : " + i);
    }

    private static void testFlowable() throws InterruptedException {
        Flowable.range(1, MAX_LENGTH).observeOn(Schedulers.computation()).subscribe(FlowableTest::compute);

        Thread.sleep(500 * MAX_LENGTH);
    }

在这段代码中我们生成一段整数,然后每隔500毫秒执行依次计算操作。从输出的结果来看,在程序的实际执行过程中,数据的发射是串行的。
也就是发射完一个数据之后进入`compute`进行计算,等待500毫秒之后才发射下一个。
因此,在程序的执行过程中没有抛出异常,也没有过多的内存消耗。

而下面的这段代码就会在程序运行的时候立刻抛出`MissingBackpressureException`异常:

    PublishProcessor<Integer> source = PublishProcessor.create();
    source.observeOn(Schedulers.computation()).subscribe(v -> compute(v), Throwable::printStackTrace);
    for (int i = 0; i < 1_000_000; i++) source.onNext(i);
    Thread.sleep(10_000);

这是因为`PublishProcessor`底层会调用`PublishSubscription`,而后者实现了`AtomicLong`,它会通过判断引用的long是否为0来抛出异常,这个long型整数会在调用`PublishSubscription.request()`的时候被改写。前面的一个例子的原理就是当每次调用了观察者的`onNext`之后会调用`PublishSubscription.request()`来请求数据,这样相当于消费者会在消费完事件之后向生产者请求,因此整个序列的执行看上去是串行的,从而不会抛出异常。

## 2、onBackpressureXXX

大多数开发者在遇到`MissingBackpressureException`通常是因为使用`observeOn`方法监听了非背压的`PublishProcessor`, `timer()`, `interval()`或者自定义的`create()`。我们有以下几种方式来解决这个问题:

### 2.1 增加缓存大小

`observeOn`方法的默认缓存大小是16,当生产的速率过快的时候,那么可能很快会超出该缓存大小,从而导致缓存溢出。
一种简单的解决办法是通过提升该缓存的大小来防止缓存溢出,我们可以使用`observeOn`的重载方法来设置缓存的大小。比如:

    PublishProcessor<Integer> source = PublishProcessor.create();
    source.observeOn(Schedulers.computation(), 1024 * 1024)
          .subscribe(e -> { }, Throwable::printStackTrace);

但是这种解决方案只能解决暂时的问题,当生产的速率过快的时候还是有可能造成缓存溢出,所以这不是根本的解决办法。

### 2.2 通过丢弃和过滤来减轻缓存压力

我们可以根据自己的应用的场景和数据的重要性,选择使用一些方法来过滤和丢弃数据。
比如,丢弃的方式可以选择`throttleFirst`, `throttleLast`, `throttleWithTimeout`等,还可以使用按照时间采样的方式来减少接受的数据。

    PublishProcessor<Integer> source = PublishProcessor.create();
    source.sample(1, TimeUnit.MILLISECONDS)
          .observeOn(Schedulers.computation(), 1024)
          .subscribe(v -> compute(v), Throwable::printStackTrace);
    
但是,这种方式仅仅用来减少下游接收的数据,当缓存的数据不断增加的时候还是有可能导致缓存溢出,所以,这也不是一种根本的解决办法。

### 2.3 onBackpressureBuffer()

这种无参的方法会使用一个无界的缓存,只要虚拟机没有抛出OOM异常,它就会把所有的数据缓存起来。

     Flowable.range(1, 1_000_000)
               .onBackpressureBuffer()
               .observeOn(Schedulers.computation(), 8)
               .subscribe(e -> { }, Throwable::printStackTrace);

上面的例子即使使用了很小的缓存也不会有异常抛出,因为`onBackpressureBuffer`会将发射的所有数据缓存起来,只会将一小部分的数据传递给`observeOn`。

这种处理方式实际上是不存在背压的,因为`onBackpressureBuffer`缓存了所有的数据,我们可以使用该方法的4个重载方法来对背压进行个性化设置。

### 2.4 onBackpressureBuffer(int capacity)

这个方法使用一个有界的缓存,当达到了缓存大小的时候会抛出一个`BufferOverflowError`错误。
通过这种方法可以增加默认的缓存大小,但是通过`observeOn`方法一样可以指定缓存的大小,因此,这个方法的应用变得越来越少。

### 2.5 onBackpressureBuffer(int capacity, Action onOverflow)

这方法除了可以指定一个有界的缓存还提供了一个,当缓存溢出的时候还会回调指定的Action。
但是这种回调的用途比较有限,因为它除了提供当前回调的栈信息以外提供不了任何有用的信息。

### 2.6 onBackpressureBuffer(int capacity, Action onOverflow, BackpressureOverflowStrategy strategy)

这个重载方法相对比较实用一些,它除了上面的那些功能之外,还指定了当缓存到达指定的缓存时的行为。
这里的`BackpressureOverflowStrategy`顾名思义是一个策略,它是一个枚举类型,预定义了三种枚举值,最终会在`FlowableOnBackpressureBufferStrategy`中根据指定的枚举类型选择不同的实现策略,因此,我们可以使用它来指定缓存溢出时候的行为。

下面是该枚举类型的三个值及其含义:

1. `ERROR`:当缓存溢出的时候会抛出一个异常;
2. `DROP_OLDEST`:当缓存发生溢出的时候,会丢弃最老的值,并将新的值插入到缓存中;
3. `DROP_LATEST`:当缓存发生溢出的时候,最新的值会被忽略,只有比较老的值会被传递给下游使用;

需要注意的地方是,后面的两种策略会造成下游获取到的值是不连续的,因为有一部分值会因为缓存不够被丢弃,但是它们不会抛出`BufferOverflowException`。

### 2.7 onBackpressureDrop()

这个方法会在数据达到缓存大小的时候丢弃最新的数据。可以将其看成是`onBackpressureBuffer`+`0 capacity`+`DROP_LATEST`的组合。

这个方法特别适用于那种可以忽略从源中发射出值的那种场景,比如GPS定位问题,定位数据会不断发射出来,即使丢失当前数据,等会儿一样能拿到最新的数据。

    component.mouseMoves()
        .onBackpressureDrop()
        .observeOn(Schedulers.computation(), 1)
        .subscribe(event -> compute(event.x, event.y));

该方法还存在一个重载方法`onBackpressureDrop(Consumer<? super T> onDrop)`,它允许我们传入一个接口来指定当某个数据被丢失时的行为。

### 2.8 onBackpressureLatest()

对应于`onBackpressureDrop()`的,还有`onBackpressureLatest()`方法,该方法只会保留最新的数据并会覆盖较老、没有分发的数据。
我们可以将其看成是`onBackpressureBuffer`+`1 capacity`+`DROP_OLDEST`的组合。

与`onBackpressureDrop()`不同的地方在于,当下游消费过慢的时候,这种方式总会存在一个缓存的值。
这种特别适用于那种数据的生产非常频繁,但是只有最新的数据会被消费的那种情形。比如,当用户点击了屏幕,那么我们倾向于只处理最新按下的位置的事件。

    component.mouseClicks()
        .onBackpressureLatest()
        .observeOn(Schedulers.computation())
        .subscribe(event -> compute(event.x, event.y), Throwable::printStackTrace);

所以,总结一下:

1. `onBackpressureDrop()`:不会缓存任何数据,专注于当下,新来的数据来不及处理就丢掉,以后会有更好的;
2. `onBackpressureLatest()`:会缓存一个数据,当正在执行某个任务的时候有新的数据过来,会把它缓存起来,如果又有新的数据过来,那就把之前的替换掉,缓存里面的总是最新的。

## 3、总结

以上就是背压机制的一些内容,以及我们介绍了`Flowable`中的几个背压相关的方法。
实际上,RxJava的官方文档也有说明——`Flowable`适用于数据量比较大的情景,因为它的一些创建方法本身就使用了背压机制。
这部分方法我们就不再一一进行说明,因为,它们的方法签名和`Observable`基本一致,只是多了一层背压机制。

比较匆匆地整理完了背压的内容,但是我想这块还会有更加丰富的内容值得我们去发现和探索。

以上。

================================================
FILE: 响应式编程/RxJava系列(3):用RxJava打造EventBus.md
================================================
# RxJava2 系列 (3):使用 Subject

在这篇文章中,我们会先分析一下 RxJava2 中的 Subject ;然后,我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。

在了解本篇文章的内容之前,你需要先了解 RxJava2 中的一些基本的用法,比如 Observable 以及背压的概念,你可以参考我的其他两篇文章来获取这部分内容:[《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd)和[《RxJava2 系列 (2):背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187)。

## 1、Subject

### 1.1 Subject 的两个特性

Subject 可以同时代表 Observer 和 Observable,允许从数据源中多次发送结果给多个观察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外,所有的方法都是线程安全的。此外,你还可以使用 toSerialized() 方法,也就是转换成串行的,将这些方法设置成线程安全的。

如果你已经了解了 Observable 和 Observer ,那么也许直接看 Subject 的源码定义会更容易理解:

```
public abstract class Subject<T> extends Observable<T> implements Observer<T> {

    // ...
}
```

从上面看出,Subject 同时继承了 Observable 和 Observer 两个接口,说明它既是被观察的对象,同时又是观察对象,也就是可以生产、可以消费、也可以自己生产自己消费。所以,我们可以项下面这样来使用它。这里我们用到的是该接口的一个实现 PublishSubject :

    public static void main(String...args) {
        PublishSubject<Integer> subject = PublishSubject.create();
        subject.subscribe(System.out::println);

        Executor executor = Executors.newFixedThreadPool(5);
        Disposable disposable = Observable.range(1, 5).subscribe(i ->
                executor.execute(() -> {
                    try {
                        Thread.sleep(i * 200);
                        subject.onNext(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }));
    }

根据程序的执行结果,程序在第200, 400, 600, 800, 1000毫秒依次输出了1到5的数字。

在这里,我们用 PublishSubject 创建了一个**主题**并对其监听,然后在线程当中又通知该主题内容变化,整个过程我们都只操作了 PublishSubject 一个对象。显然,使用 Subject 我们可以达到对一个指定类型的值的结果进行监听的目的——我们把值改变之后对应的逻辑写在 subscribe() 方法中,然后每次调用 onNext() 等方法通知结果之后就可以自动调用 subscribe() 方法进行更新操作。

同时,因为 Subject 实现了 Observer 接口,并且在 Observable 等的 subscribe() 方法中存在一个以 Observer 作为参数的方法(如下),所以,Subject 也是可以作为消费者来对事件进行消费的。

    public final void subscribe(Observer<? super T> observer) 

以上就是 Subject 的两个主要的特性。

### 1.2 Subject 的实现类

在 RxJava2 ,Subject 有几个默认的实现,下面我们对它们之间的区别做简单的说明:

1. `AsyncSubject`:只有当 Subject 调用 onComplete 方法时,才会将 Subject 中的**最后一个事件**传递给所有的 Observer。
2. `BehaviorSubject`:该类有创建时需要一个默认参数,该默认参数会在 Subject 未发送过其他的事件时,向注册的 Observer 发送;新注册的 Observer 不会收到之前发送的事件,这点和 PublishSubject 一致。
3. `PublishSubject`:不会改变事件的发送顺序;在已经发送了一部分事件之后注册的 Observer 不会收到之前发送的事件。
4. `ReplaySubject`:无论什么时候注册 Observer 都可以接收到任何时候通过该 Observable 发射的事件。
5. `UnicastSubject`:只允许一个 Observer 进行监听,在该 Observer 注册之前会将发射的所有的事件放进一个队列中,并在 Observer 注册的时候一起通知给它。

对比 PublishSubject 和 ReplaySubject,它们的区别在于新注册的 Observer 是否能够收到在它注册之前发送的事件。这个类似于 EventBus 中的 StickyEvent 即黏性事件,为了说明这一点,我们准备了下面两段代码:

    private static void testPublishSubject() throws InterruptedException {
        PublishSubject<Integer> subject = PublishSubject.create();
        subject.subscribe(i -> System.out.print("(1: " + i + ") "));

        Executor executor = Executors.newFixedThreadPool(5);
        Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
            try {
                Thread.sleep(i * 200);
                subject.onNext(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));

        Thread.sleep(500);
        subject.subscribe(i -> System.out.print("(2: " + i + ") "));

        Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
    }

    private static void testReplaySubject() throws InterruptedException {
        ReplaySubject<Integer> subject = ReplaySubject.create();
        subject.subscribe(i -> System.out.print("(1: " + i + ") "));

        Executor executor = Executors.newFixedThreadPool(5);
        Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {
            try {
                Thread.sleep(i * 200);
                subject.onNext(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));

        Thread.sleep(500);
        subject.subscribe(i -> System.out.print("(2: " + i + ") "));

        Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());
    }

它们的输出结果依次是

    PublishSubject的结果:(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)
    ReplaySubject的结果: (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)

从上面的结果对比中,我们可以看出前者与后者的区别在于新注册的 Observer 并没有收到在它注册之前发送的事件。试验的结果与上面的叙述是一致的。

其他的测试代码这不一并给出了,详细的代码可以参考[Github - Java Advanced](https://github.com/Shouheng88/Java-advanced)。

## 2、用 RxJava 打造 EventBus

### 2.1 打造 EventBus

清楚了 Subject 的概念之后,让我们来做一个实践——用 RxJava 打造 EventBus。

我们先考虑用一个全局的 PublishSubject 来解决这个问题,当然,这意味着我们发送的事件不是黏性事件。不过,没关系,只要这种实现方式搞懂了,用 ReplaySubject 做一个发送黏性事件的 EventBus 也非难事。

考虑一下,如果要实现这个功能我们需要做哪些准备:

1. **我们需要发送事件并能够正确地接收到事件。**要实现这个目的并不难,因为 Subject 本身就具有发送和接收两个能力,作为全局的之后就具有了全局的注册和通知的能力。因此,不论你在什么位置发送了事件,任何订阅的地方都能收到该事件。
2. **首先,我们要在合适的位置对事件进行监听,并在合适的位置取消事件的监听。如果我们没有在适当的时机释放事件,会不会造成内存泄漏呢?这还是有可能的。**所以,我们需要对注册监听的观察者进行记录,并提供注册和取消注册的方法,给它们在指定的生命周期中进行调用。

好了,首先是全局的 Subject 的问题,我们可以实现一个静态的或者单例的 Subject。这里我们选择使用后者,所以,我们需要一个单例的方式来使用 Subject:

public class RxBus {

    private static volatile RxBus rxBus;

    private final Subject<Object> subject = PublishSubject.create().toSerialized();

    public static RxBus getRxBus() {
        if (rxBus == null) {
            synchronized (RxBus.class) {
                if(rxBus == null) {
                    rxBus = new RxBus();
                }
            }
        }
        return rxBus;
    }
}

这里我们应用了 DCL 的单例模式提供一个单例的 RxBus,对应一个唯一的 Subject. 这里我们用到了 Subject 的`toSerialized()`,我们上面已经提到过它的作用,就是用来保证 onNext() 等方法的线程安全性。

另外,因为 Observalbe 本身是不支持背压的,所以,我们还需要将该 Observable 转换成 Flowable 来实现背压的效果:

    public <T> Flowable<T> getObservable(Class<T> type){
        return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type);
    }

这里我们用到的背压的策略是`BackpressureStrategy.BUFFER`,它会缓存发射结果,直到有消费者订阅了它。而这里的`ofType()`方法的作用是用来过滤发射的事件的类型,只有指定类型的事件会被发布。

然后,我们需要记录订阅者的信息以便在适当的时机取消订阅,这里我们用一个`Map<String, CompositeDisposable>`类型的哈希表来解决。这里的`CompositeDisposable`用来存储 Disposable,从而达到一个订阅者对应多个 Disposable 的目的。`CompositeDisposable`是一个 Disposable 的容器,声称可以达到 O(1) 的增、删的复杂度。这里的做法目的是使用注册观察之后的 Disposable 的 dispose() 方法来取消订阅。所以,我们可以得到下面的这段代码:

    public void addSubscription(Object o, Disposable disposable) {
        String key = o.getClass().getName();
        if (disposableMap.get(key) != null) {
            disposableMap.get(key).add(disposable);
        } else {
            CompositeDisposable disposables = new CompositeDisposable();
            disposables.add(disposable);
            disposableMap.put(key, disposables);
        }
    }

    public void unSubscribe(Object o) {
        String key = o.getClass().getName();
        if (!disposableMap.containsKey(key)) {
            return;
        }
        if (disposableMap.get(key) != null) {
            disposableMap.get(key).dispose();
        }
        disposableMap.remove(key);
    }

最后,对外提供一下 Subject 的订阅和发布方法,整个 EventBus 就制作完成了:

    public void post(Object o){
        subject.onNext(o);
    }

    public <T> Disposable doSubscribe(Class<T> type, Consumer<T> next, Consumer<Throwable> error){
        return getObservable(type)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(next,error);
    }

### 2.2 测试效果

我们只需要在最顶层的 Activity 基类中加入如下的代码。这样,我们就不需要在各个 Activity 中取消注册了。然后,就可以使用这些顶层的方法来进行操作了。

    protected void postEvent(Object object) {
        RxBus.getRxBus().post(object);
    }

    protected <M> void addSubscription(Class<M> eventType, Consumer<M> action) {
        Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d);
        RxBus.getRxBus().addSubscription(this, disposable);
    }

    protected <M> void addSubscription(Class<M> eventType, Consumer<M> action, Consumer<Throwable> error) {
        Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error);
        RxBus.getRxBus().addSubscription(this, disposable);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RxBus.getRxBus().unSubscribe(this);
    }

在第一个 Activity 中我们对指定的类型的结果进行监听:

    addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message));

然后,我们在另一个 Activity 中发布事件:

    postEvent(new RxMessage("Hello world!"));

这样当第二个 Activity 中调用指定的发送事件的方法之后,第一个 Activity 就可以接收到发射的事件了。

## 总结

好了,以上就是 Subject 的使用,如果要用一个词来形容它的话,那么只能是“自给自足”了。就是说,它同时做了 Observable 和 Observer 的工作,既可以发射事件又可以对事件进行消费,可谓身兼数职。它在那种想要对某个值进行监听并处理的情形特别有用。因为它不需要你写多个冗余的类,只要它一个就完成了其他两个类来完成的任务,因而代码更加简洁。


================================================
FILE: 四大组件/Activity.md
================================================
# Android 基础回顾:Activity 基础

## 1、Activity 的生命周期

### 1.1 一般情况下的生命周期

下图是一般情况下一个 Activity 将会经过的生命周期的流程图:

![Activity的生命周期](res/activity_life.png)

关于上图中生命周期方法的说明:

1. **onCreate() / onDestroy()**:onCreate() 表示 Activity 正在被创建,可以用来做初始化工作;onDestroy() 表示 Activity 正在被销毁,可以用来做释放资源的工作;
2. **onStart() / onStop()**:onStart() 在 Activity 从不可见变成可见的时候被调用;onStop() 在 Activity 从可见变成不可见的时候被调用;
3. **onRestart()**:在 Activity 从不可见到变成可见的过程中被调用;
4. **onResume() / onPause()**:onResume() 在 Activity() 可以与用户交互的时候被调用,onPause() 在 Activity 不可与用户交互的时候被调用。

所以根据上面的分析,我们可以将Activity的生命周期概况为:**创建->可见->可交互->不可交互->不可见->销毁**。因此,我们可以得到下面的这张图:

### 1.2 特殊情况下的生命周期

这里我们总结一下在实际的使用过程中可能会遇到的一些 Acitivity 的生命周期过程:

1. **当用户打开新的 Activity 或者切换回桌面**:会经过的生命周期为 `onPause()->onStop()`。因为此时 Activity 已经变成不可见了,当然,如果新打开的 Activity 用了透明主题,那么 onStop() 不会被调用,因此原来的 Activity 只是不能交互,但是仍然可见。
2. **从新的 Activity 回到之前的 Activity 或者从桌面回到之前的 Activity**:会经过的生命周期为 `onRestart()->onStart()-onResume()`。此时是从 onStop() 经 onRestart() 回到 onResume() 状态。
3. 如果在上述 1 的情况下,进入后台的 Activity 因为内存不足被销毁了,那么当再次回到该 Activity 的时候,生命周期方法将会从 onCreate() 开始执行到 onResume()。
4. **当用户按下 Back 键时**:如果当前 Activity 被销毁,那么经过的生命周期将会是 `onPause()->onStop()->onDestroy()`。

具体地,当存在两个 Activity,分别是 A 和 B 的时候,在各种情况下,它们的生命周期将会经过:

1. **Back 键 Home 键**
    1. 当用户点击 A 中按钮来到 B 时,假设 B 全部遮挡住了 A,将依次执行:`A.onPause()->B.onCreate()->B.onStart()->B.onResume->A.onStop()`。
    2. 接1,此时如果点击 Back 键,将依次执行:`B.onPause()->A.onRestart()->A.onStart()->A.onResume()->B.onStop()->B.onDestroy()`。
    3. 接2,此时如果按下 Back 键,系统返回到桌面,并依次执行:`A.onPause()->A.onStop()->A.onDestroy()`。
    4. 接2,此时如果按下 Home 键(非长按),系统返回到桌面,并依次执行`A.onPause()->A.onStop()`。由此可见,Back 键和 Home 键主要区别在于是否会执行 onDestroy()。
    5. 接2,此时如果长按 Home 键,不同手机可能弹出不同内容,Activity 生命周期未发生变化。
2. **横竖屏切换时 Activity 的生命周期**
    1. 不设置 Activity 的 `android:configChanges` 时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次。
    2. 设置 Activity 的 `android:configChanges=“orientation”` 时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次。
    3. 设置 Activity 的 `android:configChanges=“orientation|keyboardHidden”` 时,切屏不会重新调用各个生命周期,只会执行 onConfiguration() 方法。

### 1.3 onSaveInstanceState() 和 onRestoreInstanceState()

当 Activity 被销毁的时候回调用 `onSaveInstanceState()` 方法来存储当前的状态。这样当 Activity 被重建的时候,可以在 `onCreate()` 和 `onRestoreInstanceState()` 中恢复状态。

对于 targetAPI 为 28 及以后的应用,该方法会在 `onStop()` 方法之后调用,对于之前的设备,这方法会在 `onStop()` 之前调用,但是无法确定是在 `onPause()` 之前还是之后调用。

`onRestoreInstanceState()` 方法用来恢复之前存储的状态,它会在 `onStart()` 和 `onPostCreate()` 之间被调用。此外,你也可以直接在 `onCreate()` 方法中进行恢复,但是基于这个方法调用的时机,如果有特别需求,可以在这个方法中进行处理。

## 2、Activity 的启动模式

Activity 共有四种启动模式:

1. **standard**:默认,每次启动的时候会创建一个新的实例,并且被创建的实例所在的栈与启动它的 Activity 是同一个栈。比如,A 启动了 B,那么 B 将会与 A 处在同一个栈。假如,我们使用 Application 的 Context 启动一个 Activity 的时候会抛出异常,这是因为新启动的 Activity 不知道自己将会处于哪个栈。可以在启动 Activity 的时候使用 `FLAG_ACTIVITY_NEW_TASK`。这样新启动的 Acitivyt 将会创建一个新的栈。
2. **singleTop**:栈顶复用,如果将要启动的 Activity 已经位于栈顶,那么将会复用栈顶的 Activity,并且会调用它的 `onNewIntent()`。常见的应用场景是从通知打开 Activity 时。
3. **singleTask**:单例,如果启动它的任务栈中存在该 Activity,那么将会复用该 Activity,并且会将栈内的、它之上的所有的 Activity 清理出去,以使得该 Activity 位于栈顶。常见的应用场景是启动页面、购物界面、确认订单界面和付款界面等。
4. **singleInstance**:这种启动模式会在启动的时候为其指定一个单独的栈来执行。如果用同样的intent 再次启动这个 Activity,那么这个 Activity 会被调到前台,并且会调用其 `onNewIntent()` 方法。

## 3、Activity 的 Flags

1. **FLAG_ACTIVITY_CLEAR_TOP** : 会清理掉该栈中位于 Activity 上面的所有的 Activity,通常与 FLAG_ACTIVITY_NEW_TASK 配合使用;
2. **FLAG_ACTIVITY_SINGLE_TOP**: 同样等同于 mainfest 中配置的 singleTop;
3. **FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS**: 对应于 mainfest 中的属性为`android:excludeFromRecents="true"`,当用户按了 “最近任务列表” 时,该任务不会出现在最近任务列表中,可达到隐藏应用的目的。
4. **FLAG_ACTIVITY_NO_HISTORY**: 对应于 mainfest 中的 `android:noHistory="true"`。这个 FLAG 启动的 Activity,一旦退出,它不会存在于栈中。
5. **FLAG_ACTIVITY_NEW_TASK**: 等同于 mainfest 中配置的 singleTask.




================================================
FILE: 四大组件/Broadcast.md
================================================
# Android 基础回顾:Broadcast 基础

## 1、关于广播

广播是 Android 提供的一种全局通信机制。

### 1.1 分类

1. 按照注册方式:**静态注册和动态注册**两种;
2. 按照作用范围:**本地广播和普通广播**两种,普通广播是全局的,所有应用程序都可以接收到,容易会引起安全问题。本地广播只能够在应用内传递,广播接收器也只能接收应用内发出的广播;
3. 按照是否有序:**有序广播和无序广播**两种,无序广播各接收器接收的顺序无法确定,并且在广播发出之后接收器只能接收,不能拦截和进行其他处理,两者的区别主要体现在发送时调用的方法上。

### 1.2 实现

#### 1.2.1 静态广播

注册,这里的 StaticBroadcastReceiver 是自定义类:

```xml
<receiver android:name=".StaticBroadcastReceiver">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>
```

我们可以将要实现的逻辑放在这个类的方法中进行执行:

```java
public class StaticBroadcastReceiver extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		// Do something
	}
}
```

需要注意 Andrdoid 8.0 之后系统对广播进行了一些限制([官方文档](https://developer.android.google.cn/about/versions/oreo/android-8.0)),具体地:

1. 在 Android 8.0 的平台上,应用不能对大部分的广播进行静态注册,也就是说,不能在AndroidManifest 文件对**有些**广播进行静态注册(注意“有些”,因为不是所有的广播都不能注册)。
2. 当程序运行在后台的时候,静态广播中不能启动服务。比如之前实现闹钟的时候是监听时间变化来实现的,在 8.0 之后就会抛出异常。

解决方式是使用动态注册方式(一般情况下使用动态注册就好了)。

#### 1.2.2 动态广播

与静态广播相似,但是不需要在 Manifest 中进行注册。

```java
    // 监听广播:一般在 Activity 的 onCreate() 方法中注册
    netWorkChangReceiver = new StaticBroadcastReceiver();
    IntentFilter filter = new IntentFilter();
    filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
    registerReceiver(netWorkChangReceiver, filter);

    // 取消监听:然后在 Activity 的 onDestroy() 中取消注册
    unregisterReceiver(netWorkChangReceiver);
```

**注意当页面被销毁的时候需要取消注册广播!**

#### 1.2.3 本地广播

本地广播的核心类是 LocalBroadcastManager,使用它的静态方法 `getInstance()` 获取一个单例之后就可以使用该单例的 `registerReceiver()`、`unregisterReceiver()` 和 `sendBroadcast()` 等方法来进行操作了。

```java
    // 获取单例
    localBroadcastManager = LocalBroadcastManager.getInstance(this);

    // 注册广播
    IntentFilter filter = new IntentFilter();
    filter.addAction("me.shouheng.MyBroadcastReceiver");
    localReceiver = new LocalReceiver();
    localBroadcastManager.registerReceiver(localReceiver, filter);

    // 发送广播
    Intent intent = new Intent("me.shouheng.MyBroadcastReceiver");
    localBroadcastManager.sendBroadcast(intent);

    // 取消注册
    localBroadcastManager.unregisterReceiver(localReceiver);
```

#### 1.2.4 有序广播

在 xml 中进行注册的时候通过 `android:priority` 指定一个范围在 -1000~1000 之间的整数来指定广播的接收顺序。优先级高的会先接收到,优先级相等的话则顺序不确定。并且前面的广播可以在方法中向 Intent 写入数据,后面的广播可以接收到写入的值。

```xml
    <receiver android:name=".MyReceiver_1">
        <intent-filter android:priority="200">
            <action android:name="com.song.123"/>
        </intent-filter>
    </receiver>
    <receiver android:name=".MyReceiver_2">
        <intent-filter android:priority="1000">
            <action android:name="com.song.123"/>
        </intent-filter>
    </receiver>
```





================================================
FILE: 四大组件/Fragment.md
================================================
# Android 基础回顾:Fragment 基础

## 1、Fragment 的生命周期

### 1.1 Fragment 的生命周期

下面是 Fragment 的生命周期的流程图:

![Fragment的生命周期](res/fragment_lifecycle.png)

从上图可以看出,Fragment 的生命周期相比于 Activity,在创建的过程中增加了 `onAttach()`、`onCreateView()` 和 `onActivityCreated()` 三个方法,在销毁的过程中增加了 `onDestroyView()` 和 `onDetach()` 两个方法。

### 1.2 Activity 与 Fragment 生命周期对应关系

![Activity 与 Fragment 生命周期对应关系](res/activity_fragment_lifecycle.png)




================================================
FILE: 四大组件/Service.md
================================================
# Android 基础回顾:Service 基础

Service 主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让 Service 在后台继续保持运行状态。相对于使用线程来实现异步任务的方式,它的安全性更高。(但是 Service 是运行在主线程中的,如果需要实现异步任务,可以单开线程。)

## 1、基础使用示例

### 1.1 使用示例

首先,通过继承 Service 来定义 Service:

```java
    public class MyService extends Service {

        public static final String TAG = "MyService";

        @Override
        public void onCreate() {
            super.onCreate();
            Log.d(TAG, "onCreate() executed");
        }

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            Log.d(TAG, "onStartCommand() executed");
            return super.onStartCommand(intent, flags, startId);
        }

        @Override
        public void onDestroy() {
            super.onDestroy();
            Log.d(TAG, "onDestroy() executed");
        }

        @Override
        public IBinder onBind(Intent intent) {
            return new MyBinder();
        }

        class MyBinder extends Binder {  
            public void startDownload() {  
                Log.d("TAG", "startDownload() executed");  
            }   
        }  
    }
```

定义了 Service 之后还要在 `AndroidManifest.xml` 中注册才能正常使用:

```xml
    <service android:name="com.example.servicetest.MyService"/>  
```

然后,就可以使用该 Service 了:

```java
    // 启动 Service
    Intent startIntent = new Intent(this, MyService.class);
    startService(startIntent);  

    // 停止 Service
    Intent stopIntent = new Intent(this, MyService.class);
    stopService(stopIntent);

    // 关联 Service 和 Activity
    Intent bindIntent = new Intent(this, MyService.class);  
    bindService(bindIntent, connection, BIND_AUTO_CREATE);

    // 解除 Service 和 Activity 关联
    unbindService(connection);

    private ServiceConnection connection = new ServiceConnection() {  
  
        @Override  
        public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            myBinder = (MyService.MyBinder) service;  
            myBinder.startDownload();  
        }  
    };
```

以上就是 Service 的基础使用示例。

### 1.2 Service 的使用小结

首先是 Service 的生命周期图

![Service的生命周期图](res/service_life.png)

其他,

1. Service 有绑定模式和非绑定模式,以及这两种模式的混合使用方式。不同的使用方法生命周期方法也不同。 
    1. **非绑定模式**:当第一次调用 `startService()` 的时候执行的方法依次为 `onCreate()->onStartCommand()`;当 Service 关闭的时候调用 `onDestory()`。
    2. **绑定模式**:第一次 `bindService()` 的时候,执行的方法为 `onCreate()->onBind()`;解除绑定的时候会执行 `onUnbind()->onDestory()`。
2. 我们在开发的过程中还必须注意 Service 实例只会有一个,也就是说如果当前要启动的 Service 已经存在了那么就不会再次创建该 Service 当然也不会调用 onCreate() 方法。所以,
    1. 当第一次执行 `startService(intent)` 的时候,会调用该 Service 中的 `onCreate()` 和`onStartCommand()` 方法。
    2. 当第二次执行 `startService(intent)` 的时候,只会调用该 Service 中的 `onStartCommand()` 方法。(因此已经创建了服务,所以不需要再次调用 `onCreate()` 方法了)。
3. `bindService()` 方法的第三个参数是一个标志位,这里传入 `BIND_AUTO_CREATE` 表示在Activity 和 Service 建立关联后自动创建 Service,这会使得 MyService 中的 `onCreate()` 方法得到执行,但 `onStartCommand()` 方法不会执行。所以,在上面的程序中当调用了`bindService()` 方法的时候,会执行的方法有,Service 的 `onCreate()` 方法,以及 ServiceConnection 的 `onServiceConnected()` 方法。
4. 在 3 中,如果想要停止 Service,需要调用 `unbindService()` 才行。 
5. 如果我们既调用了 `startService()`,又调用 `bindService()` 会怎么样呢?这时不管你是单独调用 `stopService()` 还是 `unbindService()`,Service 都不会被销毁,必须要将两个方法都调用 Service 才会被销毁。也就是说,`stopService()` 只会让 Service 停止,`unbindService()` 只会让 Service 和 Activity 解除关联,一个 Service 必须要在既没有和任何 Activity 关联又处理停止状态的时候才会被销毁。

## 2、Service 与线程

Service 运行在主线程里的,也就是说如果你在 Service 里编写了非常耗时的代码,程序可能会出现ANR。 

Service 只意味着不需要前台 UI 的支持,即使 Activity 被销毁,或者程序被关闭,只要进程还在,Service 就可以继续运行。但是我们可以在 Service 中再创建一个子线程,然后在这里去处理耗时逻辑。

虽然也可以在 Activity 中创建线程来执行耗时任务,但是它的缺点在于该线程只能与该 Activity 关联,其他 Activity 无法对其进行控制。

所以,标准的使用是:

```java
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 开始执行后台任务  
            }
        }).start();
        return super.onStartCommand(intent, flags, startId);
    }

    class MyBinder extends Binder {

        public void startDownload() {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 执行具体的下载任务  
                }
            }).start();
        }
    }
```

当然,你也可以使用 RxJava 等封装的线程池来实现异步任务。

## 3、前台 Service

因为 Service 的系统优先级较低,所以当系统出现内存不足情况时,就有可能会回收掉正在后台运行的 Service。我们可以通过使用前台 Service 来解决 Service 可能被回收的问题。它的效果是在系统中显示一个驻留的通知。

前台服务的

```java
    public class MyService extends Service {

        public static final String TAG = "MyService";

        @Override
        public void onCreate() {
            super.onCreate();
            Notification notification = new Notification(R.drawable.ic_launcher, 
                "Msg", System.currentTimeMillis());
            Intent intent = new Intent(this, MainActivity.class);
            PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
            notification.setLatestEventInfo(this, "Title", "Content", pi);
            startForeground(1, notification);
        }  
    }
```

## 4、远程 Service

远程 Service 是一种运行在其他进程中的服务。我们可以在 Manifest 中进行注册的时候通过指定进程来讲服务设置成远程的。远程的服务因为运行在另一个进程中,所以涉及跨进程调用的问题。

可以通过在 `AndroidManifest.xml` 中进行如下设置来将一个 Service 设置在非主线程中:

```xml
    <service  
        android:name="com.example.servicetest.MyService"  
        android:process=":remote" >  
    </service>  
```

也就说当前的 Service 运行在其他的进程了,不会阻碍主进行,从而也不会存在 ANR 了。但是这种方式中的 Service 是无法与 Activity 进行关联的,也就是说调用 `bindService()` 的时候会出现错误。如果我们想要将该 Service 与 Activity 进行关联,就需要使用 AIDL 进行跨进程通信了(IPC)。

要实现跨进程调用,我们可以按照如下步骤来实现:

首先,新建 `MyAIDLService.aidl` 文件:

```java
    package com.example.servicetest;  

    interface MyAIDLService {  
        int plus(int a, int b);  
        String toUpperCase(String str);  
    }  
```

然后,我们要修改之前 Service 中的 `bind()` 方法:

```java
    @Override  
    public IBinder onBind(Intent intent) {  
        return mBinder;  
    }

    MyAIDLService.Stub mBinder = new Stub() {  
  
        @Override  
        public String toUpperCase(String str) throws RemoteException {  
            if (str != null) {  
                return str.toUpperCase();  
            }  
            return null;  
        }  
  
        @Override  
        public int plus(int a, int b) throws RemoteException {  
            return a + b;  
        }  
    }; 
```

然后,我们在 ServiceConnection 中进行如下实现:

```java
    private ServiceConnection connection = new ServiceConnection() {  
  
        @Override  
        public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ }  
  
        @Override  
        public void onServiceConnected(ComponentName name, IBinder service) {  
            myAIDLService = MyAIDLService.Stub.asInterface(service);  
            try {  
                int result = myAIDLService.plus(3, 5);  
                String upperStr = myAIDLService.toUpperCase("hello world");  
            } catch (RemoteException e) {  
                e.printStackTrace();  
            }  
        }  
    };  
```

也就是使用 `MyAIDLService.Stub.asInterface()` 方法获取 MyAIDLService,并调用 MyAIDLService 的方法。这时候再调用 `bindService()` 就不会出现错误了。

如果我们想要在其他进程(APP)中调用该 Service,我们可以进行如下操作:

首先在 Service 添加 `Intent-Filter`:

```xml
    <service android:name="com.example.servicetest.MyService"  
        android:process=":remote" >  
        <intent-filter>  
            <action android:name="com.example.servicetest.MyAIDLService"/>  
        </intent-filter>  
    </service>  
```

这样,我们就将该 Service 设置成其他程序可访问的了。

然后,在要访问该 Service 的程序中进行如下操作:

1. 将上述定义的 MyAIDLService 连同其包拷贝到当前程序中,即 src 目录下面。
2. 然后在绑定 Service 的时候按照下面的方式绑定:

```java
    Intent intent = new Intent("com.example.servicetest.MyAIDLService");
    bindService(intent, connection, BIND_AUTO_CREATE);

    private ServiceConnection connection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {}

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            myAIDLService = MyAIDLService.Stub.asInterface(service);
            try {
                int result = myAIDLService.plus(50, 50);
                String upperStr = myAIDLService.toUpperCase("comes from ClientTest");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };
```

这样我们就将该 Service 设置成了其他进程可访问的。

## 5、IntentService

相比于一般的 Service,IntentService 具有的特征:

1. 会创建独立的线程来处理所有的 `Intent` 请求;。
2. 会创建独立的线程来处理 `onHandleIntent()` 方法实现的代码,无需处理多线程问题。
3. 所有请求处理完成后,`IntentService` 会自动停止,无需调用 `stopSelf()` 方法停止 Service;。
4. 为 Service 的 `onBind()` 提供默认实现,返回 null. 
5. 为 Service 的 `onStartCommand()` 提供默认实现,将请求 Intent 添加到队列中。 
6. IntentService 内置的是 HandlerThread 作为异步线程,每一个交给 IntentService 的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的

关于 IntentService 的源码分析,可以参考下面这篇文章:

[Android 多线程编程:IntentService & HandlerThread](https://blog.csdn.net/github_35186068/article/details/83758049)

## 5、Service 保活的问题

我们可以用通过 `setForeground(true)` 来提升 Service 的优先级。当然这并不能保证你得Service 永远不被杀掉,只是提高了他的优先级。这种方式的缺点是会设置一个长期停留的通知,用户体验比较差。

那么如何避免后台进程被杀死?

首先,服务被杀死的情况包含下面三种:

1. 系统根据资源分配情况杀死服务
2. 用户通过 `settings->Apps->Running->Stop` 方式杀死服务
3. 用户通过 `settings->Apps->Downloaded->Force Stop` 方式杀死服务

以及对应的解决办法:

1. 调用 `startForegound()`,让你的 Service 所在的线程成为前台进程;
2. Service 的 `onStartCommond()` 返回 START_STICKY 或 START_REDELIVER_INTENT;
3. Service 的 `onDestroy()` 里面重新启动自己。

关于 `onStartCommond()` 的返回值的总结:

|No|可选值|含义|
|:-:|:-:|:-|
|1|START_STICKY|当 Service 因内存不足而被系统 kill 后,一段时间后内存再次空闲时,系统将会尝试重新创建此 Service,一旦创建成功后将回调 `onStartCommand()` 方法,但其中的 Intent 将是 null,除非有挂起的 Intent,如 pendingintent,这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务|
|2|START_NOT_STICKY|当 Service 因内存不足而被系统 kill 后,即使系统内存再次空闲时,系统也不会尝试重新创建此 Service。除非程序中再次调用 `startService()` 启动此 Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务|
|3|START_REDELIVER_INTENT|当 Service 因内存不足而被系统 kill 后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 `onStartCommand()`,任何挂起 Intent 均依次传递。与START_STICKY 不同的是,其中的传递的 Intent 将是非空,是最后一次调用 `startService()` 中的 intent。这个值适用于主动执行应该立即恢复的作业(例如下载文件)的服务|

在 `onDestroy()` 中自启的示例:

```java
	public void onCreate() {  
	    super.onCreate();  
	    mBroadcast = new BroadcastReceiver() {  
		    @Override  
		    public void onReceive(Context context, Intent intent) {  
		        Intent a = new Intent(ServiceA.this, ServiceA.class);  
		        startService(a);  
		    }  
	    };  
	    mIF = new IntentFilter();  
	    mIF.addAction("listener");  
	    registerReceiver(mBroadcast, mIF);  
	}
	
	@Override  
	public void onDestroy() {  
	  super.onDestroy();  
	  Intent intent = new Intent();  
	  intent.setAction("listener");  
	  sendBroadcast(intent);  
	  unregisterReceiver(mBroadcast);  
	}  
```

上面的这种启动的实例会因为系统处于后台线程而抛出异常。

参考:

1. [Android Service完全解析,关于服务你所需知道的一切(上)](http://blog.csdn.net/guolin_blog/article/details/11952435)
2. [Android Service完全解析,关于服务你所需知道的一切(下)](http://blog.csdn.net/guolin_blog/article/details/9797169)
3. [关于Android Service真正的完全详解,你需要知道的一切](https://blog.csdn.net/javazejian/article/details/52709857)

================================================
FILE: 图片加载/Android相机最佳实践.md
================================================
# CameraX:Android 相机库开发实践

## 前言

前段时间因为工作的需要对项目中的相机模块进行了优化,我们项目中的相机模块是基于开源库 CameraView 进行开发的。那次优化主要包括两个方面,一个是相机的启动速度,另一个是相机的拍摄的清晰度的问题。因为时间仓促,那次只是在原来的代码的基础之上进行的优化,然而那份代码本身存在一些问题,导致相机的启动速度无法进一步提升。所以,我准备自己开发一款功能完善,并且可拓展的相机库,于是 [CameraX](https://github.com/Shouheng88/CameraX) 就诞生了。

## Android 相加开源库的现状

要使用 Android 相机实现图片拍照功能本身并不复杂,Camera1 + SurfaceView 就可以搞定。但是如果让相机能够自由拓展,就需要花费很多的功夫。我所接触的开源库包括 Google 非官方的 CameraView,以及 CameraFragment. 两个库的设计有各自的优点和缺点。

|开源库|优点|缺点|
|:-|:-|:-|
|CameraView|1.支持基本的拍照、缩放等功能;2.支持自定义图片的宽高比;3.支持多种预览布局方式;|1.每次获取相机支持的尺寸的时候,会先将其组装到一个有序的 Set 中,这个过程会占用一定的启动时间;2.不支持拍摄视频;3.代码堆砌,结构混乱|
|CameraFragment|1.支持拍摄照片和视频;2.代码结构清晰|1.不支持缩放;2.默认宽高比4:3,无法运行时修改;3.必须基于 Fragment|

以上是两个开源库的优点和缺点,而我们可以结合它们的优缺点实现一个更加完善的相机库,同时对性能的优化和用户自定义配置,我们也提供了更多的可用的接口。

## CameraX 整体结构设计

虽然文章的题目是相机开发实践,但是我们并不打算介绍太多关于如何使用 Camera API 的内容,因为本项目是开源的,读者可以自行 Fork 代码进行阅读。在这里,我们只对项目中的一些关键部分的设计思路进行说明。

![相机整体架构](res/开发.png)

连接:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049

以上是我们相机库的整体架构的设计图,这里笔者使用了 UML 建模进行基础的架构设计(当然,并非严格遵循 UML 建模的语言规则)。下面,我们介绍下项目的关键部分的设计思路。

### Camera1 还是 Camera2?

了解 Android 相机 API 的同学可能知道,在 LoliPop 上面提出了 Camera2 API. 就笔者个人的实践开发的效果来看,Camera2 相机的性能确实比 Camera1 要好得多,这体现在相机对焦的速率和相机启动的速率上。当然,这和硬件也有一定的关系。Camera2 比 Camera1 使用起来确实复杂得多,但提供的可以调用的 API 也更丰富。Camera2 的另一个问题是国内的很多手机设备对 Camera2 的支持并不好。

对于这个问题,首先,我们可以根据系统的参数来判断该设备是否支持 Camera2:

```java
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static boolean hasCamera2(Context context) {
        if (context == null) return false;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false;
        try {
            CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
            assert manager != null;
            String[] idList = manager.getCameraIdList();
            boolean notNull = true;
            if (idList.length == 0) {
                notNull = false;
            } else {
                for (final String str : idList) {
                    if (str == null || str.trim().isEmpty()) {
                        notNull = false;
                        break;
                    }
                    final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str);

                    Integer iSupportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
                    if (iSupportLevel != null && iSupportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
                        notNull = false;
                        break;
                    }
                }
            }
            return notNull;
        } catch (Throwable ignore) {
            return false;
        }
    }
```

不过,即便上面方法返回的结果标明支持 Camera2,但相机仍然可能在启动中出现异常。所以 CameraView 的解决方案是,相机启动的方法返回一个 boolean 类型标明 Camera2 是否启动成功,如果失败了,就降级并使用 Camera1。但是降级的过程会浪费一定的启动时间,因此,有人提出了使用 SharedPreferences 存储降级的记录,下次直接使用 Camera1 的解决方案。

上面两种方案各自有优缺点,使用第二种方案意味着你要修改相机库的源代码,而我们希望以一种更加灵活的方式提供给用户选择相机的权力。没错,就是**策略设计模式**。

因为虽然 Camera1 和 Camera2 的 API 设计和使用不同,但是我们并不需要知道内部如何实现,我们只需要给用户提供切换相机、打开闪光灯、拍照、缩放等的接口即可。在这种情况下,当然使用**门面设计模式**是最好的选择。

另外,对于 TextureView 还是 SurfaceView 的选择,我们也使用了**策略模式+门面模式**的思路。

即。对于相机的选择,我们提供门面 CameraManager 接口,Camera1 的实现类 Camera1Manager 以及 Camera2 的实现类 Camera2Manager. Camera1Manager 和 Camera2Manager 又统一继承自 BaseCameraManager. 这里的 BaseCameraManager 是一个抽象类,用来封装一些通用的相机方法。

所以问题到了是 Camera1Manager 还是 Camera2Manager 的问题。这里我们提供了策略接口 CameraManagerCreator,它返回 CameraManager:

```java
public interface CameraManagerCreator {

    CameraManager create(Context context, CameraPreview cameraPreview);
}
```

以及一个默认的实现:

```java
public class CameraManagerCreatorImpl implements CameraManagerCreator {

    @Override
    public CameraManager create(Context context, CameraPreview cameraPreview) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && CameraHelper.hasCamera2(context)) {
            return new Camera2Manager(cameraPreview);
        }
        return new Camera1Manager(cameraPreview);
    }
}
```

因此,我们只需要在相机的全局配置中指定自己的 CameraManager 创建策略就可以使用指定的相机了。

### 全局配置

之前考虑指定 CameraManager 创建策略的时候,思路是直接对静态的变量赋值的方式,不过后来考虑到对相机的支持的尺寸进行缓存的问题,所以将其设计了静态单实例的类:

```java
public class ConfigurationProvider {

    private static volatile ConfigurationProvider configurationProvider;

    private ConfigurationProvider() {
        if (configurationProvider != null) {
            throw new UnsupportedOperationException("U can't initialize me!");
        }
        initWithDefaultValues();
    }

    public static ConfigurationProvider get() {
        if (configurationProvider == null) {
            synchronized (ConfigurationProvider.class) {
                if (configurationProvider == null) {
                    configurationProvider = new ConfigurationProvider();
                }
            }
        }
        return configurationProvider;
    }

    // ... ...
}
```

除了指定一些全局的配置之外,我们还可以在 ConfigurationProvider 中缓存一些相机的信息,比如相机支持的尺寸的问题。因为相机所支持的尺寸属于相机属性的一部分,是不变的,我们没有必要获取多次,可以将其缓存起来,下次直接使用。当然,我们还提供了不使用缓存的接口:

```java
public class ConfigurationProvider {

    // ...
    private boolean useCacheValues;
    private List<Size> pictureSizes;

    public List<Size> getPictureSizes(android.hardware.Camera camera) {
        if (useCacheValues && pictureSizes != null) {
            return pictureSizes;
        }
        List<Size> sizes = Size.fromList(camera.getParameters().getSupportedPictureSizes());
        if (useCacheValues) {
            pictureSizes = sizes;
        }
        return sizes;
    }

}
```

这样,我们在获取相机支持的图片尺寸信息的时候只需要传入 Camera 即可使用缓存的信息。当然,缓存信息在某些极端的情况下可能会带来问题,比如从 Camera1 切换到 Camera2 的时候,需要清除缓存。

*注:这里缓存的时候应该使用 SoftReference,但是考虑到数据量不大,没有这么设计,以后会考虑修改。*

### 输出媒体文件的尺寸的问题

使用 Android 相机一个让人头疼的地方是计算尺寸的问题:因为相机支持的尺寸有三种,包括相片的支持尺寸、预览的支持尺寸和视频的支持尺寸。预览的尺寸决定了用户看到的画面的清晰程度,但是真正拍摄出图片的清晰度取决于相片的尺寸,同理输出的视频的尺寸取决于视频的尺寸。

在 CameraView 中,它允许你指定一个图片的尺寸,当没有满足的要求的尺寸的时候会 Crash…这样的处理方式是将其不好的,因为用户根本无法确定相机最大的支持尺寸,而 CameraView 甚至没有提供获取相机支持尺寸的接口……

为了解决这个问题,我们首先提供了一系列用户获取相机支持尺寸的接口:

```java
    Size getSize(@Camera.SizeFor int sizeFor);

    SizeMap getSizes(@Camera.SizeFor int sizeFor);
```

这里的 SizeFor 是基于注解的枚举,我们通过它来判断用户是希望获取相片、预览还是视频的尺寸信息。这里的 SizeMap 是一个哈希表,从相机的宽高比映射到对应的尺寸列表。跟 CameraView 处理方式不同的是,我们只有在调用上述方法的时候才计算图片的宽高比信息,虽然调用下面的方法的时候会花费一丁点儿时间,但是相机的启动速度大大提升了:

```java
    @Override
    public SizeMap getSizes(@Camera.SizeFor int sizeFor) {
        switch (sizeFor) {
            case Camera.SIZE_FOR_PREVIEW:
                if (previewSizeMap == null) {
                    previewSizeMap = CameraHelper.getSizeMapFromSizes(previewSizes);
                }
                return previewSizeMap;
            case Camera.SIZE_FOR_PICTURE:
                if (pictureSizeMap == null) {
                    pictureSizeMap = CameraHelper.getSizeMapFromSizes(pictureSizes);
                }
                return pictureSizeMap;
            case Camera.SIZE_FOR_VIDEO:
                if (videoSizeMap == null) {
                    videoSizeMap = CameraHelper.getSizeMapFromSizes(videoSizes);
                }
                return videoSizeMap;
        }
        return null;
    }
```

获取了相机的尺寸信息的目的当然是将其设置到相机上面,所以我们提供了两个用来设置相机尺寸的接口:

```java
    void setExpectSize(Size expectSize);

    void setExpectAspectRatio(AspectRatio expectAspectRatio);
```

它们一个用来指定期望的输出文件的尺寸,一个用来指定期望的图片的宽高比。

OK,既然用户可以指定计算参数,那么怎么计算呢?这当然还是用户说了算的,因为我们一样在全局配置中为用户提供了计算的策略接口:

```java
public interface CameraSizeCalculator {

    Size getPicturePreviewSize(@NonNull List<Size> previewSizes, @NonNull Size pictureSize);

    Size getVideoPreviewSize(@NonNull List<Size> previewSizes, @NonNull Size videoSize);

    Size getPictureSize(@NonNull List<Size> pictureSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);

    Size getVideoSize(@NonNull List<Size> videoSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);
}
```

当然,我们也会提供一个默认的计算策略。在 CameraManager 内部,我们会在需要的地方调用上述接口的方法以获取最终的相机尺寸信息:

```java
    private void adjustCameraParameters(boolean forceCalculateSizes, boolean changeFocusMode, boolean changeFlashMode) {
        Size oldPreview = previewSize;
        long start = System.currentTimeMillis();
        CameraSizeCalculator cameraSizeCalculator = ConfigurationProvider.get().getCameraSizeCalculator();
        android.hardware.Camera.Parameters parameters = camera.getParameters();
        if (mediaType == Media.TYPE_PICTURE && (pictureSize == null || forceCalculateSizes)) {
            pictureSize = cameraSizeCalculator.getPictureSize(pictureSizes, expectAspectRatio, expectSize);
            previewSize = cameraSizeCalculator.getPicturePreviewSize(previewSizes, pictureSize);
            parameters.setPictureSize(pictureSize.width, pictureSize.height);
            notifyPictureSizeUpdated(pictureSize);
        }

        // ... ...
    }
```

### 性能优化

为了对相机的性能进行优化,笔者可是花了大量的精力。因为在之前进行优化的时候积累了一些经验,所以这次开发的时候就容易得多。下面是 TraceView 进行分析的图:

![Android 相机 TraceView 分析](res/QQ图片20190423230233.png)

可以看出从相机当中获取支持尺寸的本身会占用一定时间的,而这种属于相机固有的信息,一般是不会发生变化的,所以我们可以通过将其缓存起来来提升下一次打开相机的速率。

整体上,该项目的优化主要体现在几个地方:

1. 使用注解+常量取代枚举:因为枚举占用的内存空间比较大,而单纯使用注解无法约束输入参数的范围。这在 enums 包下面可以看到,这也是 Android 性能优化最常见的手段之一。

2. 延迟初始化:我们为了达到只在使用到某些数据的时候才初始化的目的采用了延迟初始化的解决方案,比如 Size 的宽高比的问题:

```
public class Size {

    // ...

    private double ratio;

    public double ratio() {
        if (ratio == 0 && width != 0) {
            ratio = (double) height / width;
        }
        return ratio;
    }

}
```

3. 数据结构的应用和选择:选择合适的数据结构和自定义数据结构往往能起到化腐朽为神奇的作用。比如 SizeMap 

```java
public class SizeMap extends HashMap<AspectRatio, List<Size>> {
}
```

比如在列表数据结构的应用上面,使用 ArrayList 但是提前指定数组大小,减小数组扩容的次数:

```java
    public static List<Size> fromList(@NonNull List<Camera.Size> cameraSizes) {
        List<Size> sizes = new ArrayList<>(cameraSizes.size());
        for (Camera.Size size : cameraSizes) {
            sizes.add(of(size.width, size.height));
        }
        return sizes;
    }
```

4. 缓存,这个我们之前已经提到过,除了尺寸信息我们还缓存了一些其他的信息,具体可以参考源码。

5. 异步线程:这个当然是最能提升应用相应速度的方式。它能够让我们不阻塞主线程,从而提升界面相应的速度。但是在相机开发的时候存在一个问题,即通常打开的相机的时候比较耗时,所以放在异步线程中;而开启预览处于主线程,这很容易因为线程执行的顺序的问题导致一些难以预测的异常。在之前,笔者的解决方案是使用一个私有锁来实现线程的控制。

## 总结

本次相机库开发占用的时间其实不多,更多的时间花费在了 UML 建模图的设计和在真正开发之前收集资料信息。不得不说,如果你开发一个小的项目,不需要做什么设计,直接就可以上了,但是如果你设计一个比较复杂的库,花费更多时间在 UML 建模上面是值得的,因为它能让你的开发思路更加清晰。另外,为了开发 Camera2,笔者不仅找遍了开源库,还翻译了相关的官方文档,这在开源项目中会一并奉上。

### 相机目前支持的功能

|编号|功能|
|:-|:-|
|1|拍摄照片|
|2|拍摄视频|
|3|指定使用 Camera1 还是 Camera2|
|4|指定使用 TextureView 还是 SurfaceView|
|5|闪光灯打开和关闭|
|6|自动对焦的选择|
|7|前置和后置相机|
|8|快门声|
|9|指定缩放的大小|
|10|指定期望的图片大小|
|11|指定期望的图片宽高比|
|12|获取支持的图片、预览和视频的尺寸信息|
|13|相机尺寸发生变化监听|
|14|输出视频的文件位置|
|15|输出视频的时间长度|
|16|手指界面滑动的监听|
|17|触摸进行缩放|
|18|预览自适应和裁剪等|
|19|缓存相机信息,清除和不适用缓存信息|

### 最后是关于项目的一些小问题

该项目目前所有功能已经开发完毕,不过仍有一些小的问题需要完善:

1. Camera2 预览放大之后拍摄出的图片没有放大效果的问题;
2. Camera1 拍摄出的图片需要旋转 90 度;
3. Camera2 在屏幕旋转成横屏之后相机预览需要同时选择 90 度的问题;
4. Camera1 和 Camera2 切换存在一些问题。

另外,由于时间限制,该相机库目前没有进行严格的测试,所以建议使用的时候进行充分测试之后再使用。

### 是否会继续完善该项目?

是的,包括对相机的功能进行充分测试。只是目前的时间结点,笔者有其他的事务需要处理,所以先把它介绍给读者。当然也希望能够有更多感兴趣的朋友对该项目贡献代码。

## 项目地址:

1. 项目地址:https://github.com/Shouheng88/CameraX
2. UML 建模图地址:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049
3. 笔者翻译的Camera2 文档:https://github.com/Shouheng88/Android-notes/blob/master/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/Android%E7%9B%B8%E6%9C%BACamera2%E8%B5%84%E6%96%99.md


================================================
FILE: 图片加载/Glide系列:Glide主流程源码分析.md
================================================
# Glide 系列-2:主流程源码分析(4.8.0)

Glide 是 Android 端比较常用的图片加载框架,这里我们就不再介绍它的基础的使用方式。你可以通过查看其官方文档学习其基础使用。这里,我们给出一个 Glide 的最基本的使用示例,并以此来研究这个整个过程发生了什么:

```java
Glide.with(fragment).load(myUrl).into(imageView);
```

上面的代码虽然简单,但是整个执行过程涉及许多类,其流程也比较复杂。为了更清楚地说明这整个过程,我们将 Glide 的图片加载按照调用的时间关系分成了下面几个部分:

1. `with()` 方法的执行过程
2. `load()` 方法的执行过程
3. `into()` 方法的执行过程
    1. 阶段1:开启 `DecodeJob` 的过程
    2. 阶段2:打开网络流的过程
    3. 阶段3:将输入流转换为 `Drawable` 的过程
    4. 阶段4:将 `Drawable` 展示到 `ImageView` 的过程

即按照上面的示例代码,先分成 `with()`、`load()` 和 `into()` 三个过程,而 `into()` 过程又被细化成四个阶段。

下面我们就按照上面划分的过程来分别介绍一下各个过程中都做了哪些操作。

## 1、with() 方法的执行过程

### 1.1 实例化单例的 Glide 的过程

当调用了 Glide 的 `with()` 方法的时候会得到一个 `RequestManager` 实例。`with()` 有多个重载方法,我们可以使用 `Activity` 或者 `Fragment` 等来获取 `Glide` 实例。它们最终都会调用下面这个方法来完成最终的操作:

```java
public static RequestManager with(Context context) {
    return getRetriever(context).get(context);
}
```

在 `getRetriever()` 方法内部我们会先使用 `Glide` 的 `get()` 方法获取一个单例的 Glide 实例,然后从该 Glide 实例中得到一个 `RequestManagerRetriever`:

```java
private static RequestManagerRetriever getRetriever(Context context) {
    return Glide.get(context).getRequestManagerRetriever();
}
```

这里调用了 Glide 的 `get()` 方法,它最终会调用 `initializeGlide()` 方法实例化一个**单例**的 `Glide` 实例。在之前的文中我们已经介绍了这个方法。它主要用来从注解和 Manifest 中获取 GlideModule,并根据各 GlideModule 中的方法对 Glide 进行自定义:

[《Glide 系列-1:预热、Glide 的常用配置方式及其原理》](Glide系列:Glide的配置和使用方式.md)

下面的方法中需要传入一个 `GlideBuilder` 实例。很明显这是一种构建者模式的应用,我们可以使用它的方法来实现对 Glide 的个性化配置:

```java
private static void initializeGlide(Context context, GlideBuilder builder) {

    // ... 各种操作,略

    // 赋值给静态的单例实例
    Glide.glide = glide;
}
```

最终 Glide 实例由 `GlideBuilder` 的 `build()` 方法构建完毕。它会直接调用 Glide 的构造方法来完成 Glide 的创建。在该构造方法中会将各种类型的图片资源及其对应的加载类的映射关系注册到 Glide 中,你可以阅读源码了解这部分内容。

### 1.2 Glide 的生命周期管理

在 `with()` 方法的执行过程还有一个重要的地方是 Glide 的生命周期管理。因为当我们正在进行图片加载的时候,Fragment 或者 Activity 的生命周期可能已经结束了,所以,我们需要对 Glide 的生命周期进行管理。

Glide 对这部分内容的处理也非常巧妙,它使用没有 UI 的 Fragment 来管理 Glide 的生命周期。这也是一种非常常用的生命周期管理方式,比如 `RxPermission` 等框架都使用了这种方式。你可以通过下面的示例来了解它的作用原理:

[示例代码:使用 Fragment 管理 onActivityResult()](https://github.com/Shouheng88/Android-references/tree/master/advanced/src/main/java/me/shouheng/advanced/callback)

在 `with()` 方法中,当我们调用了 `RequestManagerRetriever` 的 `get()` 方法之后,会根据 Context 的类型调用 `get()` 的各个重载方法。

```java
  public RequestManager get(@NonNull Context context) {
    if (context == null) {
      throw new IllegalArgumentException("You cannot start a load on a null Context");
    } else if (Util.isOnMainThread() && !(context instanceof Application)) {
      if (context instanceof FragmentActivity) {
        return get((FragmentActivity) context);
      } else if (context instanceof Activity) {
        return get((Activity) context);
      } else if (context instanceof ContextWrapper) {
        return get(((ContextWrapper) context).getBaseContext());
      }
    }

    return getApplicationManager(context);
  }
```

我们以 Activity 为例。如下面的方法所示,当当前位于后台线程的时候,会使用 Application 的 Context 获取 `RequestManager`,否则会使用无 UI 的 Fragment 进行管理:

```java
  public RequestManager get(@NonNull Activity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      android.app.FragmentManager fm = activity.getFragmentManager();
      return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
  }
```

然后就调用到了 `fragmentGet()` 方法。这里我们从 `RequestManagerFragment` 中通过 `getGlideLifecycle()` 获取到了 `Lifecycle` 对象。`Lifecycle` 对象提供了一系列的、针对 Fragment 生命周期的方法。它们将会在 Fragment 的各个生命周期方法中被回调。

```java
  private RequestManager fragmentGet(Context context, FragmentManager fm, 
    Fragment parentHint, boolean isParentVisible) {
    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
      Glide glide = Glide.get(context);
      requestManager =
          factory.build(
              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
      current.setRequestManager(requestManager);
    }
    return requestManager;
  }
```

然后,我们将该 `Lifecycle` 传入到 `RequestManager` 中,以 `RequestManager` 中的两个方法为例,`RequestManager` 会对 `Lifecycle` 进行监听,从而达到了对 Fragment 的生命周期进行监听的目的:

```java
  public void onStart() {
    resumeRequests();
    targetTracker.onStart();
  }

  public void onStop() {
    pauseRequests();
    targetTracker.onStop();
  }
```

### 1.3 小结

经过上述分析,我们可以使用下面的流程图总结 Glide 的 `with()` 方法的执行过程:

![Glide 的 with() 方法的执行过程](res/glide_with.jpg)

## 2、load() 方法的执行过程

### 2.1 load() 的过程

当我们拿到了 `RequestManager` 之后就可以使用它来调用 `load()` 方法了。在我们的示例中传入的是一个 url 对象。`load()` 方法也是重载的,我们可以传入包括 Bitmap, Drawable, Uri 和 String 等在内的多种资源类型。示例中会调用下面的这个方法得到一个 `RequestBuilder` 对象,显然这是一种构建者模式的应用。我们可以使用 `RequestBuilder` 的其他方法来继续构建图片加载请求,你可以通过查看它的源码了解 Glide 都为我们提供了哪些构建方法:

```java
  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }
```

在 `RequestBuilder` 的构造方法中存在一个 `apply()` 方法值得我们一提,其定义如下。从下面的方法定义中可以看出,我们可以通过为 `RequestBuilder` 指定 `RequestOptions` 来配置当前图片加载请求。比如,指定磁盘缓存的策略,指定占位图,指定图片加载出错时显示的图片等等。那么我们怎么得到 `RequestOptions` 呢?在 Glide 4.8.0 中的类 `RequestOptions` 为我们提供了一系列的静态方法,我们可以这些方法来得到 `RequestOptions` 的实例:

```java
  public RequestBuilder<TranscodeType> apply(RequestOptions requestOptions) {
    Preconditions.checkNotNull(requestOptions);
    this.requestOptions = getMutableOptions().apply(requestOptions);
    return this;
  }
```

回过头来,我们可以继续跟踪 `load()` 方法。其实,不论我们使用了 `load()` 的哪个重载方法,最终都会调用到下面的方法。它的逻辑也比较简单,就是将我们的图片资源信息赋值给 `RequestBuilder` 的局部变量就完事了。至于图片如何被加载和显示,则在 `into()` 方法中进行处理。

```java
  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }

  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }
```

### 2.2 小结

所以,我们可以总结 Glide 的 `load()` 方法的执行过程如下。也就是使用 `RequestManger` 得到一个 `RequestBuilder` 的过程:

![Glide 的 load() 方法执行过程](res/glide_load.jpg)

## 3、into() 方法的执行过程

考虑到 `into()` 方法流程比较长、涉及的类比较多,我们按照图片加载的过程将其分成四个阶段来进行介绍。

第一个阶段是开启 `DecodeJob` 的过程。`DecodeJob` 负责从缓存或者从原始的数据源中加载图片资源,对图片进行变换和转码,是 Glide 图片加载过程的核心。`DecodeJob` 继承了 `Runnable`,实际进行图片加载的时候会将其放置到线程池当中执行。这个阶段我们重点介绍的是从 `RequestBuilder` 构建一个 `DecodeJob` 并开启 `DecodeJob` 任务的过程。即构建一个 `DecodeJob` 并将其丢到线程池里的过程。

第二个阶段是打开网络流的过程。这个阶段会根据我们的图片资源来从数据源中加载图片数据。以我们的示例为例,在默认情况下会从网络当中加载图片,并得到一个 `InputStream`. 

第三个阶段是将输入流转换为 `Drawable` 的过程。得到了 `InputStream` 之后还要调用 `BitmapFactory` 的 `decodeStream()` 方法来从 `InputStream` 中得到一个 `Drawable`. 

第四个阶段是将 `Drawable` 显示到 `ImageView` 上面的过程。

### 3.1 阶段1:开启 DecodeJob 的过程

#### 3.1.1 流程分析

我们继续沿着 `into()` 方法进行分析。

`into()` 方法也定义在 `RequestBuilder` 中,并且也是重载的。不论我们调用哪个重载方法都会将要用来显示图片的对象封装成一个 `Target` 类型。`Target` 主要用来对用来显示图片的对象的生命周期进行管理。当我们要将图片加载到 ImageView 的时候,最终会调用下面的 `buildTarget()` 方法来讲我们的 ImageView 封装成一个 `ViewTarget`,然后调用 `into()` 的重载方法进行后续处理:

```java
  public <Z> ViewTarget<ImageView, Z> buildTarget(ImageView view, Class<Z> clazz) {
    if (Bitmap.class.equals(clazz)) {
      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);
    } else if (Drawable.class.isAssignableFrom(clazz)) {
      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);
    } else {
      throw new IllegalArgumentException(
          "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)");
    }
  }

  private <Y extends Target<TranscodeType>> Y into(Y target,
      RequestListener<TranscodeType> targetListener,
      RequestOptions options) {

    options = options.autoClone();
    Request request = buildRequest(target, targetListener, options); // 1

    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous)
        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
      request.recycle();
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        previous.begin();
      }
      return target;
    }

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request); // 2

    return target;
  }
```

在上面的 `into()` 方法的 `1` 处最终会调用到下面的方法来构建一个请求对象。(这里我们忽略掉具体的参数,只给看构建请求的逻辑)。简而言之,该方法会根据我们是否调用过 `RequestBuilder` 的 `error()` 方法设置过图片加载出错时候显示的图片来决定返回 `mainRequest` 还是 `errorRequestCoordinator`。因为我们没有设置该参数,所以会直接返回 `mainRequest`。
	
```java
  private Request buildRequestRecursive(/*各种参数*/) {

    ErrorRequestCoordinator errorRequestCoordinator = null;
    if (errorBuilder != null) {
      errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }

    Request mainRequest = buildThumbnailRequestRecursive(/*各种参数*/); // 1

    if (errorRequestCoordinator == null) {
      return mainRequest;
    }

    // ... 略

    Request errorRequest = errorBuilder.buildRequestRecursive(/*各种参数*/);
    errorRequestCoordinator.setRequests(mainRequest, errorRequest);
    return errorRequestCoordinator;
  }
```

上面是根据是否设置加载失败时显示的图片来决定返回的请求对象的。如果你使用过 Glide 的话,那么一定记得除了设置加载失败时的图片,我们还会先加载一张小图,即 `Thumbnail`。所以,在上面方法的 `1` 处会根据设置调用过 `RequestBuilder` 的 `thumbnail()` 方法来决定返回 `Thumbnail` 的请求还是真实图片的请求。同样因为我们没有设置过该方法,所以最终会调用下面的方法来构建最终的图片加载请求。

```java
  private Request obtainRequest(/*各种参数*/) {
    return SingleRequest.obtain(/*各种参数*/);
  }
```

在 `SingleRequest` 的 `obtain()` 方法中会先尝试从请求的池中取出一个请求,当请求不存在的时候就会实例化一个 `SingleRequest`,然后调用它的 `init()` 方法完成请求的初始化工作。这里的请求池使用了 Android 的 support v4 包中的 `Pool` 相关的 API. 它被设计用来构建基于数组的请求池,具体如何使用可以参考相关的文档和源码。

```java
  public static <R> SingleRequest<R> obtain(/*各种参数*/) {
    SingleRequest<R> request = (SingleRequest<R>) POOL.acquire();
    if (request == null) {
      request = new SingleRequest<>();
    }
    request.init(/*各种参数*/);
    return request;
  }
```

得到了请求之后会用 `RequestManager` 的 `track()` 方法:

```java
  void track(@NonNull Target<?> target, @NonNull Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
  }
```

该方法的主要作用有两个:

1. 调用 `TargetTracker` 的 `track()` 方法对对当前 `Target` 的生命周期进行管理;
2. 调用 `RequestTracker` 的 `runRequest()` 方法对当前请求进行管理,当 Glide 未处于暂停状态的时候,会直接使用 `Request` 的 `begin()` 方法开启请求。

下面是 `SingeleRequest` 的 `begin()` 方法。它会根据当前加载的状态来判断应该调用哪个方法。因为我们之前图片加载的过程可能因为一些意想不到的原因被终止,所以当重启的时候就需要根据之前的状态进行恢复。对于我们第一次加载的情况,则会直接进入到下方 1 处的 `onSizeReady()` 方法中:

```java
  public void begin() {
    if (model == null) {
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        width = overrideWidth;
        height = overrideHeight;
      }
      int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
      onLoadFailed(new GlideException("Received null model"), logLevel);
      return;
    }

    if (status == Status.RUNNING) {
      throw new IllegalArgumentException("Cannot restart a running request");
    }

    // 如果我们在完成之后重新启动(通常通过诸如 notifyDataSetChanged() 之类的方法,
    // 在相同的目标或视图中启动相同的请求),我们可以使用我们上次检索的资源和大小
    // 并跳过获取新的大小。所以,如果你因为 View 大小发生了变化而想要重新加载图片
    // 就需要在开始新加载之前清除视图 (View) 或目标 (Target)。
    if (status == Status.COMPLETE) {
      onResourceReady(resource, DataSource.MEMORY_CACHE);
      return;
    }

    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight); // 1
    } else {
      target.getSize(this);
    }

    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      target.onLoadStarted(getPlaceholderDrawable()); // 2
    }
  }
```

下面是 `onSizeReady()` 方法,我们可以看出它会先判断当前是否处于 `Status.WAITING_FOR_SIZE` 状态,并随后将状态更改为 `Status.RUNNING` 并调用 `engine` 的 `load()` 方法。显然,更改完状态之后继续回到上面的方法,在 2 处即调用了 `Target` 的 `onLoadStarted()` 方法。这样 `Target` 的第一个生命周期就被触发了。

```java
  public void onSizeReady(int width, int height) {
    if (status != Status.WAITING_FOR_SIZE) {
      return;
    }
    status = Status.RUNNING;

    float sizeMultiplier = requestOptions.getSizeMultiplier();
    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

    loadStatus = engine.load(/*各种参数*/);

    if (status != Status.RUNNING) {
      loadStatus = null;
    }
  }
```

然后,让我们将重点放到 `Engine` 的 `load()` 方法。该方法虽然不长,但是却包含了许多重要的内容。我们在下篇文章中将要研究的 Glide 的缓存就是在这里实现的。该方法大致的逻辑上,先尝试从内存缓存当中查找指定的资源,当内存中不存在的时候就准备使用 `DecodeJob` 来加载图片。

```java
  public <R> LoadStatus load(/*各种参数*/) {
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      return null;
    }

    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      return null;
    }

    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      current.addCallback(cb);
      return new LoadStatus(cb, current);
    }

    EngineJob<R> engineJob = engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    DecodeJob<R> decodeJob = decodeJobFactory.build(/*各种参数*/);

    jobs.put(key, engineJob);

    engineJob.addCallback(cb);
    engineJob.start(decodeJob);

    return new LoadStatus(cb, engineJob);
  }
```

上面方法中涉及两个类,一个是 `DecodeJob`、一个是 `EngineJob`。它们之间的关系是,`EngineJob` 内部维护了线程池,用来管理资源加载,已经当资源加载完毕的时候通知回调。 `DecodeJob` 继承了 `Runnable`,是线程池当中的一个任务。就像上面那样,我们通过调用 `engineJob.start(decodeJob)` 来开始资源加载。

#### 3.1.2 小结

![阶段1:开启 DecodeJob 的过程](res/glide_into_stage1.jpg)

根据上文中的分析,我们不难得出上面的流程图。不考虑缓存的问题,这个部分的逻辑还是比较清晰的,即:当调用了 `into()` 之后,首先构建一个请求对象 `SingleRequest`,然后调用 `RequestManager` 的 `track()` 方法对 `Request` 和 `Target` 进行管理;随后,使用 `Request` 的 `begin()` 方法来启动请求;该方法中会使用 `Engine` 的 `load()` 方法决定是从缓存当中获取资源还是从数据源中加载数据;如果是从数据源中加载数据的话,就构建一个 `DecodeJob` 交给 `EngineJob` 来执行即可。

### 3.2 阶段2:打开网络流的过程

#### 3.2.1 打开网络流的过程

在上面的分析中,将 `DecodeJob` 交给 `EngineJob` 就完事了。因为 `DecodeJob` 是一个任务,会在线程池当中进行执行。所以,如果我们继续追踪的话,就应该从 `DecodeJob` 的 `run()` 方法开始:

所以,如果想要找到加载资源和解码的逻辑,就应该查看 DecodeJob 的 `run()` 方法。下面就是这个方法的定义:

```java
  public void run() {
    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      runWrapped();
    } catch (Throwable t) {
      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      if (!isCancelled) {
        throw t;
      }
    } finally {
      if (localFetcher != null) {
        localFetcher.cleanup();
      }
      GlideTrace.endSection();
    }
  }
```

`DecodeJob` 的执行过程使用了状态模式,它会根据当前的状态决定将要执行的方法。在上面的方法中,当当前任务没有被取消的话,会进入到 `runWrapped()` 方法。该方法中会使用 `runReason` 作为当前的状态决定要执行的逻辑:

```java
  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }
```

这里的 `runReason` 是一个枚举类型,它包含的枚举值即为上面的三种类型。当我们在一个过程执行完毕之后会回调 `DecodeJob` 中的方法修改 `runReason`,然后根据新的状态值执行新的逻辑。

除了 `runReason`,`DecodeJob` 中还有一个变量 `stage` 也是用来决定 `DecodeJob` 状态的变量。同样,它也是一个枚举,用来表示将要加载数据的数据源以及数据的加载状态。它主要在加载数据的时候在 `runGenerators()`、`runWrapped()` 和 `getNextStage()` 三个方法中被修改。通常它的逻辑是,先从(大小、尺寸等)转换之后的缓存中拿数据,如果没有的话再从没有转换过的缓存中拿数据,最后还是拿不到的话就从原始的数据源中加载数据。

以上就是 `DecodeJob` 中的状态模式运行的原理。

对于一个新的任务,会在 `DecodeJob` 的 `init()` 方法中将 `runReason` 置为 `INITIALIZE`,所以,我们首先会进入到上述 `switch` 中的 `INITIALIZE` 中执行。然后,因为我们没有设置过磁盘缓存的策略,因此会使用默认的 `AUTOMATIC` 缓存方式。于是,我们将会按照上面所说的依次从各个缓存中拿数据。由于我们是第一次加载,并且暂时我们不考虑缓存的问题,所以,最终数据的加载会交给 `SourceGenerator` 进行。

不知道你是否还记得上一篇文章中我们在讲解在 Glide 中使用 OkHttp 时提到的相关的类。它们真正作用的地方就在下面的这个方法中。这是 `SourceGenerator` 的 `startNext()` 方法,它会:

1. 先使用 `DecodeHelper` 的 `getLoadData()` 方法从注册的映射表中找出当前的图片类型对应的 `ModelLoader`;
2. 然后使用它的 `DataFetcher` 的 `loadData()` 方法从原始的数据源中加载数据。

```java
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }
```

由于我们的图片时网络中的资源,在默认情况下会使用 Glide 内部的 `HttpUrlFetcher` 从网络中加载数据。其 `loadData()` 方法定义如下:

```java
  public void loadData(Priority priority, DataCallback<? super InputStream> callback) {
    try {
      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
      callback.onDataReady(result);
    } catch (IOException e) {
      callback.onLoadFailed(e);
    } finally {
    }
  }
```

很明显,这里从网络中打开输入流之后得到了一个 `InputStream` 之后就使用回调将其返回了。至于 `loadDataWithRedirects()` 方法的实现,就是使用 `HttpURLConnection` 打开网络流的过程,这里我们不进行详细的说明了。

#### 3.2.2 小结

![阶段2:打开网络流的过程](res/glide_into_stage2.jpg)

这样,`into()` 方法的第二个阶段,即从网络中获取一个输入流的过程就分析完毕了。整个过程并不算复杂,主要是在 `DecodeJob` 中的状态模式可能一开始看不太懂,还有就是其中涉及到的一些类不清楚其作用。如果你存在这两个疑惑的话,那么建议你:1).耐心思考下状态模式的转换过程;2).翻下上一篇文章了解自定义 Glide 图片加载方式的几个类的设计目的;3).最重要的,多看源码。

### 3.3 阶段3:将输入流转换为 Drawable 的过程

#### 3.3.1 转换 Drawable 的过程

在上面的小节中我们已经打开了网络流,按照 Android 自身提供的 `BitmapFactory`,我们可以很容易地从输入流中得到 Drawable 不是?那么为什么这个转换的过程还要单独分为一个阶段呢?

实际上,这里的转换过程并不比上面打开输入流的过程简单多少。这是因为它涉及转码和将图片转换成适合控件大小的过程。好了,下面就让我们来具体看一下这个过程都发生了什么吧!

首先,从上面的 `loadData()`,我们可以看出当得到了输入流之后会回调 `onDataReady()` 方法。这个方法会一直从 `HttpUrlFetcher` 中一直回调到 `SourceGenerator` 中。这里它会使用默认的磁盘缓存策略判断数据是否可以缓存,并决定对数据进行缓存还是继续回调。

```java
  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      cb.reschedule(); // 1
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }
```

因为我们的数据是使用 `HttpUrlFetcher` 加载的,所以将会进入到 1 处继续进行处理。此时,`DecodeJob` 将会根据当前的状态从 `run()` 方法开始执行一遍,并再次调用 `DataCacheGenerator` 的 `startNext()` 方法。但是,此次与上一次不同的地方在于,这次已经存在可以用于缓存的数据了。所以,下面的方法将会被触发:

```java
  private void cacheData(Object dataToCache) {
    try {
      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
      DataCacheWriter<Object> writer =
          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      helper.getDiskCache().put(originalKey, writer);
    } finally {
      loadData.fetcher.cleanup();
    }

    sourceCacheGenerator =
        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
  }
```

这里的主要逻辑是构建一个用于将数据缓存到磁盘上面的 `DataCacheGenerator`。`DataCacheGenerator` 的流程基本与 `SourceGenerator` 一致,也就是根据资源文件的类型找到 `ModelLoader`,然后使用 `DataFetcher` 加载缓存的资源。与之前不同的是,这次是用 `DataFecher` 来加载 `File` 类型的资源。也就是说,当我们从网络中拿到了数据之后 Glide 会先将其缓存到磁盘上面,然后再从磁盘上面读取图片并将其显示到控件上面。所以,当从网络打开了输入流之后 `SourceGenerator` 的任务基本结束了,而后的显示的任务都由 `DataCacheGenerator` 来完成。

与 `HttpUrlFetcher` 一样,File 类型的资源将由 `ByteBufferFetcher` 来加载,当它加载完毕之后也也会回调 `onDataReady()` 方法。此时,将会调用 `DataCacheGenerator` 的 `onDataReady()`:

```java
  public void onDataReady(Object data) {
    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);
  }
```

该方法会继续回调到 `DecodeJob` 的 `onDataFetcherReady()` 方法,后续的逻辑比较清晰,只是在不断继续调用方法,我们依次给出这些方法:

```java
  // DecodeJob#onDataFetcherReady()
  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,
      DataSource dataSource, Key attemptedKey) {
    // ... 赋值,略
    if (Thread.currentThread() != currentThread) {
      runReason = RunReason.DECODE_DATA;
      callback.reschedule(this);
    } else {
      try {
        // decode 数据以得到期待的资源类型
        decodeFromRetrievedData();
      } finally {
        GlideTrace.endSection();
      }
    }
  }

  // DecodeJob#decodeFromRetrievedData()
  private void decodeFromRetrievedData() {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      // ... 异常处理
    }
    // ... 释放资源和错误重试等
  }

  // DecodeJob#decodeFromData()
  private <Data> Resource<R> decodeFromData(DataFetcher<?> fetcher, Data data,
      DataSource dataSource) throws GlideException {
    try {
      // ... 略
      Resource<R> result = decodeFromFetcher(data, dataSource);
      return result;
    } finally {
      fetcher.cleanup();
    }
  }

  // DecodeJob#decodeFromFetcher()
  private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
      throws GlideException {
    LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
    return runLoadPath(data, dataSource, path);
  }

  // DecodeJob#runLoadPath()
  private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource,
      LoadPath<Data, ResourceType, R> path) throws GlideException {
    // ... 获取参数信息
    try {
      // 使用 LoadPath 继续处理
      return path.load(
          rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));
    } finally {
      rewinder.cleanup();
    }
  }

  // LoadPath#load()
  public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,
      int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {
    try {
      // 继续加载
      return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);
    } finally {
      listPool.release(throwables);
    }
  }

  // LoadPath#loadWithExceptionList()
  private Resource<Transcode> loadWithExceptionList(/*各种参数*/) throws GlideException {
    Resource<Transcode> result = null;
    for (int i = 0, size = decodePaths.size(); i < size; i++) {
      DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i);
      try {
        // 使用 DecodePath 继续处理
        result = path.decode(rewinder, width, height, options, decodeCallback);
      } catch (GlideException e) {
        exceptions.add(e);
      }
      if (result != null) {
        break;
      }
    }
    return result;
  }
```

经过了上面的一系列猛如虎的操作之后,我们进入了 `loadWithExceptionList()` 方法,这里会对 `DecodePath` 进行过滤,以得到我们期望的图片的类型。这个方法中调用了 `DecodePath` 的 `decode()` 方法。这个方法比较重要,它像一个岔路口:1 处的代码是将数据转换成我们期望的图片的过程;2 处的代码是当得到了期望的图片之后对处理继续处理并显示的过程。

```java
  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2
    return transcoder.transcode(transformed, options);
  }
```

然后,让我们继续沿着 `decodeResource()` 走。它会调用下面的这个循环对当前的数据类型和期望的、最终的图片类型匹配从而决定用来继续处理的 `ResourceDecoder`。

```java
  private Resource<ResourceType> decodeResourceWithList(/*各种参数*/) throws GlideException {
    Resource<ResourceType> result = null;
    for (int i = 0, size = decoders.size(); i < size; i++) {
      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
      try {
        DataType data = rewinder.rewindAndGet();
        if (decoder.handles(data, options)) {
          data = rewinder.rewindAndGet();
          result = decoder.decode(data, width, height, options);
        }
      } catch (IOException | RuntimeException | OutOfMemoryError e) {
        exceptions.add(e);
      }

      if (result != null) {
        break;
      }
    }
    return result;
  }
```

`ResourceDecoder` 具有多个实现类,比如 `BitmapDrawableDecoder`、`ByteBufferBitmapDecoder`等。从名字也可以看出来是用来将一个类型转换成另一个类型的。

在我们的程序中会使用 `ByteBufferBitmapDecoder` 来将 `ByteBuffer` 专成 `Bitmap`。它最终会在 `Downsampler` 的 `decodeStream()` 方法中调用 `BitmapFactory` 的 `decodeStream()` 方法来从输入流中得到 Bitmap。(我们的 `ByteBuffer` 在  `ByteBufferBitmapDecoder` 中先被转换成了输入流。)

```java
  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,
      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
    // ... 略
    TransformationUtils.getBitmapDrawableLock().lock();
    try {
      result = BitmapFactory.decodeStream(is, null, options);
    } catch (IllegalArgumentException e) {
      // ... 错误处理,略
    } finally {
      TransformationUtils.getBitmapDrawableLock().unlock();
    }

    if (options.inJustDecodeBounds) {
      is.reset();
    }
    return result;
  }
```

这样剩下的就只有不断继续向上回调或者返回,最终回到了我们上面所说的岔路口。这样从输入流中加载图片的逻辑就结束了:)

#### 3.3.2 小结

![阶段3:将输入流转换为 Drawable 的过程](res/glide_into_stage3.jpg)

怎么样,是不是觉得这个过程比打开输入流的过程复杂多了?毕竟这个部分涉及到了从缓存当中取数据以及向缓存写数据的过程,算的上是核心部分了。整体而言,这部分的设计还是非常巧的,即使用了状态模式,根据当前的状态来决定下一个 `Generator`。从网络中拿到输入流之后又使用 `DataCacheGenerator` 从缓存当中读取数据,这个过程连我第一次读源码的时候都没发现,以至于后来调试验证了推理之后才确信这部分是这样设计的……

### 3.4 阶段4:将 Drawable 展示到 ImageView 的过程

根据上面的分析,我们已经从网络中得到了图片数据,并且已经将其放置到了缓存中,又从缓存当中取出数据进行准备进行显示。上面的过程比较复杂,下面将要出场的这个阶段也并不轻松……

#### 3.4.1 最终展示图片的过程

在上面分析中,我们已经进入到了之前所谓的岔路口,这里我们再给出这个方法的定义如下。上面的分析到了代码 1 处,现在我们继续从代码 2 处进行分析。

```java
  // DecodePath#decode()
  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,
      Options options, DecodeCallback<ResourceType> callback) throws GlideException {
    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1
    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2
    return transcoder.transcode(transformed, options); // 3
  }
```

这里会调用 `callback` 的方法进行回调,它最终会回调到 `DecodeJob` 的 `onResourceDecoded()` 方法。其主要的逻辑是根据我们设置的参数进行变化,也就是说,如果我们使用了 `centerCrop` 等参数,那么这里将会对其进行处理。这里的 `Transformation` 是一个接口,它的一系列的实现都是对应于 `scaleType` 等参数的。

```java
  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {
    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
    Transformation<Z> appliedTransformation = null;
    Resource<Z> transformed = decoded;
    // 对得到的图片资源进行变换
    if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
      appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
      transformed = appliedTransformation.transform(glideContext, decoded, width, height);
    }
    if (!decoded.equals(transformed)) {
      decoded.recycle();
    }

    // ... 缓存相关的逻辑,略
    return result;
  }
```

在上面的方法中对图形进行变换之后还会根据图片的缓存策略决定对图片进行缓存。然后这个方法就直接返回了我们变换之后的图象。这样我们就又回到了之前的岔路口。程序继续执行就到了岔路口方法的第 3 行。这里还会使用 `BitmapDrawableTranscoder` 的 `transcode()` 方法返回 `Resouces<BitmapDrawable>`。只是这里会使用 `BitmapDrawableTranscoder` 包装一层,即做了延迟初始化处理。

这样,当第 3 行方法也执行完毕,我们的岔路口方法就分析完了。然后就是不断向上 `return` 进行返回。所以,我们又回到了 `DecodeJob` 的 `decodeFromRetrievedData()` 方法如下。这里会进入到下面方法的 1 处来完成最终的图片显示操作。

```java
  private void decodeFromRetrievedData() {
    Resource<R> resource = null;
    try {
      resource = decodeFromData(currentFetcher, currentData, currentDataSource);
    } catch (GlideException e) {
      throwables.add(e);
    }
    if (resource != null) {
      notifyEncodeAndRelease(resource, currentDataSource); // 1
    } else {
      runGenerators();
    }
  }
```

接着程序会达到 `DecodeJob` 的 `onResourceReady()` 方法如下。因为达到下面的方法的过程的逻辑比较简单,我们就不贴出这部分的代码了。

```java
  public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
  }
```

这里会获取到一个消息并将其发送到 `Handler` 中进行处理。当 `Handler` 收到消息之后会调用 `EncodeJob` 的 `handleResultOnMainThread()` 方法继续处理:

```java
  void handleResultOnMainThread() {
    // ... 略
    engineResource = engineResourceFactory.build(resource, isCacheable);
    hasResource = true;

    engineResource.acquire();
    listener.onEngineJobComplete(this, key, engineResource);

    for (int i = 0, size = cbs.size(); i < size; i++) {
      ResourceCallback cb = cbs.get(i);
      if (!isInIgnoredCallbacks(cb)) {
        engineResource.acquire();
        cb.onResourceReady(engineResource, dataSource); // 1
      }
    }
    engineResource.release();

    release(false /*isRemovedFromQueue*/);
  }
```

经过一系列的判断之后程序进入到代码 1 处,然后继续进行回调。这里的 `cb` 就是 `SingeleRequest`。

程序到了 `SingleRequest` 的方法中之后在下面的代码 1 处回调 `Target` 的方法。而这里的 `Target` 就是我们之前所说的 `ImageViewTarget`.

```java
  private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;

    isCallingCallbacks = true;
    try {
      // ... 略

      if (!anyListenerHandledUpdatingTarget) {
        Transition<? super R> animation =
            animationFactory.build(dataSource, isFirstResource);
        target.onResourceReady(result, animation); // 1
      }
    } finally {
      isCallingCallbacks = false;
    }

    notifyLoadSuccess();
  }
```

当程序到了 `ImageViewTarget` 之后会使用 `setResource()` 方法最终调用 `ImageView` 的方法将 `Drawable` 显示到控件上面。

```java
  protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }
```

这样,我们的 Glide 的加载过程就结束了。

#### 3.4.2 小结

![阶段4:将 Drawable 展示到 ImageView 的过程](res/glide_into_stage4.jpg)

上面是我们将之前得到的 `Drawable`  显示到控件上面的过程。这个方法包含了一定的逻辑,涉及的代码比较多,但是整体的逻辑比较简单,所以这部分的篇幅并不长。

### 4、总结

以上的内容便是我们的 Glide 加载图片的整个流程。从文章的篇幅和涉及的代码也可以看出,整个完整的过程是比较复杂的。从整体来看,Glide 之前启动和最终显示图片的过程比较简单、逻辑也比较清晰。最复杂的地方也是核心的地方在于 `DecodeJob` 的状态切换。

上面的文章中,我们重点梳理图片加载的整个流程,对于图片缓存和缓存的图片的加载的过程我没有做过多的介绍。我们会在下一篇文章中专门来介绍这部分内容。

以上。


================================================
FILE: 图片加载/Glide系列:Glide的缓存的实现原理.md
================================================
# Glide 系列-3:Glide 缓存的实现原理(4.8.0)

## 1、在 Glide 中配置缓存的方式

首先,我们可以在自定义的 GlideModule 中制定详细的缓存策略。即在 `applyOptions()` 中通过直接调用 `GlideBuilder` 的方法来指定缓存的信息:

```java
    @Override
    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
        builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE));
        builder.setMemoryCache(...);
        builder.setDiskCache(...);
        // ... 略
    }
```

另外,我们在每个图片加载请求中自定义当前图片加载请求的缓存策略,

```java
    Glide.with(getContext())
        .load("https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png")
        .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC))
        .apply(RequestOptions.skipMemoryCacheOf(false))
        .into(getBinding().iv);
```

以上是两个比较常用的缓存的配置方式,具体的 API 可以查看相关的源码了解. 

不论 Glide 还是其他的框架的缓存无非就是基于内存的缓存和基于磁盘的缓存两种,而且缓存的管理算法基本都是 LRU. 针对内存缓存,Android 中提供了 `LruCache`,笔者在之前的文章中曾经分析过这个框架:

[《Android 内存缓存框架 LruCache 的源码分析》](https://juejin.im/post/5bea581be51d451402494af2)

至于磁盘缓存, Glide 和 OkHttp 都是基于 [DiskLruCache](https://github.com/JakeWharton/DiskLruCache) 进行了封装。这个框架本身的逻辑并不复杂,只是指定了一系列缓存文件的规则,读者可以自行查看源码学习。本文中涉及上述两种框架的地方不再详细追究缓存框架的源码。

## 2、Glide 缓存的源码分析

### 2.1 缓存配置

首先, 我们在 `applyOptions()` 方法中的配置会在实例化单例的 Glide 对象的时候被调用. 所以, 这些方法的作用范围是全局的, 对应于整个 Glide.  下面的方法是 `RequestBuilder` 的 `build()` 方法, 也就是我们最终完
Download .txt
gitextract_c1rujubr/

├── API简析/
│   └── LruCache.md
├── Kotlin/
│   └── Kotlin.md
├── README.md
├── 其他/
│   ├── Android知识点随记.md
│   ├── MarkNote版本1的.md
│   ├── MarkNote版本2.md
│   └── 计算机视觉与Android.md
├── 响应式编程/
│   ├── RxJava系列-4:RxJava源码分析.md
│   ├── RxJava系列(1):一篇的比较全面的RxJava2方法总结.md
│   ├── RxJava系列(2):Flowable和背压.md
│   └── RxJava系列(3):用RxJava打造EventBus.md
├── 四大组件/
│   ├── Activity.md
│   ├── Broadcast.md
│   ├── Fragment.md
│   └── Service.md
├── 图片加载/
│   ├── Android相机最佳实践.md
│   ├── Glide系列:Glide主流程源码分析.md
│   ├── Glide系列:Glide的缓存的实现原理.md
│   ├── Glide系列:Glide的配置和使用方式.md
│   └── 图片压缩框架封装.md
├── 工作空间/
│   ├── OOM优化.md
│   ├── Tinker.md
│   ├── URL编码问题.md
│   ├── 文章暂时存放.md
│   ├── 百度定位API.md
│   └── 第三方库整理.md
├── 开发工具/
│   ├── ADB_常见的ADB指令总结.md
│   ├── Gradle_常见的指令和配置总结.md
│   └── Keytool_常用的指令.md
├── 异步编程/
│   ├── Android多线程编程:IntentService和HandlerThread.md
│   └── AsyncTask的使用和源码分析.md
├── 性能优化/
│   ├── Android性能优化-ANR.md
│   ├── Android性能优化-内存优化.md
│   ├── Android性能优化-启动优化.md
│   ├── Android性能优化-布局优化.md
│   ├── Android相机Camera1资料.md
│   ├── Android相机Camera2资料.md
│   └── Android进程保活.md
├── 消息机制/
│   ├── EventBus的源码分析.md
│   ├── 线程通信:Handler、MessageQueue和Looper.md
│   └── 跨进程通信:Binder机制.md
├── 混合开发/
│   └── ReactNative.md
├── 笔试面试/
│   ├── Android高级软件工程师2017.md
│   ├── Android高级面试_10_跨平台开发.md
│   ├── Android高级面试_11_JNINDK.md
│   ├── Android高级面试_12_各种三方库分析.md
│   ├── Android高级面试_12_算法.md
│   ├── Android高级面试_12_项目经验梳理.md
│   ├── Android高级面试_1_Handler相关.md
│   ├── Android高级面试_2_IPC相关.md
│   ├── Android高级面试_3_语言相关.md
│   ├── Android高级面试_4_虚拟机相关.md
│   ├── Android高级面试_5_四大组件、系统源码等.md
│   ├── Android高级面试_6_性能优化.md
│   ├── Android高级面试_7_网络相关.md
│   ├── Android高级面试_8_热修补插件化等.md
│   ├── Android高级面试_9_网络基础.md
│   ├── README.md
│   ├── java/
│   │   ├── ArrayList、LinkedList、Vector.md
│   │   ├── Collection包结构,与Collections的区别.md
│   │   ├── HashMap和ConcurrentHashMap的区别,HashMap的底层源码.md
│   │   ├── HashMap和HashTable的区别.md
│   │   ├── Hashcode的作用.md
│   │   ├── Java1.7与1.8新特性.md
│   │   ├── Java的四种引用,强弱软虚,用到的场景.md
│   │   ├── Map、Set、List、Queue、Stack的特点与用法.md
│   │   ├── Object有哪些公用方法.md
│   │   ├── Override和Overload的含义去区别.md
│   │   ├── Static class 与 non static class的区别.md
│   │   ├── String、StringBuffer与StringBuilder的区别.md
│   │   ├── Switch能否用string做参数.md
│   │   ├── TreeMap、HashMap、LindedHashMap.md
│   │   ├── equals与==的区别.md
│   │   ├── jvm-java 内存模型 以及各个分区具体内容.md
│   │   ├── throw和throws有什么区别.md
│   │   ├── wait()和sleep()的区别.md
│   │   ├── 九种基本数据类型的大小以及他们的封装类.md
│   │   ├── 内存溢出和内存泄露的区别.md
│   │   └── 解析XML的几种方式的原理与特点:DOM、SAX、PULL.md
│   ├── 今日头条Android面试.md
│   └── 初级工程师.md
├── 系统架构/
│   ├── Android应用启动过程.md
│   ├── Android应用安装过程.md
│   ├── Android打包过程.md
│   ├── Android系统启动过程.md
│   ├── Android系统架构.md
│   ├── SurefaceView_and_TextureView.md
│   ├── 控件体系/
│   │   ├── RV.md
│   │   ├── RV各种效果实现.md
│   │   ├── View体系详解:View的工作流程.md
│   │   ├── View体系详解:坐标系、滑动事件和分发机制.md
│   │   ├── View体系详解:自定义控件.md
│   │   └── 动画体系详解.md
│   └── 窗口机制/
│       └── Android的Window管理机制.md
├── 网络访问/
│   ├── OKHttp源码阅读.md
│   └── Retrofit源码阅读.md
└── 高阶技术/
    ├── Android插件化.md
    ├── Dagger从集成到源码.md
    ├── JNI技术总结.md
    ├── 探索Android架构设计.md
    ├── 注解在Android中的应用.md
    ├── 浅谈LiveData的通知过程.md
    └── 浅谈ViewModel生命周期控制.md
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,321K chars).
[
  {
    "path": "API简析/LruCache.md",
    "chars": 10399,
    "preview": "# Android 内存缓存框架 LruCache 的源码分析\n\nLruCache 是 Android 提供的一种基于内存的缓存框架。LRU 是 **Least Recently Used** 的缩写,即最近最少使用。当一块内存最近很少使用"
  },
  {
    "path": "Kotlin/Kotlin.md",
    "chars": 15902,
    "preview": "# Kotlin 学习笔记-1:基础语法\n\n## 1、初识 Kotlin\n\n### 1.1 基本特性梳理\n\n1. 本质上是**静态类型语言**,编译期确定类型,**但无需明确指定变量类型**;\n2. 对**可空类型的支持**,可以在编译期发"
  },
  {
    "path": "README.md",
    "chars": 4074,
    "preview": "# Android [DEPRECATED]\n\n## 1、目录\n\n### 基础开发\n\n- 基础回顾\n    - [Android 基础回顾:Activity 基础](四大组件/Activity.md)\n    - [Android 基础回顾"
  },
  {
    "path": "其他/Android知识点随记.md",
    "chars": 596,
    "preview": "\n## 异常处理\n\n对于未捕获的异常,借助 Thread 的静态方法来进行处理\n\n```java\n    public static void setDefaultUncaughtExceptionHandler(UncaughtExcep"
  },
  {
    "path": "其他/MarkNote版本1的.md",
    "chars": 3998,
    "preview": "# 马克笔记—Android 端开源的 Markdown 笔记应用\n\n![App 导引](https://github.com/Shouheng88/MarkNote/blob/master/resources/images/app.png"
  },
  {
    "path": "其他/MarkNote版本2.md",
    "chars": 17150,
    "preview": "# 承上启下:Markdown 笔记应用 MarkNote 的重构之路\n\n## 1、关于项目\n\n**MarkNote** 是一款 Android 端的笔记应用,它支持非常多的 Markdown 基础语法,还包括了 MathJax, Html"
  },
  {
    "path": "其他/计算机视觉与Android.md",
    "chars": 24451,
    "preview": "# Android 与计算机视觉\n\n不管你是否从事计算机视觉相关的工作,了解这方面的内容总是好的,因为即使你现在的工作与 AI 无关,采用一些开放的 API 仍然有可能让你的应用做得更好。比如,百度开发平台就提供了许多 AI 相关的 API"
  },
  {
    "path": "响应式编程/RxJava系列-4:RxJava源码分析.md",
    "chars": 12485,
    "preview": "# RxJava 系列-4:RxJava 源码分析\n\n在之前的文章中我们介绍了 RxJava 2 的常用的 API 的方法总结、背压的概念以及 RxJava 2 在项目中的实际应用。在本节中,我们将要对 RxJava 2 的源码进行分析。下"
  },
  {
    "path": "响应式编程/RxJava系列(1):一篇的比较全面的RxJava2方法总结.md",
    "chars": 32971,
    "preview": "# RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结\n\n看了许多讲解RxJava的文章,有些文章讲解的内容是基于第一个版本的,有些文章的讲解是通过比较常用的一些API和基础的概念进行讲解的。\n但是每次看到RxJava"
  },
  {
    "path": "响应式编程/RxJava系列(2):Flowable和背压.md",
    "chars": 5992,
    "preview": "# RxJava2 系列 (2):背压和Flowable\n\n背压(Back Pressure)的概念最初并不是在响应式编程中提出的,它最初用在流体力学中,指的是后端的压力,\n通常用于描述系统排出的流体在出口处或二次侧受到的与流动方向相反的压"
  },
  {
    "path": "响应式编程/RxJava系列(3):用RxJava打造EventBus.md",
    "chars": 8636,
    "preview": "# RxJava2 系列 (3):使用 Subject\n\n在这篇文章中,我们会先分析一下 RxJava2 中的 Subject ;然后,我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。\n\n在了解本篇文章的内容"
  },
  {
    "path": "四大组件/Activity.md",
    "chars": 3685,
    "preview": "# Android 基础回顾:Activity 基础\n\n## 1、Activity 的生命周期\n\n### 1.1 一般情况下的生命周期\n\n下图是一般情况下一个 Activity 将会经过的生命周期的流程图:\n\n![Activity的生命周期"
  },
  {
    "path": "四大组件/Broadcast.md",
    "chars": 2734,
    "preview": "# Android 基础回顾:Broadcast 基础\n\n## 1、关于广播\n\n广播是 Android 提供的一种全局通信机制。\n\n### 1.1 分类\n\n1. 按照注册方式:**静态注册和动态注册**两种;\n2. 按照作用范围:**本地广"
  },
  {
    "path": "四大组件/Fragment.md",
    "chars": 409,
    "preview": "# Android 基础回顾:Fragment 基础\n\n## 1、Fragment 的生命周期\n\n### 1.1 Fragment 的生命周期\n\n下面是 Fragment 的生命周期的流程图:\n\n![Fragment的生命周期](res/f"
  },
  {
    "path": "四大组件/Service.md",
    "chars": 11178,
    "preview": "# Android 基础回顾:Service 基础\n\nService 主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让 Service 在后台继续保持运行状态。相对于使用线程来"
  },
  {
    "path": "图片加载/Android相机最佳实践.md",
    "chars": 11329,
    "preview": "# CameraX:Android 相机库开发实践\n\n## 前言\n\n前段时间因为工作的需要对项目中的相机模块进行了优化,我们项目中的相机模块是基于开源库 CameraView 进行开发的。那次优化主要包括两个方面,一个是相机的启动速度,另一"
  },
  {
    "path": "图片加载/Glide系列:Glide主流程源码分析.md",
    "chars": 30303,
    "preview": "# Glide 系列-2:主流程源码分析(4.8.0)\n\nGlide 是 Android 端比较常用的图片加载框架,这里我们就不再介绍它的基础的使用方式。你可以通过查看其官方文档学习其基础使用。这里,我们给出一个 Glide 的最基本的使用"
  },
  {
    "path": "图片加载/Glide系列:Glide的缓存的实现原理.md",
    "chars": 15610,
    "preview": "# Glide 系列-3:Glide 缓存的实现原理(4.8.0)\n\n## 1、在 Glide 中配置缓存的方式\n\n首先,我们可以在自定义的 GlideModule 中制定详细的缓存策略。即在 `applyOptions()` 中通过直接调"
  },
  {
    "path": "图片加载/Glide系列:Glide的配置和使用方式.md",
    "chars": 13069,
    "preview": "# Glide 系列-1:预热、Glide 的常用配置方式及其原理\n\n在接下来的几篇文章中,我们会对 Android 中常用的图片加载框架 Glide 进行分析。在本篇文章中,我们先通过介绍 Glide 的几种常用的配置方式来了解 Glid"
  },
  {
    "path": "图片加载/图片压缩框架封装.md",
    "chars": 9833,
    "preview": "# 开源一个 Android 图片压缩框架\n\n在我们的业务场景中,需要使用客户端采集图片,上传服务器,然后对图片信息进行识别。为了提升程序的性能,我们需要保证图片上传服务器的速度的同时,保证用于识别图片的质量。整个优化包括两个方面的内容:\n"
  },
  {
    "path": "工作空间/OOM优化.md",
    "chars": 213,
    "preview": "\n### Oom\n\nOOM就是所谓的内存溢出(Out Of Memory),也就是说内存占有量超过了VM所分配的最大\n\n### 出现OOM的原因\n\n1. 加载对象过大\n2. 相应资源过多,来不及释放\n\n### 如何解决\n\n1. 在内存引用上"
  },
  {
    "path": "工作空间/Tinker.md",
    "chars": 291,
    "preview": "# Tinker 热补丁的源码分析\n\nTinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。\n\nGithub ht"
  },
  {
    "path": "工作空间/URL编码问题.md",
    "chars": 757,
    "preview": "今天在使用URL访问别人的接口的时候,因为要查询的数据中包含中文就出现了问题。\n\n    http://api.map.baidu.com/telematics/v3/weather?location=北京&output=json&ak=X"
  },
  {
    "path": "工作空间/文章暂时存放.md",
    "chars": 207,
    "preview": "\n[ Android ̱Ҫ֪һ](https://mp.weixin.qq.com/s/FudvsrZEEVnAbtMokpjP0Q)\n\n[Androidڴй©ĵ](https://mp.weixin.qq.com/s/Ag1QtUgIS"
  },
  {
    "path": "工作空间/百度定位API.md",
    "chars": 4366,
    "preview": "\n### 百度语音识别API异常:\n\n异常1:\n\n     java.lang.UnsatisfiedLinkError: No implementation found for void com.baidu.speech.core.BDS"
  },
  {
    "path": "工作空间/第三方库整理.md",
    "chars": 16784,
    "preview": "# \n\n## 1\n\n### 1.1 ܣܺʾ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ٿ|[FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)"
  },
  {
    "path": "开发工具/ADB_常见的ADB指令总结.md",
    "chars": 8,
    "preview": "\n\n\n\n\n\n\n\n"
  },
  {
    "path": "开发工具/Gradle_常见的指令和配置总结.md",
    "chars": 815,
    "preview": "# 常见的 Gralde 配置和指令总结\n\n## 1、依赖相关\n\n### 1.1 transitive = true\n\n```groovy\n    dependencies {\n        implementation (\"com.gi"
  },
  {
    "path": "开发工具/Keytool_常用的指令.md",
    "chars": 1214,
    "preview": "# Keytool 常用的指令合集\n\n### 获取 APK 的签名信息\n\n方式 1:在命令行中输入下面的命令,即可获取 SHA1 签名信息:\n\n    keytool -printcert -file C:\\META-INF\\CERT.RS"
  },
  {
    "path": "异步编程/Android多线程编程:IntentService和HandlerThread.md",
    "chars": 8174,
    "preview": "# Android 多线程编程:IntentService & HandlerThread\n\n因为 Android 是使用 Java 开发的,所以当我们谈及 Android 中的多线程,必然绕不过 Java 中的多线程编程。但在这篇文章中,"
  },
  {
    "path": "异步编程/AsyncTask的使用和源码分析.md",
    "chars": 10259,
    "preview": "# AsyncTask 的使用和源码分析\n\n## 1、AsyncTask的使用\n\n使用 `AsyncTask` 可以更加简单地实现任务的异步执行,以及任务执行完毕之后与主线程的交互。它被设计用来执行耗时比较短的任务,通常是几秒种的那种,如果"
  },
  {
    "path": "性能优化/Android性能优化-ANR.md",
    "chars": 6698,
    "preview": "# Android 性能优化 - ANR 的原因和解决方案\n\n## 1、出现 ANR 的情况\n\n满足下面的一种情况系统就会弹出 ANR 提示\n\n1. 输入事件(按键和触摸事件) 5s 内没被处理;\n2. BroadcastReceiver "
  },
  {
    "path": "性能优化/Android性能优化-内存优化.md",
    "chars": 29,
    "preview": "# Android 性能优化 - 内存优化\n\n\n\n\n\n\n\n"
  },
  {
    "path": "性能优化/Android性能优化-启动优化.md",
    "chars": 4065,
    "preview": "# Android 性能优化 - 启动优化\n\n## 1、基础\n\n### 1.1 启动的类型\n\n首先是启动的三种类型:\n\n1. **冷启动场景**:后台完全没有任何进程的情况下,启动最慢;\n2. **温启动场景**:按返回键退回主界面再从主界"
  },
  {
    "path": "性能优化/Android性能优化-布局优化.md",
    "chars": 3106,
    "preview": "# Android 性能优化 - 布局优化\n\n### 1. 合理选择 ViewGroup\n\n在选择使用 Android 中的布局方式的时候应该遵循:**尽量少使用性能比较低的容器控件,比如 RelativeLayout,但如果使用 Rela"
  },
  {
    "path": "性能优化/Android相机Camera1资料.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "性能优化/Android相机Camera2资料.md",
    "chars": 24965,
    "preview": "# Android 相机开发资料梳理\n\n使用相机的前提是获取到 CameraManager 对象,然后从 CameraManager 中获取 CameraDevice 对象。每个 CameraDevice 对象包含了一系列的属性信息。这些信"
  },
  {
    "path": "性能优化/Android进程保活.md",
    "chars": 72,
    "preview": "\n[ Android ̱Ҫ֪һ](https://mp.weixin.qq.com/s/FudvsrZEEVnAbtMokpjP0Q)\n\n\n\n"
  },
  {
    "path": "消息机制/EventBus的源码分析.md",
    "chars": 21681,
    "preview": "# Android EventBus 的源码解析\n\n## 1、EventBus 的使用\n\n### 1.1 EventBus 简介\n\nEventBus 是一款用于 Android 的事件发布-订阅总线,由 GreenRobot 开发,Gihu"
  },
  {
    "path": "消息机制/线程通信:Handler、MessageQueue和Looper.md",
    "chars": 24794,
    "preview": "# Android 消息机制:Handler、MessageQueue 和 Looper\n\n在这篇文章中,我们将会讨论 Android 的消息机制。提到 Handler,有过一些 Android 开发经验的都应该很清楚它的作用,通常我们使用"
  },
  {
    "path": "消息机制/跨进程通信:Binder机制.md",
    "chars": 28592,
    "preview": "# Android 系统源码-2:Binder 通信机制\n\nBinder 是 Android 系统中非常重要的组成部分。Android 系统中的许多功能建立在 Binder 机制之上。在这篇文章中,我们会对 Android 中的 Binde"
  },
  {
    "path": "混合开发/ReactNative.md",
    "chars": 2371,
    "preview": "\n### Text\n\n1.塢ɫֶ뷽ʽ\n\n```\n<Text style={{fontSize:20, color:'red', {textAlign:'center'}}/>\n```\n\n\n2.\n\n```\n<View style={{heig"
  },
  {
    "path": "笔试面试/Android高级软件工程师2017.md",
    "chars": 1064,
    "preview": "链接们:\n\n[11](https://www.jianshu.com/p/a2bbd8565a64)    \n\n[22](https://blog.csdn.net/WHB20081815/article/details/74436204)"
  },
  {
    "path": "笔试面试/Android高级面试_10_跨平台开发.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "笔试面试/Android高级面试_11_JNINDK.md",
    "chars": 65,
    "preview": "\n\n\n\n- [ ] Jni 用过么?\n\n\n- [ ] 是否熟悉 Android jni 开发,jni 如何调用 java 层代码\n"
  },
  {
    "path": "笔试面试/Android高级面试_12_各种三方库分析.md",
    "chars": 12699,
    "preview": "# Android 高级面试:三方库源码分析\n\n## 1、ARouter\n\n既然使用的时候需要 AnnotationProcessor,那么说明它是基于注解处理的,也就是编译期间根据注解动态生成代码,当然生成的代码要符合既定的规则,然后根据"
  },
  {
    "path": "笔试面试/Android高级面试_12_算法.md",
    "chars": 2474,
    "preview": "# Android 高级面试 12 —— 算法\n\n## 1.冒泡排序算法\n\n```java\n    protected static void exchange(Comparable[] arr, int fromPos, int toPo"
  },
  {
    "path": "笔试面试/Android高级面试_12_项目经验梳理.md",
    "chars": 2377,
    "preview": "\n\n## 相机\n\n- [ ] Android 中开启摄像头的主要步骤\n\n\n## 压缩\n\n深度研究:\n\n1. SurefaceView, TextureView, Camera\n2. RecyclerView\n3. Adapter + Fra"
  },
  {
    "path": "笔试面试/Android高级面试_1_Handler相关.md",
    "chars": 6994,
    "preview": "# Android 高级面试-1:Handler 相关\n\n**问题:Handler 实现机制(很多细节需要关注:如线程如何建立和退出消息循环等等)**    \n**问题:关于 Handler,在任何地方 new Handler 都是什么线程"
  },
  {
    "path": "笔试面试/Android高级面试_2_IPC相关.md",
    "chars": 10056,
    "preview": "# Android 高级面试-2:IPC 相关\n\n## 1、IPC\n\n**问题:Android 上的 IPC 跨进程通信时如何工作的**    \n**问题:简述 IPC?**    \n**问题:进程间通信的机制**    \n**问题:AID"
  },
  {
    "path": "笔试面试/Android高级面试_3_语言相关.md",
    "chars": 30680,
    "preview": "# Android 高级面试-3:语言相关\n\n## 1、Java 相关\n\n### 1.1 缓存相关\n\n**问题:LruCache 的原理?**\n**问题:DiskLruCache 的原理?**\n\nLruCache 用来实现基于内存的缓存,L"
  },
  {
    "path": "笔试面试/Android高级面试_4_虚拟机相关.md",
    "chars": 8291,
    "preview": "# Android 高级面试-4:虚拟机相关\n\n## 1、内存管理\n\n**问题:GC 回收策略**    \n**问题:Java 中内存区域与垃圾回收机制**    \n**问题:垃圾回收机制与调用 `System.gc()` 区别**   \n"
  },
  {
    "path": "笔试面试/Android高级面试_5_四大组件、系统源码等.md",
    "chars": 34725,
    "preview": "# Android 高级面试-5:四大组件、系统源码等\n\n## 1、四大组件\n\n### 1.1 Activity\n\n**问题:在两个 Activity 之间传递对象还需要注意什么呢?**\n\n对象的大小。Intent 中的 Bundle 是使"
  },
  {
    "path": "笔试面试/Android高级面试_6_性能优化.md",
    "chars": 14120,
    "preview": "# Android 高级面试-6:性能优化\n\n## 1、内存优化\n\n### 1.1 OOM\n\n**问题:OOM 的几种常见情形?**\n\n1. `数据太大`:比如加载图片太大,原始的图片没有经过采样,完全加载到内存中导致内存爆掉。\n2. `内"
  },
  {
    "path": "笔试面试/Android高级面试_7_网络相关.md",
    "chars": 11953,
    "preview": "# Android 高级面试-7:网络相关的三方库和网络协议等\n\n## 1、网络框架\n\n**问题:HttpUrlConnection, HttpClient, Volley 和 OkHttp 的区别?**\n\nHttpUrlConnectio"
  },
  {
    "path": "笔试面试/Android高级面试_8_热修补插件化等.md",
    "chars": 2134,
    "preview": "# Android 高级面试:插件化和热修复相关\n\n## 1、dex 和 class 文件结构\n\nclass 是 JVM 可以执行的文件类型,由 javac 编译生成;dex 是 DVM 执行的文件类型,由 dx 编译生成。\n\nclass "
  },
  {
    "path": "笔试面试/Android高级面试_9_网络基础.md",
    "chars": 25,
    "preview": "# Android 高级面试-5:网络基础\n\n\n\n"
  },
  {
    "path": "笔试面试/README.md",
    "chars": 5817,
    "preview": "# Android高级工程师学习清单\n\n## 1、Java语言\n\n*重点:Java容器、Java多线程编程、Java并发编程*\n\n### 1.1 Java容器\n\n- [ ] SpareArray原理\n- [x] LRUCache原理:[Lr"
  },
  {
    "path": "笔试面试/java/ArrayList、LinkedList、Vector.md",
    "chars": 957,
    "preview": "# 比较ArrayList、LinkedList、Vector\n\n它们之间的比较还是比较简单的:\n\n1. **同步性能**:ArrayList和LinkedList是非同步的,而Vector是同步的。也就是Vector在所有的操作方法上面都"
  },
  {
    "path": "笔试面试/java/Collection包结构,与Collections的区别.md",
    "chars": 109,
    "preview": "Collection是集合类的上级接口,子接口主要有Set 和List、Map。 \nCollections是针对集合类的一个帮助类,提供了操作集合的工具方法:一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。"
  },
  {
    "path": "笔试面试/java/HashMap和ConcurrentHashMap的区别,HashMap的底层源码.md",
    "chars": 236,
    "preview": "\n关于HashMap的底层原理可以参考:[https://github.com/Shouheng88/Java-Programming/blob/master/Java%E8%AF%AD%E8%A8%80/%E5%AE%B9%E5%99%A"
  },
  {
    "path": "笔试面试/java/HashMap和HashTable的区别.md",
    "chars": 220,
    "preview": "1. HashMap可以接受null键值和值,而Hashtable则不能。\n2. Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。\n3. HashTa"
  },
  {
    "path": "笔试面试/java/Hashcode的作用.md",
    "chars": 93,
    "preview": "hashCode()方法用来根据对象的字段信息生产对象的哈希码,主要用在比如HashMap和HashTable这样的散列表中。当对象不同的时候返回不同的哈希码有助于增强散列表的性能。\n\n"
  },
  {
    "path": "笔试面试/java/Java1.7与1.8新特性.md",
    "chars": 18,
    "preview": "# Java1.7与1.8新特性\n\n"
  },
  {
    "path": "笔试面试/java/Java的四种引用,强弱软虚,用到的场景.md",
    "chars": 1317,
    "preview": "## Java的四种引用,强软弱虚,用到的场景\n\nJava在JDK1.2之后引入了强引用、软引用、弱引用和虚引用4种概念,来表示引用的状态。四种引用强度依次减弱。\n\n### 1、强引用(StrongReference)\n\n强引用不会被GC回"
  },
  {
    "path": "笔试面试/java/Map、Set、List、Queue、Stack的特点与用法.md",
    "chars": 337,
    "preview": "## Map、Set、List、Queue、Stack的特点与用法\n\nMap与其他的几个不同。Map是基于键值对的,键Key是唯一不能重复的,一个键对应一个值,值可以重复。对于Java中的Map,它有几个实现,如HashMap和TreeMa"
  },
  {
    "path": "笔试面试/java/Object有哪些公用方法.md",
    "chars": 702,
    "preview": "## Object有哪些公用方法\n\n答案 7 个:getClass() hashCode() equals() toString() notify() notifyAll() wait()\n\n    public final native "
  },
  {
    "path": "笔试面试/java/Override和Overload的含义去区别.md",
    "chars": 514,
    "preview": "## 1.Override 特点   \n\n1. 覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配,才能达到覆盖的效果;   \n2. 覆盖的方法的返回值必须和被覆盖的方法的返回一致;   \n3. 覆盖的方法所抛出的异常必须和被覆盖方法的所抛"
  },
  {
    "path": "笔试面试/java/Static class 与 non static class的区别.md",
    "chars": 235,
    "preview": "### static class\n\n1. 用static修饰的是内部类,此时这个内部类变为静态内部类;对测试有用;\n2. 内部静态类不需要有指向外部类的引用;\n3. 静态类只能访问外部类的静态成员,不能访问外部类的非静态成员\n\n### no"
  },
  {
    "path": "笔试面试/java/String、StringBuffer与StringBuilder的区别.md",
    "chars": 813,
    "preview": "## String、StringBuffer与StringBuilder的区别\n\nString类型和StringBuilder及StringBuffer类型的主要性能区别其实在于String是不可变的对象, 因此在每次对String类型进行"
  },
  {
    "path": "笔试面试/java/Switch能否用string做参数.md",
    "chars": 334,
    "preview": "在jdk 7之前,switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。switch后面的括号里面只能放int类型的值,但由于byte,short,char类型,它们会自动转换为int类型(精"
  },
  {
    "path": "笔试面试/java/TreeMap、HashMap、LindedHashMap.md",
    "chars": 3104,
    "preview": "## TreeMap、HashMap、LindedHashMap\n\n通常在开发的过程中使用HashMap比较多,在Map中在Map中插入、删除和定位元素,HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap"
  },
  {
    "path": "笔试面试/java/equals与==的区别.md",
    "chars": 1300,
    "preview": "对这个问题,如果要回答的全面的话,我们应该这么讲:\n\n1. 当参与==比较的两个元素中有一个是值类型的,那么就按照值类型来比较。而引用类型按照值来比较的时候使用的是它们的hashCode的返回结果。只有当参与比较的两个元素都是引用类型的,那"
  },
  {
    "path": "笔试面试/java/jvm-java 内存模型 以及各个分区具体内容.md",
    "chars": 872,
    "preview": "# jvm-java 内存模型 以及各个分区具体内容\n\n1. 每一个应用程序都有一个JVM,而不是多个应用程序共享一个JVM;\n2. 首先通过编译器,把java源文件编译成 jvm语法的字节码文件。这个过程,是不涉及到JVM的;然后,jvm"
  },
  {
    "path": "笔试面试/java/throw和throws有什么区别.md",
    "chars": 282,
    "preview": "## throw和throws有什么区别\n\n下面是throw和throws的主要用法:\n\n    public void sort() throws IllegalAccessException {\n        throw new Il"
  },
  {
    "path": "笔试面试/java/wait()和sleep()的区别.md",
    "chars": 613,
    "preview": "## Java中sleep和wait的区别\n\n1. **所属的类不同**:这两个方法来自不同的类分别是,sleep来自Thread类,和wait来自Object类。\n\n    sleep是Thread的静态类方法,调用该方法的线程会进入睡眠"
  },
  {
    "path": "笔试面试/java/九种基本数据类型的大小以及他们的封装类.md",
    "chars": 685,
    "preview": "\n\t基本类型 大小(字节)   默认值         封装类\n\tbyte    1           (byte)0       Byte\n\tshort   2           (short)0      Short\n\tint   "
  },
  {
    "path": "笔试面试/java/内存溢出和内存泄露的区别.md",
    "chars": 671,
    "preview": "**内存溢出Out of Memory**:是指程序在申请内存时,没有足够的内存空间供其使用,出现Out Of Memory;比如申请了一个Integer,但给它存了long才能存下的数,那就是内存溢出。\n\n**内存泄露Memory Lea"
  },
  {
    "path": "笔试面试/java/解析XML的几种方式的原理与特点:DOM、SAX、PULL.md",
    "chars": 413,
    "preview": "# 解析XML的几种方式的原理与特点:DOM、SAX、PULL\n    \nDOM:消耗内存:先把xml文档都读到内存中,然后再用DOM API来访问树形结构,并获取数据。这个写起来很简单,但是很消耗内存。要是数据过大,手机不够牛逼,可能手机"
  },
  {
    "path": "笔试面试/今日头条Android面试.md",
    "chars": 8226,
    "preview": "连接 http://blog.51cto.com/13127751/2117127\n\n第一面 \n\n1,请编程实现单例模式,懒汉和饱汉写法。\n\n2,请编程实现Java的生产者-消费者模型 \n\n3,HashMap的内部结构? 内部原理? \n\n关"
  },
  {
    "path": "笔试面试/初级工程师.md",
    "chars": 3931,
    "preview": "# Java 面试清单\n\n## 1、Java 基础\n\n- [x] 九种基本数据类型的大小,以及他们的封装类;[参考](java/九种基本数据类型的大小以及他们的封装类.md)\n- [x] switch能否用String做参数;[参考](j"
  },
  {
    "path": "系统架构/Android应用启动过程.md",
    "chars": 14974,
    "preview": "# Android 应用启动过程\n\n在之前的文中,我们已经了解过了 Android 系统启动的过程。系统启动之后会由 PMS 安装系统应用,并启动 Launcher,也就是桌面程序。然后,我们安装的程序的图标将会显示到桌面上面。\n\n所谓应用"
  },
  {
    "path": "系统架构/Android应用安装过程.md",
    "chars": 15449,
    "preview": "# Android 系统源码源码-应用安装过程\n\nAndroid 中应用安装的过程就是解析 AndroidManifest.xml 的过程,系统可以从 Manifest 中得到应用程序的相关信息,比如 Activity、Service、Br"
  },
  {
    "path": "系统架构/Android打包过程.md",
    "chars": 1010,
    "preview": "# Android 打包过程\n\n![打包过程](https://upload-images.jianshu.io/upload_images/1441907-8a2c24bbb71c2cbf.png?imageMogr2/auto-orie"
  },
  {
    "path": "系统架构/Android系统启动过程.md",
    "chars": 23720,
    "preview": "# Android 系统启动过程\n\n现在我们来梳理下 Android 系统的启动过程。Android 启动过程还是比较重要的,因为在这个过程中除了要完成 Linux 系统的初始化工作还要完成 Android 的基础服务和启动界面的初始化工作"
  },
  {
    "path": "系统架构/Android系统架构.md",
    "chars": 445,
    "preview": "# Android 系统架构\n\n如下所示的时Andriod系统的架构图:\n\n![Andriod系统架构图](res/Andriod系统架构图.jpg)\n\n总结:\n\n*Android 架构 = 应用部分 + 核心部分 + 底层部分*\n\n1. "
  },
  {
    "path": "系统架构/SurefaceView_and_TextureView.md",
    "chars": 6293,
    "preview": "# Android:解析 SurefaceView & TextureView\n\n## 1、关于 SurefaceView 和 TextureView\n\n### 1.1 基础\n\n[SurfaceView](https://android.g"
  },
  {
    "path": "系统架构/控件体系/RV.md",
    "chars": 26549,
    "preview": "# RecyclerView 源码分析\n\nRV 和 ListView:\n\n1. 不同的地方\n2. 高效的原因\n3. 使用不便的地方\n\n要分析的内容:\n\n1. 基本的流程;\n2. 一些效果的实现的逻辑是如何解耦出来的;\n3. 缓存实现的原理。"
  },
  {
    "path": "系统架构/控件体系/RV各种效果实现.md",
    "chars": 2316,
    "preview": "##  RV 的各种效果实现\n\n### 触摸\n\n```kotlin\n// 定义 ItemTouchHelper.Callback\nclass SimpleItemTouchCallback<T, K : BaseViewHolder>(pr"
  },
  {
    "path": "系统架构/控件体系/View体系详解:View的工作流程.md",
    "chars": 23806,
    "preview": "# View 的绘制流程\n\n## 1、View 树的加载流程\n\n当我们调用 `startActivity()` 方法的时候,会调用到 `ActivityThread` 中的 `performLaunchActivity()` 获取一个 Ac"
  },
  {
    "path": "系统架构/控件体系/View体系详解:坐标系、滑动事件和分发机制.md",
    "chars": 17580,
    "preview": "# View 体系详解:坐标系、滑动、手势和事件分发机制\n\n## 1、位置\n\n### 1.1 坐标系\n\n下面是 Android 中的 View 坐标系的基本图。要获得一个 View 的位置,我们可以借助两个对象,一个是 View ,一个是 "
  },
  {
    "path": "系统架构/控件体系/View体系详解:自定义控件.md",
    "chars": 3115,
    "preview": "# View 体系详解:自定义控件\n\n在自定义控件之前,我们应该至少了解 Android 中控件体系的基础内容,你可以通过下面的三篇文章来获取这方面的知识:\n\n1. [《View 体系详解:坐标系、滑动、手势和事件分发机制》](https:"
  },
  {
    "path": "系统架构/控件体系/动画体系详解.md",
    "chars": 13268,
    "preview": "# Android 动画详解:属性动画、View 动画和帧动画\n\n在 Android 中,基本的动画共有三种类型:\n\n1. **View 动画**:也叫视图动画或者补间动画,主要是指 `android.view.animation` 包下面"
  },
  {
    "path": "系统架构/窗口机制/Android的Window管理机制.md",
    "chars": 7197,
    "preview": "# Android 窗口管理机制详解\n\n在之前的文章 [《View 体系详解:View 的工作流程》](https://juejin.im/post/5bc5e3fbe51d450e7e51befa) 中,我们分析了 View 的工作流程。"
  },
  {
    "path": "网络访问/OKHttp源码阅读.md",
    "chars": 33096,
    "preview": "# Andriod 网络框架 OkHttp 源码解析\n\n## 1、OkHttp 的基本使用\n\nOkHttp 是 Square 的一款应用于 Android 和 Java 的 Http 和 Http/2 客户端。使用的时候只需要在 Gradl"
  },
  {
    "path": "网络访问/Retrofit源码阅读.md",
    "chars": 14703,
    "preview": "# 动态代理在 Android 中的应用:Retrofit 源码解析\n\n在之前的文章 [《Andriod 网络框架 OkHttp 源码解析》](https://shouheng88.github.io/2018/10/17/Andriod%"
  },
  {
    "path": "高阶技术/Android插件化.md",
    "chars": 8996,
    "preview": "# Android 插件化框架 DynamicLoadApk 源码分析\n\nDynamicLoadApk 应该算是 Android 插件化诸多框架中资历比较老的一个了。它的项目地址在:[dynamic-load-apk](https://gi"
  },
  {
    "path": "高阶技术/Dagger从集成到源码.md",
    "chars": 8722,
    "preview": "# Dagger 从集成到源码带你理解依赖注入框架\n\n> 本文从例子到源码来帮助你学习和理解 Dagger 的集成,因为只有例子没有源码的博文看了之后经常让人一头雾水。\n> 学编程重点在于理解,而不是死记硬背每个注解该怎么使用!\n> 所以,"
  },
  {
    "path": "高阶技术/JNI技术总结.md",
    "chars": 11019,
    "preview": "# 在 Android 中使用 JNI 的总结\n\n最近在研究 Android 相机相关的东西,因为想要对相机做一个封装,于是想到要提供支持滤镜和图像动态识别相关的接口。在我找到一些资料中,它们的实现:一个是基于 OpenGL 的,一个是基于"
  },
  {
    "path": "高阶技术/探索Android架构设计.md",
    "chars": 14553,
    "preview": "# Android 应用架构设计探索:MVC、MVP、MVVM和组件化\n\nMVC、MVP和MVVM是常见的三种架构设计模式,当前MVP和MVVM的使用相对比较广泛,当然MVC也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个"
  },
  {
    "path": "高阶技术/注解在Android中的应用.md",
    "chars": 17800,
    "preview": "# Java 注解及其两种使用方法\n\n一般的,注解在 Android 中有两种应用方式,一种方式是基于反射的,即在程序的运行期间获取类信息进行反射调用;另一种是使用注解处理,在编译期间生成许多代码,然后在运行期间通过调用这些代码来实现目标功"
  },
  {
    "path": "高阶技术/浅谈LiveData的通知过程.md",
    "chars": 9527,
    "preview": "# 浅谈 LiveData 的通知机制\n\nLiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 Liv"
  },
  {
    "path": "高阶技术/浅谈ViewModel生命周期控制.md",
    "chars": 7149,
    "preview": "# 浅谈 ViewModel 的生命周期控制\n\n## 1、从一个 Bug 说起\n\n想必有过一定开发经验的同学对 ViewModel 都不会陌生,它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了,毕竟这种"
  }
]

About this extraction

This page contains the full source code of the Shouheng88/Android-notes GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (825.2 KB), approximately 309.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!