[
  {
    "path": "API简析/LruCache.md",
    "content": "# Android 内存缓存框架 LruCache 的源码分析\n\nLruCache 是 Android 提供的一种基于内存的缓存框架。LRU 是 **Least Recently Used** 的缩写，即最近最少使用。当一块内存最近很少使用的时候就会被从缓存中移除。在这篇文章中，我们会先简单介绍 LruCache 的使用，然后我们会对它的源码进行分析。\n\n## 1、基本的使用示例\n\n首先，让我们来简单介绍一下如何使用 LruCache 实现内存缓存。下面是 LruCache 的一个使用示例。\n\n这里我们实现的是对 RecyclerView 的列表的截图的功能。因为我们需要将列表的每个项的 Bitmap 存储下来，然后当所有的列表项的 Bitmap 都拿到的时候，将其按照顺序和位置绘制到一个完整的 Bitmap 上面。如果我们不使用 LruCache 的话，当然也能够是实现这个功能——将所有的列表项的 Bitmap 放置到一个 List 中即可。但是那种方式存在缺点：因为是强引用类型，所以当内存不足的时候会导致 OOM。\n\n在下面的方法中，我们先获取了内存的大小的 8 分之一作为缓存空间的大小，用来初始化 LruCache 对象，然后从 RecyclerView 的适配器中取出所有的 ViewHolder 并获取其对应的 Bitmap，然后按照键值对的方式将其放置到 LruCache 中。当所有的列表项的 Bitmap 都拿到之后，我们再创建最终的 Bitmap 并将之前的 Bitmap 依次绘制到最终的 Bitmap 上面：\n\n```java\n    public static Bitmap shotRecyclerView(RecyclerView view) {\n        RecyclerView.Adapter adapter = view.getAdapter();\n        Bitmap bigBitmap = null;\n        if (adapter != null) {\n            int size = adapter.getItemCount();\n            int height = 0;\n            Paint paint = new Paint();\n            int iHeight = 0;\n            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);\n\n            // 使用内存的 8 分之一作为该缓存框架的缓存空间\n            final int cacheSize = maxMemory / 8;\n            LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);\n            for (int i = 0; i < size; i++) {\n                RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));\n                adapter.onBindViewHolder(holder, i);\n                holder.itemView.measure(\n                        View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),\n                        View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));\n                holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),\n                        holder.itemView.getMeasuredHeight());\n                holder.itemView.setDrawingCacheEnabled(true);\n                holder.itemView.buildDrawingCache();\n                Bitmap drawingCache = holder.itemView.getDrawingCache();\n                if (drawingCache != null) {\n                    bitmaCache.put(String.valueOf(i), drawingCache);\n                }\n                height += holder.itemView.getMeasuredHeight();\n            }\n\n            bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);\n            Canvas bigCanvas = new Canvas(bigBitmap);\n            Drawable lBackground = view.getBackground();\n            if (lBackground instanceof ColorDrawable) {\n                ColorDrawable lColorDrawable = (ColorDrawable) lBackground;\n                int lColor = lColorDrawable.getColor();\n                bigCanvas.drawColor(lColor);\n            }\n\n            for (int i = 0; i < size; i++) {\n                Bitmap bitmap = bitmaCache.get(String.valueOf(i));\n                bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);\n                iHeight += bitmap.getHeight();\n                bitmap.recycle();\n            }\n        }\n\n        return bigBitmap;\n    }\n```\n\n因此，我们可以总结出 LruCahce 的基本用法如下：\n\n首先，你要声明一个缓存空间的大小，在这里我们用了运行时内存的 8 分之 1 作为缓存空间的大小\n\n```java\n    LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);\n```\n\n**但是应该注意的一个问题是缓存空间的单位的问题**。因为 LruCache 的键值对的值可能是任何类型的，所以你传入的类型的大小如何统计需要自己去指定。后面我们在分析它的源码的时候会指出它的单位的问题。LruCahce 的 API 中也已经提供了计算传入的值的大小的方法。我们只需要在实例化一个 LruCache 的时候覆写该方法即可。而这里我们认为一个 Bitmap 对象所占用的内存的大小不超过 1KB. \n\n然后，我们可以像普通的 Map 一样调用它的 `put()` 和 `get()` 方法向缓存中插入和从缓存中取出数据：\n\n```java\n    bitmaCache.put(String.valueOf(i), drawingCache);\n    Bitmap bitmap = bitmaCache.get(String.valueOf(i));\n```\n\n## 2、LruCahce 源码分析\n\n### 2.1 分析之前：当我们自己实现一个 LruCache 的时候，我们需要考虑什么\n\n在我们对 LruCache 的源码进行分析之前，我们现来考虑一下当我们自己去实现一个 LruCache 的时候需要考虑哪些东西，以此来带着问题阅读源码。\n\n因为我们需要对数据进行存储，并且又能够根据指定的 id 将数据从缓存中取出，所以我们需要使用哈希表表结构。或者使用两个数组，一个作为键一个作为值，然后使用它们的索引来实现映射也行。但是，后者的效率不如前者高。\n\n此外，我们还要对插入的元素进行排序，因为我们需要移除那些使用频率最小的元素。我们可以使用链表来达到这个目的，每当一个数据被用到的时候，我们可以将其移向链表的头节点。这样当要插入的元素大于缓存的最大空间的时候，我们就将链表末位的元素移除，以在缓存中腾出空间。\n\n综合这两点，我们需要一个既有哈希表功能，又有队列功能的数据结构。在 Java 的集合中，已经为我们提供了 LinkedHashMap 用来实现这个功能。\n\n实际上在 Android 中的 LruCache 也正是使用 LinkedHashMap 来实现的。LinkedHashMap 拓展自 HashMap。如果理解 HashMap 的话，它的源码就不难阅读。LinkedHashMap 仅在 HashMap 的基础之上，又将各个节点放进了一个双向链表中。每次增加和删除一个元素的时候，被操作的元素会被移到到链表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基础之上进行了一层拓展，不过 Android 中的 LruCache 的实现具有一些很巧妙的地方值得我们学习。\n\n### 2.2 LruCache 源代码分析\n\n从上面的分析中我们知道了选择 LinkedHashMap 作为底层数据结构的原因。下面我们分析其中的一些方法。这个类的实现还有许多的细节考虑得非常周到，非常值得我们借鉴和学习。\n\n#### 2.2.1 缓存的最大可用空间\n\n在 LruCache 中有两个字段 size 和 maxSize. maxSize 会在 LruCache 的构造方法中被赋值，用来表示该缓存的最大可用的空间：\n\n```java\n    int cacheSize = 4 * 1024 * 1024; // 4MiB，cacheSize 的单位是 KB\n    LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {\n        protected int sizeOf(String key, Bitmap value) {\n            return value.getByteCount();\n        }\n    }};\n```\n\n这里我们使用 4MB 来设置缓存空间的大小。我们知道 LruCache 的原理是指定了空间的大小之后，如果继续插入元素时，空间超出了指定的大小就会将那些“可以被移除”的元素移除掉，以此来为新的元素腾出空间。那么，因为插入的类型时不确定的，所以具体被插入的对象如何计算大小就应该交给用户来实现。\n\n在上面的代码中，我们直接使用了 Bitmap 的 `getByteCount()` 方法来获取 Bitmap 的大小。同时，我们也注意到在最初的例子中，我们并没有这样去操作。那样的话一个 Bitmap 将会被当作 1KB 来计算。\n\n这里的 sizeOf() 是一个受保护的方法，显然是希望用户自己去实现计算的逻辑。它的默认值是 1，单位和设置缓存大小指定的 maxSize 的单位相同：\n\n```java\n    protected int sizeOf(K key, V value) {\n        return 1;\n    }\n```\n\n这里我们还需要提及一下：虽然这个方法交给用户来实现，但是在 LruCache 的源码中，不会直接调用这个方法，而是\n\n```java\n    private int safeSizeOf(K key, V value) {\n        int result = sizeOf(key, value);\n        if (result < 0) {\n            throw new IllegalStateException(\"Negative size: \" + key + \"=\" + value);\n        }\n        return result;\n    }\n```\n\n所以，这里又增加了一个检查，防止参数错误。其实，这个考虑是非常周到的，试想如果传入了一个非法的参数，导致了意外的错误，那么错误的地方就很难跟踪了。如果我们自己想设计 API 给别人用并且提供给他们自己可以覆写的方法的时候，不妨借鉴一下这个设计。\n\n#### 2.2.2 LruCache 的 get() 方法\n\n下面我们分析它的 get() 方法。它用来从 LruCahce 中根据指定的键来获取对应的值：\n\n```java\n    /**\n     * 1). 获取指定 key 对应的元素，如果不存在的话就用 craete() 方法创建一个。\n     * 2). 当返回一个元素的时候，该元素将被移动到队列的首位；\n     * 3). 如果在缓存中不存在又不能创建，就返回n ull\n     */\n    public final V get(K key) {\n        if (key == null) {\n            throw new NullPointerException(\"key == null\");\n        }\n\n        V mapValue;\n        synchronized (this) {\n            // 在这里如果返回不为空的话就会将返回的元素移动到队列头部，这是在 LinkedHashMap 中实现的\n            mapValue = map.get(key);\n            if (mapValue != null) {\n                // 缓存命中\n                hitCount++;\n                return mapValue;\n            }\n            // 缓存没有命中，可能是因为这个键值对被移除了\n            missCount++;\n        }\n\n        // 这里的创建是单线程的，在创建的时候指定的 key 可能已经被其他的键值对占用\n        V createdValue = create(key);\n        if (createdValue == null) {\n            return null;\n        }\n\n        // 这里设计的目的是防止创建的时候，指定的 key 已经被其他的 value 占用，如果冲突就撤销插入\n        synchronized (this) {\n            createCount++;\n            // 向表中插入一个新的数据的时候会返回该 key 之前对应的值，如果没有的话就返回 null\n            mapValue = map.put(key, createdValue);\n            if (mapValue != null) {\n                // 冲突了，还要撤销之前的插入操作\n                map.put(key, mapValue);\n            } else {\n                size += safeSizeOf(key, createdValue);\n            }\n        }\n\n        if (mapValue != null) {\n            entryRemoved(false, key, createdValue, mapValue);\n            return mapValue;\n        } else {\n            trimToSize(maxSize);\n            return createdValue;\n        }\n    }\n```\n\n这里获取值的时候对当前的实例进行了加锁以保证线程安全。当用 map 的 get() 方法获取不到数据的时候用了 `create()` 方法。因为当指定的键值对找不到的时候，可能它本来就不存在，可能是因为缓存不足被移除了，所以，我们需要提供这个方法让用户来处理这种情况，该方法默认返回 null. 如果用户覆写了 `create()` 方法，并且返回的值不为 null，那么我们需要将该值插入到哈希表中。\n\n插入的逻辑也在同步代码块中进行。这是因为，创建的操作可能过长而且是非同步的。当我们再次向指定的 key 插入值的时候，它可能已经存在值了。所以当调用 map 的 put() 的时候如果返回不为 null，就表明对应的 key 已经有对应的值了，就需要撤销插入操作。最后，当 mapValue 非 null，还要调用 `entryRemoved()` 方法。每当一个键值对从哈希表中被移除的时候，这个方法将会被回调一次。\n\n最后调用了 `trimToSize()` 方法，用来保证新的值被插入之后缓存的空间大小不会超过我们指定的值。当发现已经使用的缓存超出最大的缓存大小的时候，“最近最少使用” 的项目将会被从哈希表中移除。\n\n那么如何来判断哪个是 “最近最少使用” 的项目呢？我们先来看下 `trimToSize()` 的方法定义：\n\n```java\n    public void trimToSize(int maxSize) {\n        while (true) {\n            K key;\n            V value;\n            synchronized (this) {\n                if (size < 0 || (map.isEmpty() && size != 0)) {\n                    throw new IllegalStateException(getClass().getName()\n                            + \".sizeOf() is reporting inconsistent results!\");\n                }\n\n                if (size <= maxSize) {\n                    break;\n                }\n\n                // 获取用来移除的 “最近最少使用” 的项目\n                Map.Entry<K, V> toEvict = map.eldest();\n                if (toEvict == null) {\n                    break;\n                }\n\n                key = toEvict.getKey();\n                value = toEvict.getValue();\n                map.remove(key);\n                size -= safeSizeOf(key, value);\n                evictionCount++;\n            }\n\n            entryRemoved(true, key, value, null);\n        }\n    }\n```\n\n显然，这里是使用了 LinkedHashMap 的 `eldest()` 方法，这个方法的返回值是：\n\n```java\n    public Map.Entry<K, V> eldest() {\n        return head;\n    }\n```\n\n也就是 LinkedHashMap 的头结点。那么为什么要移除头结点呢？这不符合 LRU 的原则啊，这里分明是直接移除了头结点。实际上不是这样，魔力发生在 `get()` 方法中。在 LruCache 的 get() 方法中，我们调用了 LinkedHashMap 的 `get()` 方法，这个方法中又会在拿到值的时候调用下面的方法：\n\n```java\n    void afterNodeAccess(Node<K,V> e) { // move node to last\n        LinkedHashMapEntry<K,V> last;\n        if (accessOrder && (last = tail) != e) {\n            LinkedHashMapEntry<K,V> p =\n                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;\n            p.after = null;\n            if (b == null)\n                head = a;\n            else\n                b.after = a;\n            if (a != null)\n                a.before = b;\n            else\n                last = b;\n            if (last == null)\n                head = p;\n            else {\n                p.before = last;\n                last.after = p;\n            }\n            tail = p;\n            ++modCount;\n        }\n    }\n```\n\n这里的逻辑是把 `get()` 方法中返回的结点移动到双向链表的末尾。所以，最近最少使用的结点必然就是头结点了。\n\n## 3、总结\n\n以上是我们对 LruCache 的是使用和源码的总结，这里我们实际上只分析了 `get()` 的过程。因为这个方法才是 LruCache 的核心，它包含了插入值和移动最近使用的项目的过程。至于 `put()` 和 `remove()` 两种方法，它们内部实际上直接调用了 LinkedHashMap 的方法。这里我们不再对它们进行分析。\n\n------\n**如果您喜欢我的文章，可以在以下平台关注我：**\n\n- 个人主页：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n"
  },
  {
    "path": "Kotlin/Kotlin.md",
    "content": "# Kotlin 学习笔记-1：基础语法\n\n## 1、初识 Kotlin\n\n### 1.1 基本特性梳理\n\n1. 本质上是**静态类型语言**，编译期确定类型，**但无需明确指定变量类型**；\n2. 对**可空类型的支持**，可以在编译期发现空指针；\n3. 支持**函数式编程**，虽然 Java 8 以后都支持了；\n4. 类文件的后缀名式 `.kt`，编译之后还是生成 class 文件，只是编译器使用的是 `kotlinc`（对应于 javac），执行 class 的时候还是使用 java；\n5. 可以使用转换器将 Java 转换成 kotlin；\n6. Kotlin 标准库给 Java 库做了封装，我们可以简化原生 Java 库的调用；\n\n### 1.2 类文件结构\n\n在 Kotlin 中文件名称和文件的内容没有关系（在 Java 中文件名和类名相同），并且文件内部定义的是函数还是类都没关系。比如，下面是定义在目录 `me/shouheng/demo1/FirstDemo.kt` 中的类和函数，这里类和函数处于文件的同一层次。另外，一个文件中还可以定义多个类和多个函数，都是允许的。\n\n```kotlin\npackage me.shouheng                                 // 包的声明应处于源文件顶部\n\nclass Person (age : Int, name : String)             // 声明了一个类\nclass Person2 (val age : Int, val name : String)    // 声明了一个类\n```\n\n类和函数的真实包名是由文件中的 **package** 关键字指定的，与文件结构没有必然的关系。当然，我们建议按照 Java 的规则使其对应起来，因为这样维护起来更好、逻辑更清晰些。\n\n### 1.3 定义函数\n\n```kotlin\nfun doSomething(person: Person) : Int {             // 定义了一个函数\n    // 在字符串中使用 “$+变量名” 的格式进行占位，相当于 \"My name is\" + persion + \"!\"\n    println(\"My name is $person!\")\n}\n\nfun sum(a: Int, b: Int) = a + b\n```\n\n1. 函数的定义使用关键字 `fun`，覆写函数的话就在函数名前面加上 `override fun`。\n2. 变量和返回值的类型被放在冒号后面，如果返回无意义的类型，可以使用 `Unit`，也可以省略。\n3. **字符串模板**：在字符串中使用 `$+变量名` 的格式进行占位（这叫字符串模板），如果希望使用美元符号，前面加上反斜杠即可。\n4. 也可以将表达式作为函数体、返回值类型自动推断。\n5. 如果需要把一个字符串当作正则表达式，需要显式调用字符串的 `toRegex()` 方法才行。\n6. 三重引号中的字符不会做任何转义，即 `\"\"\"$\"\"\"` 可以直接当作美元。\n\n### 1.4 定义变量\n\n```kotlin\nfun test(args : Array<String>) {\n    val a = Person(10, \"Ming\")\n    var b: Person\n    b = Person(11, \"Xing\")\n    doSomething(a)\n}\n```\n\n1. **数组**：没有专门用来声明数组的，全部都是类。可以像下面这样声明数组 `Array<String>`。另外，可以按照 `args[0]` 的方式获取数组元素。\n2. **声明变量有 `var` 和 `val` 两中方式**：系统可以自动推断类型，`var` 声明的变量可以二次赋值，而 `val` 不行，后者相当于 `final` 的。\n3. 虽然 `var` 类型的变量可以二次赋值，但是两次赋值的类型必须相同。\n4. 可以在声明变量的时候使用冒号指定变量的类型，像上面的 b 一样（大部分情况下可以省略，因为编译器可以自动推断）。\n5. 初始化一个类的时候不需要 `new` 关键字（抛出异常的时候自然也一样）。\n\n### 1.5 使用循环\n\n```kotlin               \n    // 循环 Map\n    val map = HashMap<Char, String>()\n    for (c in 'A'..'F') {           // 循环字符串\n        map[c] = Integer.toHexString(c.toInt())\n    }\n    for ((k, v) in map) { // 输出结果是 <A,41> <B,42> <C,43> <D,44> <E,45> <F,46>\n        print(\"<$k,$v> \")\n    }\n\n    // 循环列表\n    val items = listOf(\"apple\", \"banana\", \"kiwifruit\")\n    for (index in items.indices) {\n        println(\"item at $index is ${items[index]}\")\n    }\n\n    // while 循环\n    var index = 0\n    while (index < items.size) {    // 使用 while 循环\n        println(\"item at $index is ${items[index]}\")\n        index++\n    }\n```\n\n总结，\n\n1. Kotlin 的 for 循环与 Java 稍有不同，它跟 js 等更相似，即使用 `in` 关键字。\n2. 遍历 map 的时候使用上述方式，以键值对的形式遍历即可。\n3. 要按照索引的方式进行遍历，需要先使用列表的 `indices` 得到索引列表。\n4. while 循环和 Java 中的使用方式基本一致，包括 `while` 和 `do...while` 两种形式。\n\n### 1.6 when\n\n类似于 Java 中的 switch，但是它的每个条件中默认加入了 break. 另外，它还有一个比较好的地方是，它会检查枚举是否都包含进去了，如果没全部包含，它会提示你全包含或者加入 else 语句。\n\n```kotlin\nfun multiple(city: City2) = when(city) {\n    City2.BEIGING -> {\n        2*10000\n        2+2\n    }\n    City2.SHANGHAI,City2.GUANGZHOU->3*10000\n    else -> 5\n}\n\nfun describe(obj: Any): String =\n    when (obj) {\n        1          -> \"One\"\n        \"Hello\"    -> \"Greeting\"\n        is Long    -> \"Long\"\n        !is String -> \"Not a string\"\n        else       -> \"Unknown\"\n    }\n```\n\n另外，从示例 2 中可以看出，\n\n1. 注意每个条件之后需要加上 `->` 才行哦！\n2. 当多个类型的逻辑相同的时候，可以把它们放在 when 的一个条件里，然后用逗号分隔开。\n3. when 比 switch 的功能更加强大，它还可以使用不同类型的判断条件。（参考示例 2）\n\n### 1.7 控制语句\n\n#### 1.7.1 if 语句\n\n在 Kotlin 中，if是一个表达式，即它会返回一个值。 因此就不需要三元运算符 `? : `，比如 `val max = if (a > b) a else b`。\n\n#### 1.7.2 返回与跳转\n\nKotlin 中返回与跳转语句也是 return、break 和 continue 三种。它们的基本使用方式与 Java 相同。此外，Kotlin 中还支持标签。标签的格式为标识符后跟 @ 符号，例如：`abc@`、`fooBar@` 都是有效的标签。我们可以使用标签进行流程的控制（用的比较少）。\n\n### 1.8 异常处理\n\n`try..catch` 语句的基本结构如下，和 Java 基本相似，只是 catch 中声明变量的方式，下面的函数会当小于 0 时返回 -1，否则返回 1. 另外，kotlin 中不分受检异常和非受检异常，不会强制你捕获异常。\n\n```kotlin\nfun tryTest(i : Int) = try {\n    if (i < 0) throw IllegalArgumentException(\"< 0\")\n    else 1\n} catch (e : Exception) {\n    -1\n}\n```\n\n## 2、类与对象\n\n### 2.1 类声明\n\nKotlin 中使用关键字 `class` 声明类。类声明由类名、类头（指定其类型参数、主构造函数等）以及由花括号包围的类体构成。类头与类体都是可选的；如果一个类没有类体，可以省略花括号。\n\n```kotlin\nclass MyClass { /*...*/ }\nclass Empty\n```\n\n### 2.2 构造方法\n\n在 Kotlin 中的一个类可以有一个`主构造函数`以及一个或多个`次构造函数`。主构造函数是类头的一部分：它跟在类名（与可选的类型参数）后。如果主构造函数没有任何注解或者可见性修饰符，可以省略这个 `constructor` 关键字。\n\n```kotlin\nclass Person constructor(firstName: String) { ... } // 主构造函数\nclass Person(firstName: String) { ... }             // 省略主构造函数\nclass Person(val firstName: String) { ... } \nclass DontCreateMe private constructor () { ... }   // 将构造函数设置成私有的\n```\n\n注意上述声明方式中的 2 和 3 的区别，后者声明之后有一个局部变量 firstName，而前者没有声明任何变量。可以通过 private 关键字将构造函数设置成私有的。\n\n类也可以声明前缀有 constructor 的次构造函数。果类有一个主构造函数，每个次构造函数需要委托给主构造函数，可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可：\n\n```kotlin\n// 声明了一个次构造函数\nclass Person {\n    constructor(parent: Person) {\n        parent.children.add(this)\n    }\n}\n// 有主构造函数时，次构造函数的声明方式\nclass Person(val name: String) {\n    constructor(name: String, parent: Person) : this(name) {\n        parent.children.add(this)\n    }\n}\n```\n\n### 2.3 初始化代码块\n\nKotlin 中也有初始化代码块，非静态初始化代码块使用 `init` 关键字即可。\n\n```kotlin\nclass Person {\n    init { //\n        // ...\n    }\n}\n```\n\n初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句，因此所有初始化块中的代码都会在次构造函数体之前执行。\n\n静态代码块与静态变量定义的方式一致，略显繁琐，后续说明。\n\n### 2.4 函数\n\n函数是 Kotlin 中非常重要的概念，Kotlin 提供了许多便利的函数。\n\n```kotlin\n// 默认参数\nfun MyFun(a: String = \"a\", b: String) {\n    println(\"$a $b\")\n}\n\n// 为 String 增加函数\nfun String.lastChar() : Char = this[length - 1]\n\n// 为 String 增加属性\nval String.lastChar: Char\n    get() = get(length - 1) \n\n// 可变数量的参数\nfun varFun(vararg args: String) { \n    for (arg in args) println(arg)\n}\n\nfun main(args: Array<String>) {\n    MyFun(a = \"x\", b = \"y\")   // 指定参数名称：输出 x y，允许指定参数的名称\n    MyFun(b = \"y\")            // 指定参数名称：输出 a y，使用默认的参数\n    val args = arrayOf(\"A\", \"B\", \"C\")\n    varFun(*args)             // 使用伸展操作符调用可变数量参数的函数\n}\n```\n\n1. 允许在调用方法的时候指定参数的名称，并且指定了一个参数之后，后面的参数都要指定名称；\n2. 允许为函数的参数指定 `缺省参数`，比如上面的 a 默认是 `a`；\n3. 可以为别人的函数添加函数和属性，但是 `拓展函数无法访问私有的或者受保护的成员`。本质上拓展函数将调用它的实例当作第一个参数，这是本质的实现原理，很多问题可以依靠这个理解。拓展函数无法被继承，原因很简单，就是因为它们只相当于调用了一个静态方法而把实例当作参数实现的\n4. 可变数量参数函数调用的时候可以使用伸展（spread）操作符（就是在数组前面加 `*`）。缺省参数定义的时候需要使用 `vararg`（也许是因为 `..` 被当作其他用途了），当传入数组的时候的需要解包，也就是数组前面加 `*`。\n5. 把函数提升到与类同一层次，这样它就成 `静态函数` 了，把字段提升到与类同一层次，这样它就成 `静态字段` 了。\n6. 可以在函数内部定义`局部函数`，并且局部函数可以访问外部函数（即闭包）的局部变量。\n7. 导入函数的时候可以使用 `as` 重命名导入以简化使用。\n\n### 2.5 属性\n\n#### 2.5.1 声明属性\n\n声明类的属性有 var 和 val 两种方式。声明一个属性的格式是，\n\n```kotlin\nvar <propertyName>[: <PropertyType>] [= <property_initializer>]\n    [<getter>]\n    [<setter>]\n```\n\n示例程序，\n\n```kotlin\nclass Person{\n    var grade: Int = 0\n        get() = field + 1\n        set(value) {\n            field = value + 1\n        }\n    var age: Int = 0\n        private set     // 修改默认访问权限\n}\n```\n\nKotlin 中会将类的局部变量的访问权默认为 pulic 的，所以外部可以直接通过实例获取字段和赋值。可以通过上述方式来修改它的默认方法权限。\n\n可以通过 `get()` 和 `set()` 来重写 getter 和 setter 方法。一般情况下，使用默认的 `get()` 和 `set()` 默认逻辑即可，这也是 Java 规范。如果想增加新的逻辑，可以增加一个新的方法。注意，在覆写的时候，如果要修改属性的值，需要通过 field 来完成。field 标识符只能用在属性的访问器内，也被称为幕后字段。\n\n#### 2.5.2 编译期常量 const\n\n```kotlin\n// 定义在类顶层\nconst val SUBSYSTEM_DEPRECATED: String = \"This subsystem is deprecated\"\n\n// 定义在类内部，可以用来为类添加静态常量\nclass MyClass {\n    companion object {\n        const val EXTRA_LAUNCH_TYPE = \"__extra_launch_type\"\n    } // 外部访问方式是：MyClass.EXTRA_LAUNCH_TYPE\n}\n```\n\n使用 `const` 修饰符标记为编译期常量。 这些属性需要满足以下要求：\n\n1. 位于`顶层`或者是 `object` 声明或 `companion object` 的一个成员；\n2. 以 String 或原生类型值初始化；\n3. 没有自定义 getter。\n\n#### 2.5.3 延迟初始化 lateinit 属性与变量\n\n```kotlin\nlateinit var subject: TestSubject\n```\n\n一般地，属性声明为非空类型必须在构造函数中初始化。当无法在构造器中对属性初始化时，可以用 `lateinit` 修饰该属性。该修饰符只能用于在类体中的属性，而自 Kotlin 1.2 起，也用于顶层属性与局部变量。该属性或变量必须为`非空`类型，并且是`非原生类型`。\n\n在初始化前访问一个 lateinit 属性会抛出一个特定异常，该异常明确标识该属性被访问及它没有初始化的事实。自 1.2 起，可以该属性的引用上使用 `.isInitialized` 检测一个 lateinit var 是否已初始化。\n\n### 2.6 嵌套类与内部类\n\n#### 2.6.1 内部类\n\n下面是 Kotlin 中内部类的使用示例。在这个例子中，声明的内部类类似于 Java 中的非静态内部类，因此进行实例化的时候需要先获取到外部类的实例。\n\n```kotlin\nclass Outer {\n    private val bar: Int = 1\n    class Nested {\n        fun foo() = 2\n    }\n    inner class Inner {\n        fun foo() = bar // 可以访问外部类变量\n    }\n}\n\nval demo = Outer.Nested().foo() // == 2\n```\n\n类可以标记为 `inner` 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用。使用 `inner` 修饰的类属于内部类，没有使用的属于嵌套类。所以，上面的 Nested 属于嵌套类，Inner 属于内部类。但是注意嵌套类和内部类的区别：嵌套类不是内部类，不包含对外部类的引用。所以，比如 Android 中常见的内存泄漏的问题就可以避免了。\n\n#### 2.6.2 匿名内部类\n\n匿名内部类也是我们开发过程中比较常用的定义方式，比如设置点击事件的回调的时候。匿名类的定义又分成下面两种方式：\n\n```kotlin\n// 定义一个类\nopen class A(x: Int) {\n    public open val y: Int = x\n}\n\n// 使用匿名内部类\nwindow.addMouseListener(object : MouseAdapter() {\n    override fun mouseClicked(e: MouseEvent) { …… }\n    override fun mouseEntered(e: MouseEvent) { …… }\n})\n\n// 对函数式接口使用匿名内部类\nval listener = ActionListener { println(\"clicked\") }\n\n// 匿名内部类有多个超类的情况\nval val ab: A = object : A(1), MouseAdapter {\n    override val y = 15\n}\n\n// 不适用任何类创建匿名类实例\nval adHoc = object {\n    var x: Int = 0\n    var y: Int = 0\n}\n```\n\n第一种方式适用于类中包含多个方法的情形，如上面的 MouseAdapter 的匿名类；另一种方式适用于函数式接口，即只有一个方法的接口，如 ActionListener。\n\n如果一个类的超类型有一个构造函数，则必须传递适当的构造函数参数给它。多个超类型可以由跟在冒号后面的逗号分隔的列表指定，如 ab 的定义。\n\n如果不想要明确创建哪种类型，而只是想创建一个匿名类实例，可以按按上面的 `adHoc` 那样定义。\n\n#### 2.6.3 对象表达式\n\n上面是 object 定义匿名类的几个示例，除此之外，它还可以用来定义单例类，\n\n```kotlin\nobject DataProviderManager {\n    // 单例的方法\n    fun registerDataProvider(provider: DataProvider) {\n        // ……\n    }\n\n    // 单例类的属性\n    val allDataProviders: Collection<DataProvider>\n        get() = // ……\n}\n\n// 调用单例类的方法\nDataProviderManager.registerDataProvider(……)\n```\n\n这种形式定义的单例在初始化的时候是线程安全的，它调用的时候有点类似于 Java 中静态类的方法和属性的调用。这些对象也可以有父类，它的实现方式与普通的类的继承并无二致。\n\n注意：对象声明不能在局部作用域（即直接嵌套在函数内部），但是它们可以嵌套到其他对象声明或非内部类中。\n\n#### 2.6.4 伴生对象\n\n类内部的对象声明可以用 companion 关键字标记的对象是伴生对象，它的使用效果类似于 Java 中的静态字段和静态方法。伴生对象也是可以实现基类和接口的。比如，\n\n```kotlin\nclass MyClass {\n    companion object : Factory<MyClass> {\n        override fun create(): MyClass = MyClass()\n    }\n}\n\n// 调用的方式是：MyClass.create()\n```\n\n### 2.7 继承\n\n在 Kotlin 中所有类都有一个共同的超类 Any，类似于 Java 中的 Object，但是两者不同。\n\nKotlin 中的声明默认都是 `public final` 的，即公共且无法继承，如果希望一个类可以被继承，可以使用 open 关键字进行修饰。覆写函数的时候需要使用 override 关键字进行修饰，并且是必须的。属性的覆盖与函数的覆盖类似，都是使用 override 进行修饰。\n\n```kotlin\n// 基类，使用 open 关键字修饰\nopen class Base(p: Int)\n\n// 继承的时候调用基类的构造器\nclass Derived(p: Int) : Base(p)\n\n// 当基类有多个构造器的时候\nclass MyView : View {\n    constructor(ctx: Context) : super(ctx)\n\n    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)\n}\n```\n\n如果派生类有一个主构造函数，其基类型可以（并且必须） 用基类的主构造函数参数就地初始化。\n\n如果派生类没有主构造函数，那么每个次构造函数必须使用 super 关键字初始化其基类型，或委托给另一个构造函数做到这一点。 注意，在这种情况下，不同的次构造函数可以调用基类型的不同的构造函数。\n\n另外，\n\n1. 当在派生类的函数中调用父类函数的时候使用 `super.函数名` 即可，与 Java 一致。\n2. 如果实现了多个接口，想要调某个父接口的实现，需要按照 `super<接口>.函数名` 的形式调用。\n3. `abstract` 关键字的用法和 Java 一样，它同时具有 open 的语义。\n4. **可见性修饰符**：总共有四个，即 private、 protected、 internal 和 public。修饰符 `internal` 表示模块内可见；`protected` 表示子类可见；`private` 表示类内可见，并且子类可见并不代表模块内可见，两个之间没有关系；public 表示没有任何限制，并且是默认级别。\n5. 非静态内部类可以使用 `this@外部类名称` 访问外部类的方法和变量。\n\n### 2.8 接口\n\nKotlin 的接口与 Java 8 类似，既包含抽象方法的声明，也包含实现:                       \n\n```kotlin\ninterface IClickable {\n    fun onClick()\n    fun defaulFun() {  // 默认函数，不需要任何声明\n        println(\"I'm defaulFun().\")\n    }\n    fun defaulFun2() {\n        println(\"I'm defaulFun2().\")\n    }\n}\n```\n\n### 2.9 数据类\n\n专门用来存储数据的类，在普通类的基础之上使用 data 关键字修饰。系统会自动为我们的数据类生成：equals()、hashCode()、toString()、componentN() 和 copy() 函数的实现。\n\n```kotlin\ndata class User(val name: String, val age: Int)\n```\n\n数据类要求：\n\n1. 主构造函数需要至少有一个参数\n2. 主构造函数的所有参数需要标记为 val 或 var\n3. 数据类不能是抽象、开放、密封或者内部的\n4. （在1.1之前）数据类只能实现接口\n\n上面的 `copy()` 函数类似于 Java 中的 `clone()` 函数，用来实现函数的克隆。                                                                                            \n### 2.10 密封类\n\n密封类有点类似于枚举，要声明一个密封类，需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类，但是所有子类都必须在与密封类自身相同的文件中声明。密封类不允许有非-private 构造函数（其构造函数默认为 private）。\n\n```kotlin\nsealed class Expr\ndata class Const(val number: Double) : Expr()\ndata class Sum(val e1: Expr, val e2: Expr) : Expr()\nobject NotANumber : Expr()\n```\n\n理解上，密封类的作用是类似于枚举，但是对类的位置进行了限制。这是为了让运用于 when 的子类能够更容易被发现。\n\n### 2.11 枚举类\n\n声明枚举类的时候需要使用 enum 关键字，也可以给枚举增加一些属性，其定义方式基本同 Java. \n\n```kotlin\nenum class City {\n    BEIGING, SHANGHAI, GUANGZHOU\n}\nenum class City2(level:Int) {\n    BEIGING(1), SHANGHAI(2), GUANGZHOU(3)\n}\n```\n\n## 3、高阶特性\n\n### 3.1 Lambda 表达式\n\n#### 3.1.1 Lambda 表达式的基本示例\n\nLambda 表达式的格式是：`{ x: Int, y: Int -> x + y }`。它的使用比较简单，通常用来定义函数式接口。如果变量的含义明确，它还可以进一步简化，比如 `{ it * it}` 也是可以的。\n\n#### 3.1.2 集合与 Lambda\n\n以下面的程序为例，我们可以在集合中使用 Lambda 表达式。在 Java 8 中，我们可以使用 Stream 进行编程，而 Android 中要求 API 24 以上才能使用 Stream，所以 Kotlin 可以帮助我们解决这个遗憾。\n\n```kotlin\nlistOf(1,2,3,4).filter { it > 2 }.map { it.toString() }.all { it.length > 2 }\n```\n\n它支持的操作符包括：`filter`, `map`, `all`, `any`, `count` 和 `find`, `groupBy`, `flatMap` 和 `flatten`。它们的用法和效果与 Stream 或者 RxJava 中的操作符的含义一致。\n\n### 3.1.3 with 与 apply\n\n`with` 表示以某个类作为开始，对其进行操作，最后返回。`apply` 对应于 with，表示对某个实例进行某种操作；（省去了声明一个实例的过程，仅此而已，但是新添加一个语法……）\n\n它们的效果有点类似于在 Java 中的这种写法。\n\n```java\nnew LinedList<String>{\n    {\n        add(\"A\");\n        add(\"B\");\n    }\n}\n```\n\n也就是可以为声明的对象增加一些操作，但是这些过程都被包含在了 `with` 和 `apply` 中。参考下面的程序：\n\n```kotlin\n// with\nfun getString() : String = with(LinkedList<Int>()) {\n    for (i in 1..10) {\n        this.add(i) // 这里的 this 就是上面传入的列表\n    }\n    this.toString()\n}\n// apply\nfun getString2() : String = LinkedList<Int>().apply { \n    for (i in 1..10) {\n        add(i)\n    }\n}.toString()\n```\n\n### 3.2 区间\n\n与区间相关的几个操作符是 `..`、`in`、`!in`、`until`、`downTo` 以及 `until`。其含义如下，Kotlin 中的区间默认是闭区间的：\n\n```kotlin\n    val nums = 1..10\n    // 输出结果是 1..10\n    println(nums)                   \n \n    // 输出结果是 12345678910\n    for (num in nums) {             \n        print(num)\n    }                  \n \n    // 输出结果是 1086，10 递减到 5，步进 2\n    for (i in 10 downTo 5 step 2) { \n        print(i)\n    }                   \n \n    // 输出结果是 12345，1 递增到 6，步进 1\n    for (i in 1 until 6) {          \n        print(i)\n    }    \n```\n\n### 3.3 集合\n\nKotlin 中的集合比 Java 中的集合，增加了可变和不可变的概念。不可变集合的好处在于它的线程安全性（估计这个又是从 Guava 中借鉴来的概念）。在创建集合的时候，我们无需按照 Java 中使用 new 的方式来创建。在使用的时候还是要注意区分。下面我们来列举些这些集合，\n\n![不可变集合的创建方法](res/QQ截图20190303112453.png)\n\n![可变集合的创建方法](res/QQ截图20190303112506.png)\n\nKotlin 中的不可变集合的一个好处是，它本身就不会提供插入和删除的方法，所以无需担心因为该方法没有实现而出现的运行时异常。\n\n上面也说过，Kotlin 中的集合支持 Stream 的一些操作，除了上面的那些，它还支持许多其他的操作，这里就不一一列举了。\n\n### 3.4 类型系统\n\nKotlin 对空类型的处理比较好：默认所有的参数都是非空的，除非显式声明其可以为空。而 Java 中默认全部都是可空的。这可以有效帮助我们减少程序中的 NPE. \n\n```kotlin\nfun testFun1(param : String) {\n    print(param.length)\n}\n\n// 如果一个类是可空的那么必须显式声明，所以下面的程序编译器提示错误\nfun testFun2(param : String?) {\n//    print(param.length)\n}\n\nfun main(args : Array<String>) {\n//    testFun1(null) // 编译器提示错误\n    testFun2(null)\n    val str : String? = null\n    println(str?.length)            // null\n    println(str ?: \"B\")             // B\n    val b = \"AA\".let { it + \"A\" }   // AAA\n    println(b)\n}\n```\n\n1. 使用 `?` 在类型的后面则说明这个变量是可空的。\n2. 安全调用运算符 `?.`，以 `a?.method()` 为例，当 a 不为 null 则整个表达式的结果是 `a.method()` 否则是 null；\n3. Elvis 运算符 `?:`，以 `a ?: \"A\"` 为例，当 a 不为 null 则整个表达式的结果是 a，否则是 \"A\"；\n4. 安全转换运算符 `as?`，以 `foo as? Type` 为例，当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例，否则返回 null；\n5. 非空断言 `!!`，用在某个变量后面表示断言其非空，如 `a!!`；\n6. `let` 表示对调用 let 的实例进行某种运算，如 `val b = \"AA\".let { it + \"A\" }` 返回 \"AAA\"。如果使用 let 的某个对象是可空的，那么只有当该对象非空的时候才会执行 let。\n7. Kotlin 中进行类型之间的转换的时候必须显式进行，需要调用 `toXX()` 方法；\n8. `Any` 和 `Any?` 分别是所有非空和空类型的超类；\n9. `Unit` 相当于 Java 中的 void，返回 Unit 就相当于返回 void；\n\n## 4、协程\n\n常规的线程使用时，上下文切换会带来额外的性能开销。线程适用于 CPU 密集型的程序，而协程适合 Android 这种 IO 密集型的程序。从执行效果上面看，协程和线程达到的效果基本一致。它们的区别主要有以下几点：\n\n1. 协程不需要进行同步控制；\n2. 可以开大量的协程，但是线程数量是有限的，不然会影响程序的运行时性能；\n3. 使用 GlobalScope 启动的协程像守护线程，当程序中的所有线程都结束的时候，整个程序结束，没有执行完毕的协程不会继续执行；\n\n协程配置等相关信息：[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)\n\n**挂起函数**：使用 suspend 修饰的函数，挂起函数能够以与普通函数相同的方式获取参数和返回值，但是调用函数能挂起协程。挂起函数挂起协程时，不会阻塞协程所在的线程，挂起函数执行完成后会恢复协程。所以，挂起函数只能在协程中或其他挂起函数中调用。\n\n**CoroutineScope 和 CoroutineContext**：CoroutineScope 时协程本身，包含了 CoroutineContext。CoroutineContext，协程上下文，是一些元素的集合，主要包括 Job 和 CoroutineDispatcher 元素，可以代表一个协程的场景。\n\n**CoroutineDispatcher**：协程调度器，决定协程所在的线程或线程池。指定协程运行于特定的一个线程、一个线程池或者不指定任何线程。有三种标准实现 Dispatchers.Default、Dispatchers.IO，Dispatchers.Main和Dispatchers.Unconfined，Unconfined 就是不指定线程。\n\n**构建协程**：`CoroutineScope.launch {}` 不阻塞当前线程，在后台创建一个新协程，也可以指定协程调度器。`runBlocking {}`：创建一个新的协程同时阻塞当前线程，直到协程结束。`withContext {}` 不会创建新的协程，在指定协程上运行挂起代码块，并挂起该协程直至代码块运行完成。`async {}` 在后台创建一个新协程，跟 `CoroutineScope.launch {}` 的区别在于它有返回值。\n\n"
  },
  {
    "path": "README.md",
    "content": "# Android [DEPRECATED]\n\n## 1、目录\n\n### 基础开发\n\n- 基础回顾\n    - [Android 基础回顾：Activity 基础](四大组件/Activity.md)\n    - [Android 基础回顾：Fragment 基础](四大组件/Fragment.md)\n    - [Android 基础回顾：Service 基础](四大组件/Service.md)\n    - [Android 基础回顾：Broadcast 基础](四大组件/Broadcast.md)\n\n- 开发语言\n    - [Java 注解在 Android 中的应用](注解和依赖注入/注解在Android中的应用.md)\n    - [Kotlin 基础知识梳理](Kotlin/Kotlin.md)\n    - [在 Android 中使用 JNI 的总结](高阶技术/JNI技术总结.md)\n\n- 架构设计\n    - [Android 应用架构设计探索：MVC、MVP、MVVM和组件化](结构设计/探索Android架构设计.md)\n    - [浅谈 ViewModel 的生命周期控制](高阶技术/浅谈ViewModel生命周期控制.md)\n    - [浅谈 LiveData 的通知机制](高阶技术/浅谈LiveData的通知过程.md)\n\n- 性能优化\n    - [ANR](性能优化/Android性能优化-ANR.md)\n    - [布局优化](性能优化/Android性能优化-布局优化.md)\n    - [进程保活](性能优化/Android进程保活.md)\n    - [启动优化](性能优化/Android性能优化-启动优化.md)\n    - [内存优化](性能优化/Android性能优化-内存优化.md)\n\n- 开发环境\n    - [常见的 ADB 指令总结](开发工具/ADB_常见的ADB指令总结.md)\n    - [常见的 Gradle 指令和配置总结](开发工具/Gradle_常见的指令和配置总结.md)\n    - [常见的 Keytool 指令总结](开发工具/Keytool_常用的指令.md)\n\n### 系统源码\n\n- 核心流程\n    - [Android 系统架构](系统架构/Android系统架构.md)\n    - [Android 系统启动流程源码分析](系统架构/Android系统启动过程.md)\n    - [Android 应用打包过程](系统架构/Android打包过程.md)\n    - [Android 应用安装过程](系统架构/Android应用安装过程.md)\n\n- 消息机制\n    - [Android 消息机制：Handler、MessageQueue 和 Looper](消息机制/线程通信：Handler、MessageQueue和Looper.md.md)\n    - [Android IPC 机制：Binder 机制](消息机制/跨进程通信：Binder机制.md) \n\n- 异步编程\n    - [AsyncTask 的使用和源码分析](异步编程/AsyncTask源码分析.md)\n    - [Android 多线程编程：IntentService 和 HandlerThread](异步编程/Android多线程编程：IntentService和HandlerThread.md)\n\n- 窗口机制\n    - [Android 的窗口管理机制](系统架构/窗口机制/Android的Window管理机制.md)（编辑中）\n\n- 控件体系\n    - [View 体系详解：View的工作流程](系统架构/控件体系/View体系详解：View的工作流程.md)\n    - [View 体系详解：坐标系、滑动事件和分发机制](系统架构/控件体系/View体系详解：坐标系、滑动事件和分发机制.md)\n    - [Android 动画体系详解](系统架构/控件体系/动画体系详解.md)\n    - [SurfaceView 与 TextureView 的区别](系统架构/SurefaceView_and_TextureView.md)\n\n- 部分 API 源码\n    - [LruCache 的使用和源码分析](API简析/LruCache.md)\n\n### 三方库源码\n\n- 网络框架\n    - [网络框架 OkHttp 源码解析](网络访问/OKHttp源码阅读.md)\n    - [网络框架 Retrofit 源码解析](网络访问/Retrofit源码阅读.md)\n\n- 图片加载框架\n    - [Glide 系列-1：预热、Glide 的常用配置方式及其原理](图片加载/Glide系列：Glide的配置和使用方式.md)\n    - [Glide 系列-2：主流程源码分析](图片加载/Glide系列：Glide主流程源码分析.md)\n    - [Glide 系列-3：Glide 缓存的实现原理](图片加载/Glide系列：Glide的缓存的实现原理.md)\n\n- RxJava\n    - [RxJava2 系列-1：一篇的比较全面的 RxJava2 方法总结](响应式编程/RxJava2系列·_一篇的比较全面的RxJava2方法总结.md)\n    - [RxJava2 系列-2：Flowable 和背压](响应式编程/Flowable和背压.md)\n    - [RxJava2 系列-3：使用 Subject](响应式编程/用RxJava打造EventBus.md)\n    - [RxJava2 系列-4：RxJava 源码分析](响应式编程/RxJava系列-4：RxJava源码分析.md)\n\n- 其他框架\n    - [消息机制 EventBus 源码解析](消息机制/EventBus的源码分析.md)\n    - [Dagger 从集成到源码带你理解依赖注入框架](高阶技术/Dagger从集成到源码.md)\n\n### Java 相关\n\n- 并发编程\n    - [Java 并发编程：ThreadLocal 的使用及其源码实现](https://blog.csdn.net/github_35186068/article/details/83858944)\n\n- 设计模式\n    - [观察者模式](https://blog.csdn.net/github_35186068/article/details/83754026)\n\n- 虚拟机\n    - [内存管理](https://juejin.im/post/5b475e976fb9a04fa8671a45)\n    - [虚拟机执行子系统](https://juejin.im/post/5b4a1fb7e51d4519213fd374)\n    - [虚拟机内存模型与高效并发](https://juejin.im/post/5b4f48e75188251b1b448aa0)\n\n- 三方库\n    - [时间库 JodaTime](https://blog.csdn.net/github_35186068/article/details/83754146)\n\n### UI 相关\n\n- [自定义控件](系统架构/控件体系/View体系详解：自定义控件.md)（编辑中）\n\n### 编程基础\n\n- 数据库\n    - [MySQL 基础知识（全）](https://juejin.im/post/5a12d62bf265da431d3c4a01)\n\n### 面试题\n\n> 通过面试题梳理知识点细节\n\n- [Android高级面试_1_Handler相关](笔试面试/Android高级面试_1_Handler相关.md)\n- [Android高级面试_2_IPC相关](笔试面试/Android高级面试_2_IPC相关.md)\n- [Android高级面试_3_语言相关](笔试面试/Android高级面试_3_语言相关.md)\n- [Android高级面试_4_虚拟机相关](笔试面试/Android高级面试_4_虚拟机相关.md)\n- [Android高级面试_5_四大组件、系统源码等](笔试面试/Android高级面试_5_四大组件、系统源码等.md)\n- [Android高级面试_6_性能优化](笔试面试/Android高级面试_6_性能优化.md)\n- [Android高级面试_7_三方库相关](笔试面试/Android高级面试_7_三方库相关.md)\n- [Android高级面试_8_热修补插件化等](笔试面试/Android高级面试_8_热修补插件化等.md)\n- [Android高级面试_9_网络基础](笔试面试/Android高级面试_9_网络基础.md)\n- [Android高级面试_10_跨平台开发](笔试面试/Android高级面试_10_跨平台开发.md)\n- [Android高级面试_11_JNINDK](笔试面试/Android高级面试_11_JNINDK.md)\n- [Android高级面试_12_项目经验梳理](笔试面试/Android高级面试_12_项目经验梳理.md)\n- [Android 中高级工程师面试题总结](笔试面试/Android高级软件工程师2017.md)\n\n### 其他\n\n- [马克笔记—Android 端开源的 Markdown 笔记应用](其他/MarkNote版本1的.md)\n- [承上启下：Markdown 笔记应用 MarkNote 的重构之路](其他/MarkNote版本2.md)\n\n## 2、资源整理\n\n\n"
  },
  {
    "path": "其他/Android知识点随记.md",
    "content": "\n## 异常处理\n\n对于未捕获的异常，借助 Thread 的静态方法来进行处理\n\n```java\n    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {\n         defaultUncaughtExceptionHandler = eh;\n     }\n```\n\n## multidex\n\n这是因为安装应用时，有一步是使用 DexOpt 对 Dex 进行优化。这个过程会生成一个 ODex 文件，执行 ODex 的效率会比直接执行 Dex 文件的效率要高很多。在早期的 Android 系统中，DexOpt 把每一个类的方法 id 检索起来，存在一个链表结构里面。但是这个链表的长度是用一个 short 类型来保存的，导致了方法 id 的数目不能够超过65536 个。尽管在新版本的 Android 系统中，修复了 DexOpt 的这个问题，但是我们仍然需要对低版本的 Android 系统做兼容。\n\n为了解决方法数超限的问题，需要启用 multidex 将该 dex文 件拆成多个。\n\n## 动态布局\n\n就是指服务端使用 API 下发数据信息，然后客户端根据下发的信息进行动态布局。\n\n另一层含义可能是在代码中进行动态布局而不是使用 XML 的方式。\n\n\n\n\n"
  },
  {
    "path": "其他/MarkNote版本1的.md",
    "content": "# 马克笔记—Android 端开源的 Markdown 笔记应用\n\n![App 导引](https://github.com/Shouheng88/MarkNote/blob/master/resources/images/app.png?raw=true)\n\n> 马克笔记是运行在Android设备上面的一款开源的Markdown笔记，它的功能开发得已经比较完善，已经能够满足大部分用户的需求。现在将其开源到Github上面，用来交流和学习。当然，更希望你能够参与到项目的开发当中，帮助马克笔记变得更加有用。\n\n## 1、关于马克笔记\n\n马克笔记是一款开源的Markdown笔记应用，它的界面设计采用了Google最新的Material Design风格。该笔记现在的功能已经比较完善，能够满足用户大多数场景的需求。开源该软件的目的是希望与更多的人交流和学习，同时也希望能够有人参与到项目的开发中，一起帮助马克笔记，让它变得更加有用。\n\n你可以通过加入[Google+社区](https://plus.google.com/u/1/communities/102252970668657211916)来关注该软件开发的最新动态，并且可以参与Beta测试。\n\n马克笔记现在已经发布到了[酷安网](https://www.coolapk.com/apk/178276)上面，也欢迎你下载和使用该软件。另外，笔者还开发了一款清单应用[多功能清单](https://www.coolapk.com/apk/185660)，感兴趣的同学也可以了解一下。\n\n## 2、应用展示图\n\n<a href=\"#app\">这里</a>是该应用的一些截图通过Photoshop调整之后得到的展示图，通过展示图，你大概可以了解一下该软件的主要功能和开发状态。在接下来的行文中，我会向你更详细地介绍它使用到的一些技术以及现在开发完成的一些功能和特性。\n\n## 3、功能和特性\n\n我把该软件当前已经支持的功能列了一个清单：\n\n|编号|功能|\n|:-:|:-|\n|1|基本的**添加、修改、归档、放进垃圾箱、彻底删除**操作|\n|2|基本的Markdown语法，外加**MathJax**等高级特性|\n|3|特色的**时间线**功能，通过类似于AOP的操作记录用户的操作信息|\n|4|多种形式的媒体数据，包括**文件、视频、音频、图片、手写和位置信息**等|\n|5|**多主题**，支持**夜间主题**，并且有多种可选的**主题色和强调色**|\n|6|多彩的**图表**用于统计用户的数据信息|\n|7|三种形式的**桌面小控件**，并且可以为每个笔记添加快捷方式|\n|8|允许你为笔记指定多个多彩的标签|\n|9|使用“树结构”模拟文件夹操作，支持**多层文件夹**，并可以进行层级的搜索|\n|10|允许将笔记**导出为PDF、TXT、MD格式的文本、HTML和图片**|\n|11|使用**应用独立锁**，加强数据安全|\n|12|允许用户**备份数据到外部存储空间和OneDrive**|\n|13|图片**自动压缩**，节省本地的数据存储空间|\n\n将来希望开发和完善的功能:\n\n|编号|功能描述|\n|:-:|:-|\n|1|数据同步，本地的文件管理容易导致多平台的不一致，增加同步服务，能够实现多平台操作|\n|2|文件服务器，用于获取图片和文件的链接|\n|3|富文本编辑，即时的编辑预览|\n|4|允许添加闹钟，并且复选框可以编辑|\n|5|添加地图来展示用户的位置信息的变更|\n\n你可以从[更新日志](app/src/main/res/raw/changelog.xml)中获取到软件的更新信息。\n\n## 4、依赖和用到的一些技术\n\n马克笔记用到了MVVM的设计模式，还用到了DataBinding等一系列技术。下面的表格中列出了用到的具体的依赖和简要的描述。在此，还要感谢这些开源项目的作者：\n\n|编号|依赖|描述|\n|:-:|:-|:-|\n|1|[arch.lifecycle]()|使用ViewModel+LiveData实现Model和View的解耦|\n|2|[Stetho](https://github.com/facebook/stetho)|Facebook开源的安卓调试框架|\n|3|[Fabric]()|错误跟踪，用户数据收集|\n|4|[RxBinding](https://github.com/JakeWharton/RxBinding)||\n|5|[RxJava](https://github.com/ReactiveX/RxJava)||\n|6|[RxAndroid](https://github.com/ReactiveX/RxAndroid)||\n|7|[OkHttp](https://github.com/square/okhttp)||\n|8|[Retrofit](https://github.com/square/retrofit)||\n|9|[Glide](https://github.com/bumptech/glide)||\n|10|[BRVAH](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|非常好用的Recycler适配器|\n|11|[Gson](https://github.com/google/gson)||\n|12|[Joda-Time](https://github.com/JodaOrg/joda-time)|Java时间库|\n|13|[Apache IO](http://commons.apache.org/io/)|文件操作库|\n|14|[Material dialogs](https://github.com/afollestad/material-dialogs)||\n|15|[PhotoView](https://github.com/chrisbanes/PhotoView)||\n|16|[Hello charts](https://github.com/lecho/hellocharts-android)||\n|17|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)||\n|18|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)||\n|19|[CircleImageView](https://github.com/hdodenhof/CircleImageView)||\n|20|[Changeloglib](https://github.com/gabrielemariotti/changeloglib)|日志信息|\n|21|[PinLockView](https://github.com/aritraroy/PinLockView)|锁控件|\n|22|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)|底部弹出的对话框|\n|23|[Luban](https://github.com/Curzibn/Luban)|图片压缩|\n|24|[Flexmark](https://github.com/vsch/flexmark-java)|基于Java的Markdown文本解析|\n|25|[PrettyTime](https://github.com/ocpsoft/prettytime)|时间格式美化|\n\n\n特别需要说明的一点是，马克笔记是在开发了一段时间之后重新引入的ViewModel，因为作者本人水平有限，或者对ViewModel理解不够深入，设计难免有不足的地方，还请批评指正。\n\n### 数据库操作\n\n对于数据库部分，笔者自己设计了一套数据的访问逻辑，这里使用到了模板和单例等设计模式。它的好处在于，当你想要向程序中添加一个数据库实体的时候，只需要很少的配置即可，可以省去很多的样板代码。而且，由于该项目的一些特殊需求，比如要记录统计信息等，所以就自己设计了一下。当然，可能性能上仍然有许多值得提升的地方，但笔者认为仍不失为一个简单的学习材料。\n\n### Markdown解析\n\n对于Markdown解析，可以使用js在webview里面解析，也可以像本项目一样在程序种用java进行解析。笔者认为使用Flexmark在java种解析的好处是更方便地对解析的功能进行拓展。如该软件中的MathJax的解析就是在Flexmark的基础上进行的拓展。\n\n## 5、参与项目\n\n正如一开始提及的那样，马克笔记仍然有许多不足，我希望可以有更多的人帮助马克笔记继续完善它的功能。当然，这并不勉强。如果你希望对该项目贡献代码，你可以fork该项目，并向该项目提交请求。你可以在[waffle.io](https://waffle.io/Shouheng88/NotePal)上面跟踪issue的开发状态。或者，你发现了该软件中存在的一些问题，你可以在issue中向开发者报告。如果有其他的需求，可以直接通过[邮箱](mailto:shouheng2015@gmail.com)邮件开发者。\n\n## 6、项目地址\n\n因为这篇文章是从Github的Readme文件中拷贝出来的，所以忘记加上Github地址了，抱歉。现在补上：[Github](https://github.com/Shouheng88/MarkNote)"
  },
  {
    "path": "其他/MarkNote版本2.md",
    "content": "# 承上启下：Markdown 笔记应用 MarkNote 的重构之路\n\n## 1、关于项目\n\n**MarkNote** 是一款 Android 端的笔记应用，它支持非常多的 Markdown 基础语法，还包括了 MathJax, Html 等各种特性。此外，你还可以从相机或者相册中选择图象并将其添加到自己的笔记中。这很酷！因为你可以将自己的游记或者其他图片拍摄下来并将其作为自己笔记的一部分。这也是笔者开发这款软件的目的——希望 MarkNote 能够成为一款帮助用户记录自己生活的笔记应用。\n\n下面是我自己制作的一张部分功能预览图。这里仅仅列举了其中的部分页面，当然，你可以在酷安网或者 Google Play Store 上面获取到这个应用程序，并进一步了解它的全部功能，也可以在 Github 上得到最新版的应用的全部源代码。\n\n ![预览图](https://camo.githubusercontent.com/bea0cac9b6a352211c658024615419a785bc7fe6/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a553137333943454b3259667241726a376651546230672e6a706567)\n\n项目相关的**链接**：\n\n1. [酷安网下载链接：https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276)\n2. [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)\n3. [Github 项目链接：https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote)\n\n**最后**，之所以把这次重构称为 “承上启下” 的一个很重要的原因是：这次重构代码其实是为了后续功能的开发铺路。在未来，我会为这个应用增加更多有趣的功能。如果你对该项目感兴趣的话，可以 **Star** 或者 **Fork** 该项目，并为项目贡献代码。我们欢迎任何的、即使很小的贡献 :)\n\n## 2、关于重构\n\n在之前的版本中，MarkNote 在功能、界面和代码方面都存在一些不足，所以，前些日子我又专门抽了些时间对这些不足的地方进行了一些优化，时间大概从 11 月中旬直到 12 月中旬。这次重构也进行了大量的代码优化。经过这次重构，项目增加了大概 100 多次 commit. 下面我们列举一下本次重构所涉及的部分，其实也是这段时间以来学习到的东西的一些总结。\n\n### 2.1 项目结构优化\n\n#### 2.1.1 包结构优化\n\n首先，在之前笔者已经对项目的整个结构做了一次调整，主要是将项目中各个模块的位置进行了调整。这部分内容主要是项目中的 Gradle 配置和项目文件的路径的修改。在 `settings.gradle` 里面，我按照下面的方式指定了依赖的各个模块的路径：\n\n    include ':app', ':commons', ':data', ':pinlockview', ':fingerprint'\n    project(':commons').projectDir = new File('../commons')\n    project(':data').projectDir = new File('../data')\n    project(':pinlockview').projectDir = new File('../pinlockview')\n    project(':fingerprint').projectDir = new File('../fingerprint')\n\n这种方式最大的好处就是，项目中的 `app`, `commons`, `data` 等模块的文件路径处于相同的层次中，即：\n\n    --MarkNote\n         |----client\n         |----commons\n         |----data\n         ....\n\n这个调整当然是为了组件化开发做准备啦，当然这样的结构相比于将各个模块全部放置在 `client` 下面清晰得多。\n\n其次，我将项目中已经比较成熟的部分打包成了 `aar`，并直接引用该包，而不是继续将其作为一个依赖的形式。这样又进一步简化了项目的结构。\n\n最后是项目中的功能模块的拆分。在之前的项目中，Markdown 编辑器和解析、渲染相关的代码都被我放置在项目所引用的一个模块中。而这次，我直接将这个部分拆成了一个单独的项目并将其开源到了 Github. \n\n![EasyMark](res/easymark.png)\n\n这么做的主要目的是：\n\n1. 将核心的功能模块从项目中独立出来单独开发，以实现更多的功能并提升该部分的性能；\n2. 开源，希望能够帮助想实现一个 Markdown 笔记的开发者快速集成这个功能；\n3. 开源，希望能够有开发者参与进行以提升这部分的功能。\n\n关于 Markdown 处理的部分被开源到了 Github，其地址是：https://github.com/Shouheng88/EasyMark ，该项目中同时还包含了一个非常好用的编辑器菜单控件，感兴趣的同学可以关注一下这个项目。\n\n#### 2.1.2 MVVM 调整\n\n在该项目中，我们一直使用的是最新的 MVVM 设计模式，只是可惜的是在之前的版本中，笔者对 MVVM 的理解不够深入，所以导致程序的结构更像是 MVP. 本次，我们对这个部分做了优化，使其更符合 MVVM 设计原则。\n\n以笔记列表界面为例，当我们获取了对应于 Fragment 的 ViewModel 之后，我们统一在 `addSubscriptions()` 方法中对其通知进行订阅：\n\n        viewModel.getMutableLiveData().observe(this, resources -> {\n            assert resources != null;\n            switch (resources.status) {\n                case SUCCESS:\n                    adapter.setNewData(resources.data);\n                    getBinding().ivEmpty.showEmptyIcon();\n                    break;\n                case LOADING:\n                    getBinding().ivEmpty.showProgressBar();\n                    break;\n                case FAILED:\n                    ToastUtils.makeToast(R.string.text_failed);\n                    getBinding().ivEmpty.showEmptyIcon();\n                    break;\n            }\n        });\n\n这里返回的 resources，是封装的 `Resource` 的实例，是用来向观察者传递程序执行结果的包装类。然后，我们会使用 ViewModel 的 `fetchMultiItems()` 方法来根据之前传入的页面的状态信息拉取笔记记录：\n\n    public Disposable fetchMultiItems() {\n        if (mutableLiveData != null) {\n            mutableLiveData.setValue(Resource.loading(null));\n        }\n        return Observable.create((ObservableOnSubscribe<List<NotesAdapter.MultiItem>>) emitter -> {\n            List<NotesAdapter.MultiItem> multiItems = new LinkedList<>();\n            List list;\n            if (category != null) {\n                switch (status) {\n                    case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(category);break;\n                    case TRASHED: list = TrashHelper.getNotebooksAndNotes(category);break;\n                    default: list = NotebookHelper.getNotesAndNotebooks(category);\n                }\n            } else {\n                switch (status) {\n                    case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(notebook);break;\n                    case TRASHED: list = TrashHelper.getNotebooksAndNotes(notebook);break;\n                    default: list = NotebookHelper.getNotesAndNotebooks(notebook);\n                }\n            }\n            for (Object obj : list) {\n                if (obj instanceof Note) {\n                    multiItems.add(new NotesAdapter.MultiItem((Note) obj));\n                } else if (obj instanceof Notebook) {\n                    multiItems.add(new NotesAdapter.MultiItem((Notebook) obj));\n                }\n            }\n            emitter.onNext(multiItems);\n        }).observeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(multiItems -> {\n            if (mutableLiveData != null) {\n                mutableLiveData.setValue(Resource.success(multiItems));\n            }\n        });\n    }\n\n从上面也可以看出，我们将从数据库中获取到数据的许多逻辑放在了 ViewModel 中，并且每当想要拉取数据的时候调用一下 `fetchMultiItems()` 方法即可。这样，我们可以大大地减少 View 层的代码量。 View 层的逻辑也因此变得清晰得多。\n\n### 2.2 界面优化：更纯粹的质感设计\n\n记得在 Material Design 刚推出的时候，笔者和许多其他开发者一样兴奋。不过，在实际的开发过程中我却总是感觉不得要领，总觉少了一些什么。不过，经过前段时间的学习，我对在应用中实现质感设计有了更多的认识。\n\n#### 2.2.1 Toolbar 的阴影效果\n\n在之前的版本中，为了实现工具栏下面的阴影效果，我使用了在 Toolbar 下面增加一个高度为 `5dp` 的控件并为设置一个渐变背景的实现方式。这种实现方式可以完美兼容 Android 系统的各个版本。但是，这种实现的效果没有系统自带的显得那么自然。在新的版本中，我使用了下面的方式来实现阴影的效果：\n\n    <android.support.design.widget.CoordinatorLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\"\n        tools:context=\".activity.SearchActivity\">\n\n        <me.shouheng.commons.widget.theme.SupportAppBarLayout\n            android:id=\"@+id/bar_layout\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"?attr/colorPrimary\">\n\n            <android.support.v7.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"?attr/colorPrimary\"/>\n\n        </me.shouheng.commons.widget.theme.SupportAppBarLayout>\n\n    ...\n\n这里的 `SupportAppBarLayout` 继承自支持包的 `AppBarLayout`，主要用来实现日夜间主题的兼容。这样 Toolbar 下面就会带有一个漂亮的阴影，但是在比较低版本的手机上面是没有效果的，所以，为了兼容低版本的手机还要使用之前的那种使用控件填充的方式。（在新版本中暂时没有做这个处理）\n\n#### 2.2.2 日夜间主题兼容\n\n在之前的项目中，支持 20 多种主题颜色和强调色，不过最近随着 Google 在自己的项目中逐渐采用纯白色的设计，我也抛弃了之前的逻辑。现在整个项目中只支持三种主题：\n\n1. 白色的主题 + 蓝色的强调色\n2. 白色的主题 + 粉红的强调色\n3. 黑色的主题 + 蓝色的强调色\n\n![主题](res/themes.jpg)\n\n对于主题的支持，我依然延续了之前的实现方式——通过重建 Activity 来实现主题的切换。同时，为了达到某些控件随着主题自适应调整的目的，我定义了一些自定义控件，并在其中根据当前的设置选择使用的颜色。而对于其他可以直接使用项目中的强调色或者主题色的部分，我们可以直接使用当前的主题的值，比如下面的 Toolbar 的背景颜色会使用当前主题中的 `主题色`：\n\n    <android.support.v7.widget.Toolbar\n        android:id=\"@+id/toolbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"?attr/actionBarSize\"\n        android:background=\"?attr/colorPrimary\"/>\n\n#### 2.2.3 启动页优化\n\n之前的版本中在第一次打开程序的时候会有一个启动页来展示程序的功能，新版本中直接移除了这个功能。取而代之的是使用启动页来进行优化，首秀定义一个主题。这个主题只应用于第一次打开的 Activity。\n\n    <style name=\"AppTheme.Branded\" parent=\"LightThemeBlue\">\n        <item name=\"colorPrimaryDark\">#00a0e9</item>\n        <item name=\"android:windowBackground\">@drawable/branded_background</item>\n    </style>\n\n这里，我们将界面的背景更换成我们自己的项目的图标，因为项目图标中使用的颜色与状态栏的颜色不一致，所以，这里又重写了 `colorPrimaryDark` 属性以将状态栏的颜色和启动页的颜色设置成相同的效果：\n\n    <layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n        <item>\n            <color android:color=\"#00a0e9\"/>\n        </item>\n        <item>\n            <bitmap\n                android:src=\"@drawable/mn\"\n                android:tileMode=\"disabled\"\n                android:gravity=\"center\"/>\n        </item>\n    </layer-list>\n\n这种实现方式的效果是，在程序打开的时候不会存在白屏。之前的白屏会被我们指定的启动页替换掉（因为这个启动页是该 Activity 的窗口的背景）。当然，当页面打开完毕之后你还要在程序中将启动页背景替换掉。这样优化之后程序打开的时候显得更加自然、流畅。\n\n#### 2.2.4 动画优化\n\n因为时间的原因，在当前的版本中，我并没有加入太多的动画，而只是对程序中的一些地方增加了动画的效果。\n\n在笔记的列表中，我使用了下面的动画效果。这样当打开列表界面的时候各个条目会存在自底向上的进入动画。\n\n    private int lastPosition = -1;\n\n    @Override\n    protected void convert(BaseViewHolder helper, MultiItem item) {\n        // ... \n        /* Animations */\n        if (PalmUtils.isLollipop()) {\n            setAnimation(helper.itemView, helper.getAdapterPosition());\n        } else {\n            if (helper.getAdapterPosition() > 10) {\n                setAnimation(helper.itemView, helper.getAdapterPosition());\n            }\n        }\n    }\n\n    private void setAnimation(View viewToAnimate, int position) {\n        if (position > lastPosition) {\n            Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.anim_slide_in_bottom);\n            viewToAnimate.startAnimation(animation);\n            lastPosition = position;\n        }\n    }\n\n不过，这种方式实现的并不是最理想的效果，因为当打开页面的时候，多条记录会以一个整体的形式进入到页面中。这也是以后的一个优化的地方。\n\n### 2.3 使用 RxJava 重构\n\n在之前的项目中，当进行异步的操作的时候，需要定义一个 `AsyncTask`. 这种实现方式存在一个明显的问题，当需要执行的异步任务比较多，又无法进行复用的时候，你需要定义大量的 `AsyncTask`。另外，在各个页面之间进行数据传递的时候，如果单纯地使用 `onActivityResult()` 或者进行接口回调（Fragment 和 Activity 之间）会使得代码繁琐、难以阅读。针对这些问题，我们可以使用 RxJava 来进行很好的优化。\n\n首先是异步操作的问题，我们可以使用 RxJava 来实现线程的切换。以下面的这段代码为例，它被用来实现保存`快速笔记`的结果到文件系统和数据库中。在这段代码中，我们使用了 RxJava 的 `create()` 方法，并在其中进行逻辑的处理，然后使用 `subscribeOn()` 方法指定处理的线程是 IO 线程，并使用 `observeOn()` 方法指定最终处理的结果在主线程中进行处理：\n \n    public Disposable saveQuickNote(@NonNull Note note, QuickNote quickNote, @Nullable Attachment attachment) {\n        return Observable.create((ObservableOnSubscribe<Note>) emitter -> {\n            /* Prepare note content. */\n            String content = quickNote.getContent();\n            if (attachment != null) {\n                attachment.setModelCode(note.getCode());\n                attachment.setModelType(ModelType.NOTE);\n                AttachmentsStore.getInstance().saveModel(attachment);\n                if (Constants.MIME_TYPE_IMAGE.equalsIgnoreCase(attachment.getMineType())\n                        || Constants.MIME_TYPE_SKETCH.equalsIgnoreCase(attachment.getMineType())) {\n                    content = content + \"![](\" + quickNote.getPicture() + \")\";\n                } else {\n                    content = content + \"[](\" + quickNote.getPicture() + \")\";\n                }\n            }\n            note.setContent(content);\n            note.setTitle(NoteManager.getTitle(quickNote.getContent(), quickNote.getContent()));\n            note.setPreviewImage(quickNote.getPicture());\n            note.setPreviewContent(NoteManager.getPreview(note.getContent()));\n\n            /* Save note to the file system. */\n            String extension = UserPreferences.getInstance().getNoteFileExtension();\n            File noteFile = FileManager.createNewAttachmentFile(PalmApp.getContext(), extension);\n            try {\n                Attachment atFile = ModelFactory.getAttachment();\n                FileUtils.writeStringToFile(noteFile, note.getContent(), Constants.NOTE_FILE_ENCODING);\n                atFile.setUri(FileManager.getUriFromFile(PalmApp.getContext(), noteFile));\n                atFile.setSize(FileUtils.sizeOf(noteFile));\n                atFile.setPath(noteFile.getPath());\n                atFile.setName(noteFile.getName());\n                atFile.setModelType(ModelType.NOTE);\n                atFile.setModelCode(note.getCode());\n                AttachmentsStore.getInstance().saveModel(atFile);\n                note.setContentCode(atFile.getCode());\n            } catch (IOException e) {\n                emitter.onError(e);\n            }\n\n            /* Save note. */\n            NotesStore.getInstance().saveModel(note);\n\n            emitter.onNext(note);\n        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(note1 -> {\n            if (saveNoteLiveData != null) {\n                saveNoteLiveData.setValue(Resource.success(note1));\n            }\n        });\n    }\n\n另外是界面之间的结果传递的问题。对于 `onActivityResult()` 的执行结果，我们使用自定义的 `RxBus` 来传递信息，它的作用类似于 `EventBus`。然后，我们为此而封装了一个 `RxMessage` 对象来包装返回的结果。但是在程序中，我们尽量来简化和减少这种代码，因为过多的全局消息会让代码调试变得更加困难。我们希望代码逻辑更加简单、清晰。\n\nRxJava 除了能够完成线程切换的任务之外，对代码的可读性的提升效果也是非常明显的。另外，它还非常适用于局部的优化，比如，我们可以很轻易地改变自己的代码来将某个耗时逻辑放在异步线程中执行来提升界面的响应速度。\n\n### 2.4 增加新功能\n\n#### 2.4.1 桌面快捷方式\n\n桌面快捷方式并不是所有的 Android 桌面都支持的，我们在程序中有两个地方使用它。如下图所示，第一种方式是在笔记内部点击创建快捷方式的时候在桌面创建应用的快捷方式，我们可以通过点击快捷方式来快速打开笔记；第二种方式是长按应用图标的时候弹出一个菜单选项。\n\n![快捷方式](res/shortcuts.jpg)\n\n首先，第一种实现方式是在 7.0 之后加入的，之前我们也是可以创建快捷方式的，只是实现的方式与现在的方式不同而已。如下面这段代码所示，当 7.0 之后，我们使用 ShortcutManager 来创建快捷方式。之前，我们可以使用 \"com.android.launcher.action.INSTALL_SHORTCUT\" 这个 ACTION 并指定参数来创建快捷方式：\n\n    public static void createShortcut(Context context, @NonNull Note note) {\n        Context mContext = context.getApplicationContext();\n        Intent shortcutIntent = new Intent(mContext, MainActivity.class);\n        shortcutIntent.putExtra(SHORTCUT_EXTRA_NOTE_CODE, note.getCode());\n        shortcutIntent.setAction(SHORTCUT_ACTION_VIEW_NOTE);\n\n        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {\n            ShortcutManager mShortcutManager = context.getSystemService(ShortcutManager.class);\n            if (mShortcutManager != null && VERSION.SDK_INT >= VERSION_CODES.O) {\n                if (mShortcutManager.isRequestPinShortcutSupported()) {\n                    ShortcutInfo pinShortcutInfo = new Builder(context, String.valueOf(note.getCode()))\n                            .setShortLabel(note.getTitle())\n                            .setLongLabel(note.getTitle())\n                            .setIntent(shortcutIntent)\n                            .setIcon(Icon.createWithResource(context, R.drawable.ic_launcher_round))\n                            .build();\n\n                    Intent pinnedShortcutCallbackIntent = mShortcutManager.createShortcutResultIntent(pinShortcutInfo);\n\n                    PendingIntent successCallback = PendingIntent.getBroadcast(context, /* request code */ 0,\n                            pinnedShortcutCallbackIntent, /* flags */ 0);\n\n                    mShortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender());\n                }\n            } else {\n                createShortcutOld(context, shortcutIntent, note);\n            }\n        } else {\n            createShortcutOld(context, shortcutIntent, note);\n        }\n    }\n\n    private static void createShortcutOld(Context context, Intent shortcutIntent, Note note) {\n        Intent addIntent = new Intent();\n        addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);\n        addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, note.getTitle());\n        addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,\n                Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher_round));\n        addIntent.setAction(\"com.android.launcher.action.INSTALL_SHORTCUT\");\n        context.sendBroadcast(addIntent);\n    }\n\n对于第二种实现方式，我们可以在 Manifest 文件中进行注册，并为其指定 ACTION 和启动类来实现各个选项被点击之后发送的事件。然后，我们在指定的 Activity 中对各个 ACTION 进行处理即可，具体可以参考源代码。另外，这里的快速创建笔记还是比较有意思的，可以打开一个背景透明的 Activity 并在其中弹出一个自定义对话框来快速编辑笔记。可以帮助我们快速地记录自己的笔记。\n\n#### 2.4.2 指纹解锁 \n\n当然，这部分功能，我们直接使用了一个开源的三方库。毕竟人家为还为各个系统的指纹解锁的支持做了处理，所以这里我们直接奉行拿来主义了。这个项目的地址是：https://github.com/uccmawei/FingerprintIdentify. \n\n#### 2.4.3 打开网页的各种问题\n\n打开网页当然不难实现，我们使用一个自定义的 WebView 即可实现。不过，在这个项目的重构版本中，我们采用了一个开源的库 AgentWeb，它可以满足我们非常多场景的应用。\n\n另外，因为在我们的新的重构版本中，将支持包和 targetApi 都提升到了 28，所以出现了一个问题：使用 `http` 的网页无法打开。为了解决这个问题，我们需要在 Manifest 文件中指定网络配置文件的地址：\n\n    android:networkSecurityConfig=\"@xml/network_security_config\"\n\n然后，在该配置文件中指定我们可以访问的 http 白名单：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <network-security-config>\n        <domain-config cleartextTrafficPermitted=\"true\">\n            <domain includeSubdomains=\"true\">mikecrm.com</domain>\n            <domain includeSubdomains=\"true\">m.weibo.cn</domain>\n        </domain-config>\n    </network-security-config>\n\n在这里我们还发现了一个其他的问题：我们打开网页的时候设置的 Weibo 的链接是 https 的，但是因为我们在移动设备上面使用，所以又被重定向到了 `http://m.weibo.cn`，导致我们的网页无法打开。解决的方式即按照上面那样，将重定向之后的地址添加到白名单之中即可。\n\n#### 2.4.4 其他\n\n1. 在新的版本中，为了帮助我们进一步优化程序，我们使用了友盟进行埋点。\n2. 不注册支付宝和微信支付账号进行打赏；\n3. 分享相关的逻辑等；\n4. 其他：新版本中我们还增加了许多其他的逻辑，如果你感兴趣的话可以查看下代码。\n\n## 3、总结\n\n上面我们介绍了项目的一些内容和新版本重构时加入的新功能等。这些新加入的东西也算是这段时间以来学习成果的一个小集合。当然，因为毕竟业余时间有限，代码中可能仍然存在一些不足和设计不良的地方，如果你发现了这些不愉快的问题，可以在 Github 上面为项目提 issue，很乐意与你沟通和学习！\n\n最后，重申一下项目相关的链接：\n\n1. [酷安网下载链接：https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276)\n2. [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)\n3. [Github 项目链接：https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote)\n\n------\n**如果您喜欢我的文章，可以在以下平台关注我：**\n\n- 博客：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n\n更多文章：[Gihub: Android-notes](https://github.com/Shouheng88/Android-notes)\n"
  },
  {
    "path": "其他/计算机视觉与Android.md",
    "content": "# Android 与计算机视觉\n\n不管你是否从事计算机视觉相关的工作，了解这方面的内容总是好的，因为即使你现在的工作与 AI 无关，采用一些开放的 API 仍然有可能让你的应用做得更好。比如，百度开发平台就提供了许多 AI 相关的 API，像当下比较受欢迎的“白描”等应用，其实就是使用了百度的 API。所以，你也可以考虑一下能否借助一些语音和文字识别等功能来赋能自己的应用。\n\n因为我们所做的计算机视觉的东西更多的是对图片进行处理，这就涉及到 OpenCV 和 Tensorflow 在 Android 端的应用，以及相机和 Android 端的其他图片处理逻辑。这不可避免地要用到 JNI 和 NDK 当中的一些内容。因此，在本篇文章中，我们想要讨论的内容主要包括以下几个方面的：\n\n1. **Android 端图片压缩库封装**\n2. **Android 端相机库封装和性能优化**\n3. **JNI 和 NDK 调用，以及 CMake 在 Android 中的应用**\n4. **OpenCV 在 Android 中的集成和应用**\n5. **Tensorflow 在 Android 端的集成和应用**\n\n其实之前的文章中我们也提到过一些今天我们想讨论的内容。所以在这里相关的技术底层的知识能带过的就直接带过。我们会给出相关的技术文章的链接，如果感兴趣的可以到指定的文章下面查看更具体的知识。\n\n## 1、Android 端图片压缩库封装\n\n为什么要做图像压缩呢？因为太大的图片上传速度比较慢，会影响程序的用户体验；而过分的压缩图片会导致程序识别出来的效率比较低。识别的效率每提高 1 个百分点，标注团队可能就要多标注几万张图片。经过测试发现，把图片的短边控制在 1100 左右是最合适的，那么此时我们就需要制定一个自己的压缩策略。\n\n这个在之前的文章中我们已经讨论过，并且对 Android 端 Bitmap 相关的压缩的知识都做了介绍。您可以到下面的文章下面了解下我们是如何对图片压缩库进行封装的，以及 Android 中图片压缩的底层原理：\n\n[开源一个 Android 图片压缩框架](https://juejin.im/post/5c87d01f6fb9a049b7813784)\n\n当然，上面的文章在介绍的这方面的东西的时候，基于的是我们库的第一个版本，那个版本可以满足基本的功能。在后来的版本中，我们又对自己的库做了完善，增加了更多的 feature。这里我们主要介绍下新的框架相关的 API 以及后来我们如何做了兼容性的设计，以在第一个版本的基础之上进行了功能性的拓展。\n\n在实际的使用过程中，我们发现更多的时候你需要对 Bitmap 进行处理而不是 File. 在这个时候，第一个版本的库就应用不上了。想了想，我们希望能够对自己的库进行拓展以支持更多的应用场景。这包括：\n\n1. **在当前线程中直接获取压缩的 Bitmap 而不是通过 Callback 或者 RxJava 的形式传递结果**：因为我们有一部分代码本身就是在 RxJava 中异步执行的，回调或者使用 RxJava 会影响我们程序的逻辑结构。\n2. **直接使用 Bitmap 或者 `byte[]` 作为参数进行压缩而不是先写入到 File 中，然后对文件进行读取和压缩**：这个是存在具体的应用场景，比如当你从相机当中获取数据的时候，实际获取到的是 `byte[]`，在连续拍摄的情况下，不断写入到文件再读取并进行压缩非常影响程序性能。\n3. **支持直接返回 Bitmap 而不是只能返回 File 类型**：有时候我们需要对程序的局部做优化，比如图片处理结果的预览，此时，如果我们返回的是 File 的话，一样会影响我们程序的性能和逻辑结构。\n\n最初，我们希望能够像 Glide 那样支持对自定义的数据结构进行压缩，然后自定义图片获取的逻辑，然而考虑到时间和兼容的问题，直接放弃了这个想法，转而采用更加简单、直接的方式：\n\n1. **对入参这一块，直接使用重载函数接受不同的参数类型**；\n2. **压缩的过程可以直接使用 `get()` 方法把压缩的中间结果返回给调用者**；\n3. **增加 `asBitmap()` 方法，转换输出参数类型为 Bitmap 而不是 File 类型**。\n\n因此，在后来的版本中，你可以像下面这样直接获取到 Bitmap 的结果：\n\n```java\n    Bitmap result = Compress.with(this, bytes)\n            .setQuality(80)\n            .strategy(Strategies.compressor())\n            .setScaleMode(ScaleMode.SCALE_SMALLER) // 指定放缩的边\n            .setMaxHeight(1100f)\n            .setMaxWidth(1100f)\n            .asBitmap()  // 调用这个方法表示期望的返回类型是 Bitmap 而不是 File\n            .asFlowable()\n            .subscribe(b -> {\n                // do somthing\n            })\n```\n\n这里关于 `asBitmap()` 方法的设计可以简单说明一下。\n\n![Compressor 设计图](res/Compressor.png)\n\n> 图片链接：[https://www.processon.com/view/link/5cdfb769e4b00528648784b7](https://www.processon.com/view/link/5cdfb769e4b00528648784b7])\n\n在第一个版本中，我们采用的上图中第一个图的设计。在这里两种压缩策略均继承自 AbstractStrategy，其实上述的 `strategy()` 方法，你可以把它理解成**拐了一个弯**，也就是它返回的具体的策略，你下面能调用的方法都局限在具体的策略中。\n\n在后来的设计中要在 `asBitmap()` 方法处返回一个具体的构建者继续按照返回 Bitmap 的逻辑进行处理。此时我们直接返回的是第二张图中的 BitmapBuilder 对象，而 Abstrategy 则依然按照返回 File 类型的逻辑走。这样我们可以轻易地在原来的基础上，通过**拐一个弯**的形式把后续的构建逻辑转移到了 BitmapBuilder 对象上面。同时，为了达到代码复用的目的，我们引入了泛型的 `RequestBuilder<T>`。这样 AbstractStrategy 和 BitmapBuilder 只需要实现它，并指定各自的资源类型即可。又因为按照之前的逻辑，我们一直在构建的都是 AbstractStrategy 对象，因此，我们只需要把 AbstractStrategy 作为参数传入到具体的 RequestBuilder 里面就可以从它上面直接获取 Bitmap 了。（Bitmap 是在之间串联的“通用货币”。）这样我们既复用了大量的代码又在兼容原始版本的基础上进行了功能的拓展，妙极！\n\n## 2、Android 相机库封装和性能优化\n\n对于一个 ToC 的应用来说，用户体验直观重要。按照我们的业务场景，如果使用拍照识别的效率比人工操作的效率还要低的话，那么人工智能似乎就没有存在的必要了。毕竟我们的目标是提升别人的工作效率，所以在相机这块就必须做到快速响应。\n\n在项目的初期我们使用的是 Google 开源的 [CameraView](https://github.com/google/cameraview). 然而在实际的应用过程中，我们逐渐发现这个库存在大量不好的设计，影响了我们程序的应用性能：\n\n![相机启动 TraceView 分析](https://user-gold-cdn.xitu.io/2019/4/23/16a4ac1ac0d8a697?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n> 配图是我们使用 TraceView 对程序执行过程进行的性能分析\n\n1. **没必要的数据结构构建，影响相机启动速率**：首先，当从相机的属性当中读取相机支持的尺寸的时候它会使用这些参数构建一个尺寸的宽高比到尺寸列表的哈希表结构。然后具体的运算的时候从这个哈希表中读取尺寸再进行计算。这样设计很不好！因为当需要计算尺寸的时候遍历一遍尺寸列表可能并不会占用太多的时间，并且构建的哈希表结构使用并不频繁，而在相机启动阶段进行不必要的计算反而影响了相机的启动速率。\n\n2. **打开相机的操作在主线程当中执行，影响界面响应速率**：前提是界面能够快速响应用户，即使打开的是一个黑色的等待界面也比按下没有响应更容易接受。通过 TraceView 我们发现相机 `open()` 的过程大概占用了相机启动速率的 25%。因此把这个方法的调用放在主线程中是不太合适的。\n\n3. **相机不支持视频拍摄和预览**：这个库是不支持相机的视频拍摄和预览的。毕竟作为计算机视觉的一部分的实时处理也是很重要的一部分。就算当前的项目中没有这方面的功能，我们也应该考虑对这方面的功能进行支持。（这方面的内容基本就是 OpenGL + Camera）\n\n于是乎，我们自己开发的一款相机库就诞生了。当然当初开发的一个原因也是希望能够支持 OpenGL。只是时间太有限，暂时还没有太多时间去关注这些问题:\n\n![相机库设计 UML 建模图](https://user-gold-cdn.xitu.io/2019/4/23/16a4aae65580a62c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n> 图片链接：[https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049](https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049)\n\n关于这个库，我只是把它所有的逻辑实现了一遍，并且在我的手机上面调试没有什么问题。如果具体应用到生存环境当中还需要更多的测试和验证。关于 Android 相机开发的知识，主要覆盖 Camera1 和 Camera2 两块内容。一个方法的实现逻辑看懂了，其他方法的实现与之类似，具体的内容可以参考项目的源码。因为本人当前时间和精力有限，所以暂时无法详细讲解相机 API 的使用。\n\n我们可以简单概括一下这份设计图当中的一些内容：\n\n1. **三种主要设计模式**：\n\n    1. **门面模式**：考虑到兼容 Camera1 和 Camera2 的问题，我们需要对外提供统一的 API 调用，所以，我们考虑了使用门面模式来做一个统一的封装。这里定义了 Camera1Manager 和 Camera2Manager 两个实现，分别对应于两种不同的相机 API. 它们统一继承自 CameraManager 这个门面接口。这种设计模式的好处是对外是统一的，这样结合具体的工厂+策略模式，我们可以让用户在 Camera1 和 Camera2 之间自由选择。\n\n    2. **策略模式+工厂模式**：因为考虑到各种不同应用场景的兼容，我们希望能够用户提供最大的自由度。所以，我们采用了策略的方式，对外提供了接口给用户来计算最终想要得到的相机尺寸等参数。所以这里我们定义了一个名为 `ConfigurationProvider` 的类，它是一个单例的类，除了负责获取相机参数的计算策略，同时肩负着内存缓存的责任。这样对于很多参数的计算，包括预览尺寸、照片尺寸、视频尺寸等可以让用户自由来指定具体的大小。\n\n2. **三个主要的优化点**：\n\n    1. **内存缓存优化**：实际上作为相机属性的相机所支持尺寸等信息是不变的，使用内存缓存缓存这些数据之后下次就无需再次获取并进行处理，这样可以在下次相机启动的时候显著提升程序响应的速率。\n\n    2. **延迟初始化，不使用不计算**：为了提升程序的响应速率，我们甚至对数字的计算也进行了优化，当然这个优化点可能效果没有那么明显，但是如果你愿意精益求精的话，这也可以当作一个优化点。目前程序里面还是使用了浮点数进行计算，在早期对于作为哈希表映射的键的字段，我们甚至直接使用移位预算。当然这种优化的效果还要取决于整体的数据量，并且数据量越大的时候优化效果越明显。\n\n    3. **异步线程优化**：在早期的版本中，我们使用的是私有锁进行线程优化。因为要把线程的 `open()` 和设置参数放在两个线程当中进行，因此不可避免地要遇到线程安全问题。而所谓的私有锁其实就类似于 `Collections.syncXXX()` 所返回的同步容器。其实就是对容器的每个方法进行加锁。这样虽然可以解决问题，但是程序的结构不好看。所以，后来的版本中，我们直接使用 HandlerThread 来进行异步的调用。所谓的 HandlerThread，顾名思义，就是 Handler 和 Thread 的结合。\n\n（更多的内容可以参考：[Android 相机库开发实践](https://juejin.im/post/5cbf2667f265da037875980a)）\n\n## 3、JNI 和 NDK 调用，以及 CMake 在 Android 中的应用 \n\n在之前我们想在在 Java 或者 Android 中调用 C++ 代码是比较复杂的。这需要进行动态或者静态的注册。对于静态注册的方式，你需要一步一步地进行编译；对于动态注册的方式，你需要把方法一个个地在 native 层进行注册。不过后来有了 CMake 之后一切都变得简单了。当然对于 CMake，如果做过 native 的同学肯定不会陌生。对于一般应用层开发的同学，其实也可以了解下它。因为，有了它之后你可以很容易地把你的一部分实现逻辑放在 native 层里，而 native 层相对于 Java 层比较安全，而且借助 C++ 和 NDK 你可以做出更多有趣的东西。\n\n要在 Android 端使用 CMake 而是很简单的，你需要首先在 AS 里面安装下相关的 SDK 工具:\n\n![CMake 开发环境搭建](https://user-gold-cdn.xitu.io/2019/3/2/1693c6828071d2a6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n然后，你需要在 Gradle 里面做简单的配置：\n\n![CMake gradle 配置](https://user-gold-cdn.xitu.io/2019/3/2/1693c83b5585c77b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n当然，虽然我们这样将其来容易，但是进行配置的时候可能需要很多的相关的专业知识。\n\n这里面会配置到一个 CMake path，它就是指向的 CMake 的配置文件 `CMakeLists.txt`。通常我们程序中要用到的一些三方的 native 库就需要在这个地方进行配置。比如下面的就是之前项目里面的 CMake 的配置。这里面配置了一些 OpenCV 的库以及我们自己的代码所在的位置，并且引用了 NDK 里面的一些相关的库：\n\n```cmake\n# 设置要求的 CMake 的最低版本\ncmake_minimum_required(VERSION 3.4.1)\n\n# 指定头文件的目录\ninclude_directories(opencv/jni/include\n        src/main/cpp/include\n        ../../common)\n\nadd_library(opencv_calib3d STATIC IMPORTED)\nadd_library(opencv_core STATIC IMPORTED)\n# ....\n\n#if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)\n#    add_library(tbb STATIC IMPORTED)\n#endif()\n\nset_target_properties(opencv_calib3d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_calib3d.a)\nset_target_properties(opencv_core PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_core.a)\nset_target_properties(opencv_features2d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_features2d.a)\n# ....\n\nadd_library(everlens\n        SHARED\n        src/main/cpp/img_proc.cpp\n        src/main/cpp/img_cropper.cpp\n        src/main/cpp/android_utils.cpp\n        ../../common/EdgeFinder.cpp\n        ../../common/ImageFilter.cpp)\n\nfind_library(log-lib\n        log)\n\nfind_library(jnigraphics-lib\n        jnigraphics)\n\nif(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a)\n    target_link_libraries(\n            my_library\n            opencv_stitching\n            opencv_features2d\n            # ....\n\n            ${log-lib}\n            ${jnigraphics-lib})\nelse()\n    target_link_libraries(\n            my_library\n            opencv_stitching\n            opencv_features2d\n            # ....\n\n            ${log-lib}\n            ${jnigraphics-lib})\nendif()\n```\n\n对于 CMake 的一些指令，之前也进行过一些总结，并且对指定了官方文档的地址。如果想要了解的话，可以到文章下面了解更多的内容：\n\n[常用的 CMake 指令总结](https://blog.csdn.net/github_35186068/article/details/88639757)\n\n使用 CMake 的好处主要是 AS 支持得比较好：\n\n1. 可以根据 native 层代码到 Java 层代码之间的关系，鼠标左键 + Ctrl 即可直接完成 native 层方法和 Java 层方法之间的跳转；\n\n2. 无需进行繁琐的动态注册和静态注册，只需要在 CMake 和 Gradle 当中进行配置，可以把注意力更多地放在自己的代码的逻辑实现上。\n\n当然，就算使用了 CMake 有时候还是需要了解一些 JNI 中动态注册的内容，因为有时候当你在 native 层中从 Java 层传入的对象上面获取信息的时候还是需要进行动态注册。比如，\n\n```C++\n#include <jni.h>\n#include <string>\n#include <android_utils.h>\n\n// 定义一个结构体及实例 gPointInfo\nstatic struct {\n    jclass jClassPoint;\n    jmethodID jMethodInit;\n    jfieldID jFieldIDX;\n    jfieldID jFieldIDY;\n} gPointInfo;\n\n// 初始化 Class 信息，注意下映射关系是如何表达的，其实就类似于反编译之后的注释\nstatic void initClassInfo(JNIEnv *env) {\n    gPointInfo.jClassPoint = reinterpret_cast<jclass>(env -> NewGlobalRef(env -> FindClass(\"android/graphics/Point\")));\n    gPointInfo.jMethodInit = env -> GetMethodID(gPointInfo.jClassPoint, \"<init>\", \"(II)V\");\n    gPointInfo.jFieldIDX = env -> GetFieldID(gPointInfo.jClassPoint, \"x\", \"I\");\n    gPointInfo.jFieldIDY = env -> GetFieldID(gPointInfo.jClassPoint, \"y\", \"I\");\n}\n\n// 动态注册，在这里初始化\nextern \"C\"\nJNIEXPORT jint JNICALL\nJNI_OnLoad(JavaVM* vm, void* reserved) {\n    JNIEnv *env = NULL;\n    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {\n        return JNI_FALSE;\n    }\n    initClassInfo(env);\n    return JNI_VERSION_1_4;\n}\n```\n\n（更多的内容可以参考：[在 Android 中使用 JNI 的总结](https://juejin.im/post/5c79f5d0518825347a56275f)）\n\n## 4、OpenCV 在 Android 中的集成和应用：图片裁剪以及透视变换\n\n### 4.1 关于 OpenCV 的集成\n\n当然，不引用 OpenCV 的 C++ 库，直接使用别人封装好的 Java 库也是可以的，这取决于具体的应用场景。比如，如果你不需要实现特别复杂的功能，只需要简单的图像处理即可，那么别人包装过的 Java 库已经可以完全满足你的需求。但如果你像我们一样，本身需要包装和编译来自算法同学的 C++ 算法，甚至还需要使用 OpenCV 的拓展库，那么使用 Java 包装后的库可能无法满足你的需求。\n\n下面是 OpenCV 及其拓展库的 Github 地址：\n\n- [OpenCV](https://github.com/opencv/opencv)\n- [OpenCV-contrib](https://github.com/opencv/opencv_contrib)\n\n有了这些库你还是无法直接将其应用到程序当中的。因为上述项目得到的是 OpenCV 的源码，主要是源代码以及一些头文件，还需要对它们进行编译然后再应用到自己的项目当中。\n\n[Build OpenCV 3.3 Android SDK on Mac OSX](https://chaoyang.nz/post/build-opencv-android-sdk/)\n\n当然也有一些已经编译完成的 OpenCV 及其拓展库，我们可以在 CMake 中配置之后直接将其引用到我们的项目中：\n\n[opencv3-android-sdk-with-contrib](https://github.com/chaoyangnz/opencv3-android-sdk-with-contrib)\n\n所以最终项目的结构如下：\n\n![OpenCV 集成之后的项目结构](res/QQ截图20190718000949.png)\n\n左边圈出的部分是 OpenCV 及 CMake 的一些配置，右边是封装之后的 Java 方法。\n\n### 4.2 关于 OpenCV 的应用\n\nOpenCV 可以用来处理做很多图片处理的工作，很多工作是使用 Android 原生的 Bitmap 无法完成的。比如，图片不规则裁剪之后的透视变换、灰度化处理等。其实，不论你在 native 层如何对图片进行处理，在 Android 当中，对 native 层的输入和 native 层的输出都是 Bitmap. 而 `OpenCV::Mat` 就像是 native 层图片处理的通用货币。所以，一个完整的图片处理的流程大致是：\n\n1. Step1: Java 层的 Bitmap 转换成 native 层的 Mat;\n2. Step2: 使用 Mat 进行图片处理；\n3. Step3: 将 native 层的 Mat 转换成 Java 层的 Bitmap 并返回。\n\n将 Java 层的 Bitmap 转换成 native 层的 Mat 你可以使用下面的方法：\n\n```C++\n#include <jni.h>\n#include <android/bitmap.h>\n#include \"android_utils.h\"\n\nvoid bitmap_to_mat(JNIEnv *env, jobject &srcBitmap, Mat &srcMat) {\n    void *srcPixels = 0;\n    AndroidBitmapInfo srcBitmapInfo;\n    try {\n        // 调用 AndroidBitmap 中的方法获取 bitmap 信息\n        AndroidBitmap_getInfo(env, srcBitmap, &srcBitmapInfo);\n        AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);\n        uint32_t srcHeight = srcBitmapInfo.height;\n        uint32_t srcWidth = srcBitmapInfo.width;\n        srcMat.create(srcHeight, srcWidth, CV_8UC4);\n        // 根据 bitmap 的格式构建不同通道的 Mat\n        if (srcBitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {\n            Mat tmp(srcHeight, srcWidth, CV_8UC4, srcPixels);\n            tmp.copyTo(srcMat);\n            cvtColor(tmp, srcMat, COLOR_RGBA2RGB);\n        } else {\n            Mat tmp = Mat(srcHeight, srcWidth, CV_8UC2, srcPixels);\n            cvtColor(tmp, srcMat, COLOR_BGR5652RGBA);\n        }\n        AndroidBitmap_unlockPixels(env, srcBitmap);\n        return;\n    } catch (cv::Exception &e) {\n        AndroidBitmap_unlockPixels(env, srcBitmap);\n        // 构建一个 Java 层的异常并将其抛出\n        jclass je = env->FindClass(\"java/lang/Exception\");\n        env -> ThrowNew(je, e.what());\n        return;\n    } catch (...) {\n        AndroidBitmap_unlockPixels(env, srcBitmap);\n        jclass je = env->FindClass(\"java/lang/Exception\");\n        env -> ThrowNew(je, \"unknown\");\n        return;\n    }\n}\n```\n\n这里主要是先从 Bitmap 中获取图片具体的信息，这里调用了 NDK 里面的图像相关的一些方法。然后利用得到的图片尺寸信息和颜色信息构建 OpenCV 里面的 Mat. Mat 就类似于 MATLAB 里面的矩阵，它包含了图像的像素等信息，并且也提供了类似于 `eye()`, `zeros()` 等类似的方法用来构建特殊的矩阵。\n\n将 Bitmap 转换成 Mat 之后就是如何使用它们了。下面是一份用来对图片进行裁剪和透视变换的算法：\n\n```C++\n// 将 Java 层的顶点转换成 native 层的 Point 对象\nstatic std::vector<Point> pointsToNative(JNIEnv *env, jobjectArray points_) {\n    int arrayLength = env->GetArrayLength(points_);\n    std::vector<Point> result;\n    for(int i = 0; i < arrayLength; i++) {\n        jobject point_ = env -> GetObjectArrayElement(points_, i);\n        int pX = env -> GetIntField(point_, gPointInfo.jFieldIDX);\n        int pY = env -> GetIntField(point_, gPointInfo.jFieldIDY);\n        result.push_back(Point(pX, pY));\n    }\n    return result;\n}\n\n// 裁剪并且透视变化\nextern \"C\" JNIEXPORT void JNICALL\nJava_xxxx_MyCropper_nativeCrop(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray points_, jobject outBitmap) {\n    std::vector<Point> points = pointsToNative(env, points_);\n    if (points.size() != 4) {\n        return;\n    }\n    // 取出四个顶点\n    Point leftTop = points[0], rightTop = points[1], rightBottom = points[2], leftBottom = points[3];\n\n    // 获取源图和结果图对应的 Mat\n    Mat srcBitmapMat, dstBitmapMat;\n    bitmap_to_mat(env, srcBitmap, srcBitmapMat);\n    AndroidBitmapInfo outBitmapInfo;\n    AndroidBitmap_getInfo(env, outBitmap, &outBitmapInfo);\n    int newHeight = outBitmapInfo.height, newWidth = outBitmapInfo.width;\n    dstBitmapMat = Mat::zeros(newHeight, newWidth, srcBitmapMat.type());\n\n    // 将图片的顶点放进集合当中，用来调用透视的方法\n    std::vector<Point2f> srcTriangle, dstTriangle;\n\n    srcTriangle.push_back(Point2f(leftTop.x, leftTop.y));\n    srcTriangle.push_back(Point2f(rightTop.x, rightTop.y));\n    srcTriangle.push_back(Point2f(leftBottom.x, leftBottom.y));\n    srcTriangle.push_back(Point2f(rightBottom.x, rightBottom.y));\n\n    dstTriangle.push_back(Point2f(0, 0));\n    dstTriangle.push_back(Point2f(newWidth, 0));\n    dstTriangle.push_back(Point2f(0, newHeight));\n    dstTriangle.push_back(Point2f(newWidth, newHeight));\n\n    // 获取一个映射的转换矩阵\n    Mat transform = getPerspectiveTransform(srcTriangle, dstTriangle);\n    warpPerspective(srcBitmapMat, dstBitmapMat, transform, dstBitmapMat.size());\n\n    // 将 Mat 转换成 Bitmap 输出到 Java 层\n    mat_to_bitmap(env, dstBitmapMat, outBitmap);\n}\n```\n\n算法最终的输出结果：\n\n<div align=\"middle\"><img src=\"res/Screenshot_20190718-003619.jpg\" alt=\"透视变化和图像切割\" height=\"300\" /></div>\n\n## 5、Tensorflow 在 Android 端的集成和应用：图片边缘检测\n\n在之前对图片的边缘进行检测的时候，因为发现 OpenCV 算法效果不太理性，所以后来选择使用 TensorFlow 对图片进行边缘检测。这就涉及到在 Android 端集成 Tensorflow Lite。前段时间也看到爱奇艺的 SmartVR 的介绍。借助一些官方的资料，在 Android 端使用 TF 并不难。在 Tensorflow 的开源仓库中已经有一些 Sample 可供参考：\n\n- [Tensorflow github repository](https://github.com/tensorflow/tensorflow)\n- [Tensorflow lite offical](https://www.tensorflow.org/lite)\n\n做边缘检测当然有些大材小用的味道，但是对于我们 Android 开发者来说，借这个机会了解如何在 Android 端集成一些 Tensorflow 也可以拓展一下。毕竟这种东西属于当下比较热门的东西，说不定哪天心血来潮自己训练一个模型呢 :)\n\n在 Android 端引入 Tensorflow 并不复杂，只需要添加相关的仓库以及依赖：\n\n```groovy\nallprojects {\n    repositories {\n        jcenter()\n        maven { url 'https://google.bintray.com/tensorflow' }\n    }\n}\n\ndependencies {\n    // ...\n    // tensorflow\n    api 'org.tensorflow:tensorflow-lite:0.0.0-nightly'\n}\n```\n\n困难的地方在于如何对训练的模型的输入和输出进行处理。因为所谓的模型，你可以将其理解成锻炼出来的一个函数，你只需要按照要求的输入，它就可以按照固定的格式给你返回一个输出。所以，具体的输入和输出是什么样的还要取决于锻炼模型的同学。\n\n在我们之前开发的时候，最初是训练模型的同学使用 Python 代码调用的模型。使用 Python 虽然代码简洁，但是对于客户端开发简直就是噩梦。因为像 [NumPy](https://numpy.org/) 和 [Pillow](https://pillow.readthedocs.io/en/stable/) 这种函数库，一行代码的任务，你可能要“翻译”很久。后来，我们使用的是 `C++ + OpenCV` 的形式。对于 iOS 开发，因为他们可以使用 Object-C 与 C++ 混编，所以比较轻松。对于 Android 开发则需要做一些处理。\n\n下面是加载模型以及在调用 Tensorflow 之前在 Java 层所做的一些处理：\n\n```java\npublic class TFManager {\n\n    private static TFManager instance;\n\n    private static final float IMAGE_MEAN = 128.0f;\n    private static final float IMAGE_STD = 128.0f;\n\n    private Interpreter interpreter;\n    private int[] intValues;\n    private ByteBuffer imgData;\n    private int inputSize = 256;\n\n    public static TFManager create(Context context) { // DCL\n        if (instance == null) {\n            synchronized (TFManager.class) {\n                if (instance == null) {\n                    instance = new TFManager(context);\n                }\n            }\n        }\n        return instance;\n    }\n\n    // 从 Assets 中加载模型，用来初始化 TF\n    private static MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename) throws IOException {\n        AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename);\n        FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());\n        FileChannel fileChannel = inputStream.getChannel();\n        long startOffset = fileDescriptor.getStartOffset();\n        long declaredLength = fileDescriptor.getDeclaredLength();\n        return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);\n    }\n\n    // 初始化 TF\n    private TFManager(Context context) {\n        try {\n            interpreter = new Interpreter(loadModelFile(context.getAssets(), \"Model.tflite\"));\n            interpreter.setNumThreads(1);\n            interpreter.resizeInput(0, new int[]{1, 256, 256, 3});\n            intValues = new int[inputSize * inputSize];\n            imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);\n            imgData.order(ByteOrder.nativeOrder());\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    // 边缘识别\n    public EdgePoint[] recognize(Bitmap bitmap) {\n        long timeStart = System.currentTimeMillis();\n        imgData.rewind();\n        Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true);\n        scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight());\n        for (int i = 0; i < inputSize; ++i) {\n            for (int j = 0; j < inputSize; ++j) {\n                int pixelValue = intValues[i * inputSize + j];\n                // 取 RBG 做归一化处理，结果在 -1 到 1 之间\n                imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // R\n                imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // G\n                imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // B\n            }\n        }\n        LogUtils.d(\"----------TFManager prepare imgData cost : \" + (System.currentTimeMillis() - timeStart));\n\n        timeStart = System.currentTimeMillis();\n        Map<Integer, Object> outputMap = new HashMap<>();\n        outputMap.put(0, new float[1][256][256][5]);\n\n        Object[] inputArray = {imgData};\n        // 调用 TF 进行识别\n        interpreter.runForMultipleInputsOutputs(inputArray, outputMap);\n\n        // 对识别的结果进行处理，主要是对图片的像素进行处理\n        float[][][][] arr = (float[][][][]) outputMap.get(0);\n        int[][] colors = new int[5][256 * 256];\n        for (int i=0; i<5; i++) {\n            for (int j=0; j<256; j++) {\n                for (int k=0; k<256; k++) {\n                    colors[i][j*256 + k] = (int) (arr[0][j][k][i] * 256);\n                }\n            }\n        }\n        LogUtils.d(\"----------TFManager handle TF result cost : \" + (System.currentTimeMillis() - timeStart));\n\n        timeStart = System.currentTimeMillis();\n        // 将得到的图片像素按照固定的格式交给 native 层继续进行边缘识别\n        EdgePoint[] points = ImgProc.findEdges(bitmap, colors);\n        LogUtils.d(\"----------TFManager\" + Arrays.toString(points));\n        LogUtils.d(\"----------TFManager find edges cost : \" + (System.currentTimeMillis() - timeStart));\n        return points;\n    }\n}\n```\n\n这里程序的主要执行流程是：\n\n1. 从 Assets 的模型文件中获取打开输入流，然后从输入流中打开一个管道，这里用到了 NIO 中的一些类。然后，从管道中获取一个字节缓存区。文件读写的时候管道直接与缓冲区进行交互。除了作为一个缓存区，这个缓存区还具有内存映射的功能。类似于 mmap 吧，主要是为了提升文件读写的效率。\n\n2. 初始化并配置 Tensorflow，上面的一些参数用来设置线程数量等信息，比较简单。后面等一些参数主要用来按照模型等要求对 TF 进行调整。比如，我们使用模型来判断图片的顶点的时候使用的是只包含 RGB 三个纬度的 256 * 256 的图片。所以，这里使用了下面几行代码来进行设置：\n\n```java\n// inputSize = 256;\ninterpreter.resizeInput(0, new int[]{1, 256, 256, 3});\nintValues = new int[inputSize * inputSize];\n// 256 * 256 的图片，3 个纬度，四张图\nimgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3);\n```\n\n3. 对要识别对图片进行处理。这里需要先对图片进行放缩，将其控制到 256 * 256 的大小。然后，使用 Bitmap 的方法获取图片的像素。下面的几行代码是对图片对 RBG 三种色彩提取，并分别对其进行归一化处理。处理之后对结果统一写入到 imgData 当中，作为模型对输入。\n\n4. 按照模型对输出对文件的格式构建一个 Java 对象作为模型的输出参数。调用模型的方法进行识别。\n\n5. 对模型对输出结果进行处理。根据我们上述定义对模型输出 `new float[1][256][256][5]`，这里实际对含义是 256 * 256 的 5 张图片。因为模型输出的数据并不是原始的像素信息，所以需要乘以 256 来得到真正的图片的像素。最后就是使用这些像素以及 Bitmap 的方法来得到最终的 Bitmap.\n\n上面调用完了模型但是整个流程还没有结束。因为只是调用模型得到了五张识别之后的图片。这五张图片就是只留下了图片边缘的边框，所以想要得到图片的顶点还需要继续对这五张图进行处理。这部分需要一些算法，虽然在 Java 层去判断也是可以的，但是在 native 层，借助 OpenCV 的一些库可以使整个过程更加简单。因此，这里又要涉及一个 JNI 调用：\n\n```C++\nextern \"C\"\nJNIEXPORT void JNICALL\nJava_com_xxx_ImgProc_nativeFindEdges(JNIEnv *env, jclass type, jintArray mask1_, jintArray mask2_,\n                                                        jintArray mask3_, jintArray mask4_, jintArray mask5_,\n                                                        jobject origin, jobjectArray points) {\n    // 从 Java 中传入的数组元素\n    jint *mask1 = env->GetIntArrayElements(mask1_, NULL);\n    jint *mask2 = env->GetIntArrayElements(mask2_, NULL);\n    jint *mask3 = env->GetIntArrayElements(mask3_, NULL);\n    jint *mask4 = env->GetIntArrayElements(mask4_, NULL);\n    jint *mask5 = env->GetIntArrayElements(mask5_, NULL);\n\n    // 原图转换成 Native 层的 mat\n    Mat originMat;\n    bitmap_to_mat(env, origin, originMat);\n\n    // 构建一个集合\n    std::vector<jint*> jints;\n    jints.push_back(mask1);\n    jints.push_back(mask2);\n    jints.push_back(mask3);\n    jints.push_back(mask4);\n    jints.push_back(mask5);\n\n    // 从像素点中得到对应的 Mat 并将其放到一个集合当中\n    std::vector<cv::Mat> masks;\n    for (int k = 0; k < 5; ++k) {\n        Mat mask(256, 256, CV_8UC1);\n        for (int i = 0; i < 256; ++i) {\n            for (int j = 0; j < 256; ++j) {\n                mask.at<uint8_t>(i, j) = (char)(*(jints[k] + i * 256 + j));\n            }\n        }\n        masks.push_back(mask);\n    }\n\n    try {\n        // 调用算法进行边缘检测\n        EdgeFinder finder = ImageEngine::EdgeFinder(\n                originMat, masks[0], masks[1], masks[2], masks[3], masks[4]);\n        vector<cv::Point2d> retPoints = finder.FindBorderCrossPoint();\n        // 将得到的“点”转换成 Java 层的对象\n        jclass class_point = env->FindClass(\"com/xxx/EdgePoint\");\n        jmethodID method_point = env->GetMethodID(class_point, \"<init>\", \"(FF)V\");\n        // 将顶点组成一个 Java 数组返回\n        for (int i = 0; i < 4; ++i) {\n            jobject point = env->NewObject(class_point, method_point, retPoints[i].x, retPoints[i].y);\n            env->SetObjectArrayElement(points, i, point);\n        }\n    }  catch (cv::Exception &e) {\n        jclass je = env->FindClass(\"java/lang/Exception\");\n        env -> ThrowNew(je, e.what());\n        return;\n    } catch (...) {\n        jclass je = env->FindClass(\"java/lang/Exception\");\n        env -> ThrowNew(je, \"unknown\");\n        return;\n    }\n\n    // 释放资源\n    env->ReleaseIntArrayElements(mask1_, mask1, 0);\n    env->ReleaseIntArrayElements(mask2_, mask2, 0);\n    env->ReleaseIntArrayElements(mask3_, mask3, 0);\n    env->ReleaseIntArrayElements(mask4_, mask4, 0);\n    env->ReleaseIntArrayElements(mask5_, mask5, 0);\n}\n```\n\n这里的主要逻辑是把之前得到的像素以及原始图片的 Bitmap 统一传入到 native 层，然后这些像素中得到 OpenCV 对应的 Mat，再一起作为算法的参数调用算法来得到顶点信息。最后把得到的顶点信息映射成 Java 层的类，并将其放在数组中返回即可。\n\n从上面的流程中也可以看出，整个调用流程实际上进行了多次的 JNI 调用：\n\n1. 调用 Bitmap 的方法本身就是一次 JNI 调用，调用了 Android 底层的 Skia 的库来实现图片处理；\n2. TF-Lite 本身就是对 native 层方法的一个封装，调用其方法也涉及到 JNI 调用；\n3. 最后是对模型识别的结果进行处理，这也涉及 JNI 调用。\n\nJNI 调用的时候需要进行额外的转换操作，需要在函数开始的时候把 Java 层的对象转换成 native 层的对象，在算法调用完毕之后再将 native 层的对象转换成 Java 层的对象。这是我们以后可以优化的一个地方。\n\n## 总结\n\n以上就是计算机视觉在 Android 中的应用，主要涉及 JNI 的一些内容，以及 OpenCV 和 Tensorflow 的一些应用。再这之前介绍了图片压缩和相机库的一些封装，如果你的程序中需要一些图片处理的功能的话，我想这些东西肯定是对你有用的 :D\n\n-------\n\n关注作者获取更多知识：\n\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n- 博客：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n\n更多知识请参考 [Github, Android-notes](https://github.com/Shouheng88/Android-notes)。\n"
  },
  {
    "path": "响应式编程/RxJava系列-4：RxJava源码分析.md",
    "content": "# RxJava 系列-4：RxJava 源码分析\n\n在之前的文章中我们介绍了 RxJava 2 的常用的 API 的方法总结、背压的概念以及 RxJava 2 在项目中的实际应用。在本节中，我们将要对 RxJava 2 的源码进行分析。下面是之前文章的一些链接，如果对 RxJava 2 的使用比较感兴趣，你可以通过下面的文章进行学习：\n\n- [RxJava2 系列-1：一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd)\n- [RxJava2 系列-2：背压和 Flowable](https://juejin.im/post/5b759b9cf265da283719d187)\n- [RxJava2 系列-3：使用 Subject](https://juejin.im/post/5b801dfa51882542cb409905)\n\n下面我们就从 RxJava 2 的一个简单的示例来分析下 RxJava 2 是的主流程、设计模式以及 RxJava 2 是如何实现线程切换的。\n\n## 1、RxJava 的主流程源码分析\n\n下面是 RxJava 的一个非常典型的使用示例，在该示例中，我们在 IO 线程中执行业务逻辑，在主线程中对执行的结果进行后续的处理。\n\n```java\n    Disposable disposable = Observable.create(new ObservableOnSubscribe<Object>() {\n        @Override\n        public void subscribe(ObservableEmitter<Object> emitter) throws Exception {\n            // 在这里执行业务逻辑\n            emitter.onNext(new Object());\n        }\n    }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer<Object>() {\n        @Override\n        public void accept(Object o) throws Exception {\n            // 在主线程中进行后续的处理\n        }\n    });\n    disposable.dispose();\n```\n\n我们将这段程序分成四个阶段来进行分析：1). 调用 `create()` 方法的执行过程；2). 调用 `subscribeOn(Schedulers.io())` 和 `observeOn(AndroidSchedulers.mainThread())` 实现线程切换的过程；3). 使用 `subscribe()` 进行订阅的工程；4). 调用 `dispose()` 方法取消订阅的过程。\n\n下面先来看第一个阶段的执行过程。\n\n### 1.1 create() 和 subscribe() 方法的执行过程\n\n下面是调用了 `create()` 方法之后的执行过程，在下面的代码中，我们省略了 null 的检测相关的逻辑。在当前的小节中，我们假设没有指定线程切换相关的逻辑。也就是调用了 `create()` 之后，紧接着调用了 `subscribe()` 方法。\n\n对于 RxJavaPlugins 的静态方法，比如 `onAssembly()` 等，暂时我们先不考虑它的用途。你可以将其看作直接将传入的参数的值返回。比如下面的 `create()` 方法将返回 `ObservableCreate` 的实例。\n\n```java\n    public static <T> Observable<T> create(ObservableOnSubscribe<T> source) {\n        // 看作直接返回了 new ObservableCreate<T>(source) 即可\n        return RxJavaPlugins.onAssembly(new ObservableCreate<T>(source));\n    }\n\n    public final class ObservableCreate<T> extends Observable<T> {\n        final ObservableOnSubscribe<T> source;\n\n        public ObservableCreate(ObservableOnSubscribe<T> source) {\n            this.source = source;\n        }\n\n        @Override\n        protected void subscribeActual(Observer<? super T> observer) {\n            // 对传入的观察者进行包装\n            CreateEmitter<T> parent = new CreateEmitter<T>(observer);\n            // 调用观察者的订阅回调方法\n            observer.onSubscribe(parent);\n            try {\n                // 真正执行订阅的地方\n                source.subscribe(parent);\n            } catch (Throwable ex) {\n                Exceptions.throwIfFatal(ex);\n                parent.onError(ex);\n            }\n        }\n\n        static final class CreateEmitter<T> extends AtomicReference<Disposable> \n            implements ObservableEmitter<T>, Disposable {\n            final Observer<? super T> observer;\n\n            CreateEmitter(Observer<? super T> observer) {\n                this.observer = observer;\n            }\n\n            @Override\n            public void onNext(T t) {\n                if (t == null) {\n                    onError(new NullPointerException(\"onNext ...\"));\n                    return;\n                }\n                if (!isDisposed()) {\n                    // 调用传入的观察者的 onNext() 方法\n                    observer.onNext(t);\n                }\n            }\n        \n            @Override\n            public void dispose() {\n                // 取消订阅\n                DisposableHelper.dispose(this);\n            }\n            // ...\n        }\n        // ...\n    }\n```\n\n上面是第一个阶段的执行过程。这里我们省去了一些代码，只保留了比较具有代表性的一些方法。也许你现在还对这部分代码看得云里雾里，没关系，看了下面的内容你会慢慢理解。\n\n接下来我们看下当调用了 `subscribe()` 方法之后的处理。\n\n```java\n    public final Disposable subscribe(Consumer<? super T> onNext, Consumer<? super Throwable> onError,\n            Action onComplete, Consumer<? super Disposable> onSubscribe) {\n        // 将三种类型的观察者回调统一包装到 LambdaObserver 方法中\n        LambdaObserver<T> ls = new LambdaObserver<T>(onNext, onError, onComplete, onSubscribe);\n        subscribe(ls);\n        return ls;\n    }\n\n    public final void subscribe(Observer<? super T> observer) {\n        try {\n            // 看作直接返回 observer 即可\n            observer = RxJavaPlugins.onSubscribe(this, observer);\n            // 调用了 subscribeActual() 方法\n            subscribeActual(observer);\n        } catch (NullPointerException e) {\n            throw e;\n        } catch (Throwable e) {\n            Exceptions.throwIfFatal(e);\n            RxJavaPlugins.onError(e);\n            throw new NullPointerException(\"Actually not, but can't throw other exceptions due to RS\");\n        }\n    }\n```\n上面的这些方法都定义在 Observable 中，区别只在于调用的对象。所以，为了更清晰地分析这个过程，我们使用大写字母来进行分析：\n\n首先，整体的执行过程是，\n\n```java\nD = Observalbe.create(S).subscribe(X,Y,Z);\n```\n\n它可以被拆解成下面的两个步骤来分析（下面是一份伪代码，只是按照时间的调用顺序来排序的）：\n\n```java\nA = Observable.create(S);\nD = A.subscribe(X,Y,Z);\n```\n\n然后，调用 A 的 `subscribe()` 方法的时候，实际上会调用到 Observable 的 `subscribe()` 方法（就是上面的代码）。所以，按照调用的过程，上面的伪代码将变成下面这个样子，\n\n```java\nA = Observable.create(S)\nO = LambdaObserver(X,Y,Z)\nD = A.subscribe(O)\nA.subscribeActual(O)\n```\n\n于是我们可以得知，当调用了 `subscribe()` 方法的时候，实际上调用了 A 的 `subscribeActual()` 方法，并将 B 作为参数传入。B 是 LambdaObserver，由我们调用 `subscribe()` 的时候传入的三个参数组成。那么 A 呢？回到之前的 `create()` 代码中，我们得知它就是 `ObservableCreate` 的实例。这里会调用到它的 `subscribeActual()` 方法。按照字母表示的方式，该方法将会成为下面这个样子，\n\n```java\n    @Override\n    protected void subscribeActual(O) {\n        P = new CreateEmitter<T>(O);\n        O.onSubscribe(P);\n        S.subscribe(P);\n    }\n```\n\n这里的 S 是由 ObservableCreate 的构造方法传入的，也就是我们在 `create()` 方法中传入的对象。首先，这里会将 O 作为构造方法的参数传入到 `CreateEmitter` 实例中。然后，回调 O 的 `onSubscribe()` 方法并将 P 传出。这是我们常用的 RxJava 的回调方法之一。第三步中，我们调用了 S 的 `subscribe()` 方法并将 P 传出。所以，当我们按照示例代码的方式调用下面这行代码的时候，\n\n```java\n    emitter.onNext(new Object());\n```\n\n实际上是调用了这里的 P 的方法。那么，我们来看 P 的 `onNext()` 方法，\n\n```java\n    @Override\n    public void onNext(T t) {\n        O.onNext(t);\n    }\n```\n\n它通过调用 O 的 `onNext()` 方法实现。所以，到头来，其实还是回调了我们的在 `subscribe()` 方法中传入的 Consumer 的方法。这样就通过回调的方式把我们发送的值，传递给了我们的观察方法。\n\n### 1.2 `dispose()` 方法的执行过程\n\n上面分析了 `create()` 和 `subscribe()` 方法的主流程。那么 `dispose()` 方法呢？\n\n按照上面给出的代码，它的定义如下。也就是通过 `DisposableHelper` 的 `dispose()` 方法来最终完成取消订阅。\n\n```java\n    @Override\n    public void dispose() {\n        DisposableHelper.dispose(this);\n    }\n```\n\n`DisposableHelper` 的 `dispose()` 方法的定义如下。按照上面的分析，`dispose()` 的时候传入的 this 就是 CreateEmitter. 并且它是继承了 `AtomicReference<Disposable>` 的。\n\n```java\n    public static boolean dispose(AtomicReference<Disposable> field) {\n        Disposable current = field.get();\n        Disposable d = DISPOSED;\n        if (current != d) {\n            current = field.getAndSet(d);\n            if (current != d) {\n                if (current != null) {\n                    current.dispose();\n                }\n                return true;\n            }\n        }\n        return false;\n    }\n```\n\n对 AtomicReference，相比大家都不陌生，它是一个原子类型的引用。这里正式通过对该原子类型引用的赋值来完成取消订阅的——通过一个原子操作将其设置为 DISPOSED. \n            \n### 1.3 RxJava 执行过程的总结\n\n上面我们总结了 RxJava 的 Observable 从 `create()` 到 `subscribe()` 到 `dispose()` 方法的执行过程。虽然，我们依靠自己的逻辑能够把整个流程梳理下来，但是这太笨拙了。除了掌握了整个流程，我想我们更应该分析下它使用的设计思想。\n\n一开始，当我们分析到上面的流程的时候，我也是云里雾里，但是当我继续分析了 `subscribeOn()` 的时候才恍然大悟——它整体的设计使用的设计模式和 Java 中的流是一致的。在真正分析 `subscribeOn()` 之前，我们先来看下它的代码，\n\n```java\n    public final Observable<T> subscribeOn(Scheduler scheduler) {\n        return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<T>(this, scheduler));\n    }\n\n    public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> {\n        final Scheduler scheduler;\n\n        public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) {\n            super(source);\n            this.scheduler = scheduler;\n        }\n\n        @Override\n        public void subscribeActual(final Observer<? super T> s) {\n            final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);\n\n            s.onSubscribe(parent);\n\n            parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));\n        }\n        // ...\n    }\n```\n\n对比一下 `subscribeOn()` 方法和 `create()` 方法，我们可以很容易地发现，它们的逻辑几乎是一致的。都是传入了一个 ObservableSource 之后对其进行包装，然后在 `subscribeActual()` 方法中，得到一个 parent，然后调用 `onSubscribe()` 继而进行后续处理……也就是它和 Java 的 IO 体系一样，都使用了**装饰者设计模式**。\n\n在 Java 的 IO 体系中，我们经常可见下面的代码。\n\n```java\n    InputStream is = new FileInputStream(fileToCopy);\n    BufferedInputStream bis = new BufferedInputStream(is, 15 * 1024);\n    DataInputStream dis = new DataInputStream(bis);\n```\n\n这里的 FileInputStream 是节点流，用来打开磁盘上面的输入流。后续的 BufferedInputStream 和 DataInputStream 都用来对节点流进行修饰。它们各自只需要完成自己的功能，前者主要负责缓存以提升读取速率，后者用来将得到的流转换成我们需要的数据类型。如果我们由其他的需求只需要在这个链的基础上实现一个自定义的装饰器即可。\n\n回想一下我们在实际的开发过程中是不是经常使用链式来调用一大串，中间的各个环节分别来实现自己的功能，比如转换、过滤、统计等等。使用了装饰者模式之后，链的每个环节只需要实现自己的功能，使用者可以根据自己的需求在链上面增加环节。所以，类似于转换、过滤、统计等等，每个类的责任变得单一了，从整个调用链上面解耦出来。真是不得不佩服 RxJava 的这种设计！\n\n知道了 RxJava 的整体使用的是装饰者设计模式，我们理解其它的一些特性来就容易得多。按照装饰者设计模式的思路，RxJava 的包装过程和调用 `subscribe()` 方法之后的回调过程将如下所示：\n\n![RxJava 的包装和回调的过程](res/RxJava.png)\n\n所以，为什么 RxJava 为人诟病其调用栈太长，就是因为当我们使用一个个的装饰器套起来的时候，导致整个调用的栈变得很长。\n\n另外，捎带说一下所谓的线程切换的问题。假如我们在上述调用过程中的 4 处使用了 `subscribeOn()` 方法，并指定处理的线程为 A；在 5 处同样调用该方法，但是指定的线程为 B，那么之前的 1~3 的过程会被包装成一个对象，放在 4 指定的线程中执行；然后 4 又被包装成一个对象放在 5 所在的线程。因此，如果我们在 2 中获取当前线程，那么肯定得到的是 4 所在的线程。也就是当使用两个 `subscribeOn()` 的时候，通常会被认为只有第一个有效的原因。其实两个都有效，只是 A 是在 B 中执行的，而 1~3 又是在 A 中执行的。所以，所谓的线程切换到奥秘啊，就是依靠这层包裹的关系实现的。一个线程里面把任务执行完了，自然就切换到另一个线程里了。（`subscribeOn()` 和 `observeOn()` 实现线程的时候稍有区别，详情看下文。）\n\n### 1.4 RxJava 的线程切换的执行过程\n\n上面我们也提到过 `subscribeOn()` 和 `observeOn()` 实现线程切换的方式有所不同。所以，在下面的文章中，我们分成两种情况来分别对其进行分析。\n\n当调用 `subscribeOn()` 方法的时候，上流传入的 Observable 将会被进一步装饰成 ObservableSubscribeOn 对象。按照我们上面的分析，当最终调用 `subscribe()` 方法的时候，将会沿着装饰器构成的链，直到 ObservableSubscribeOn 的 `subscribeActual()` 方法中。下面就是该方法的定义，\n\n```java\n    @Override\n    public void subscribeActual(final Observer<? super T> s) {\n        final SubscribeOnObserver<T> parent = new SubscribeOnObserver<T>(s);\n        s.onSubscribe(parent);\n        parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));\n    }\n```\n\n除了上面分析的内容，这里多了一个 scheduler，它就是我们调用  `subscribeOn()` 方法时指定的线程。这里会直接调用它的 `scheduleDirect()` 方法将任务添加到线程池当中执行。这里传入的是 SubscribeTask 对象，它实现了 `Runnable` 接口，并且会在覆写的 `run()` 方法中调用传入的 parent 的 `subscribe()` 方法。因此，它可以被放入任何线程池当中执行，并且当被执行的时候会调用传入的 Observable 的 `subscribe()` 方法来让上流的任务在该线程池当中执行。\n\n下面是 RxJava 中异步任务执行的流程图，\n\n![RxJava 任务调度](res/RxJava_Scheduler.png)\n\n这里的传入的 Schduler 是一个顶层的类，当我们调用 `Schedulers.io()` 等方法的时候，会获取其实现类的实例，比如 IOScheduler. 上面调用 `scheduleDirect()` 方法之后会先使用 Scheduler 的模板方法 `createWorker()` 中获取到一个 Worker. 这个类用来对 RxJava 的任务进行管理。它会进一步调用自己的 `schedule()` 方法来进一步安排任务的执行。图中的 Worker 也是一个抽象类，上面用到的 NewThreadWorker 是它的一个实现。NewThreadWorker 中维护了一个线程池，当调用了它的 `scheduler()` 方法的时候，它就会进一步把该任务放进线程池当中执行。因此，我们的异步任务就在该线程池当中被执行了。\n\n然后，我们再来看下 `observeOn()` 方法是如何进行任务调度的。\n\n当我们调用 `observeOn()` 方法的时候，该任务会被包装成 ObservableObserveOn 的实例。同样，我们来看它的 `subscribeActual()` 方法，\n\n```java\n    @Override\n    protected void subscribeActual(Observer<? super T> observer) {\n        if (scheduler instanceof TrampolineScheduler) {\n            source.subscribe(observer);\n        } else {\n            Scheduler.Worker w = scheduler.createWorker();\n            source.subscribe(new ObserveOnObserver<T>(observer, w, delayError, bufferSize));\n        }\n    }\n```\n\n它会直接调用 Scheduler 的模板方法得到 Worker，然后将 Worker 和传入的 Observer 一起包装到 ObserveOnObserver 中。它会被继续向上传递到 ObservableCreate 中，然后它的 `onNext()` 等方法将会被顶层的类触发。接下来，我们就看下 ObserveOnObserver 的定义，这里我们仍然只以 `onNext()` 为例，其方法源码如下，\n\n```java\n    @Override\n    public void onNext(T t) {\n        if (done) {\n            return;\n        }\n        if (sourceMode != QueueDisposable.ASYNC) {\n            queue.offer(t);\n        }\n        schedule();\n    }\n\n    void schedule() {\n        if (getAndIncrement() == 0) {\n            worker.schedule(this);\n        }\n    }\n```\n\n因此，可以看出，在 `observeOn()` 方法中也是通过将任务放进某个 Worker 中执行来实现的，只是具体的线程将取决于 Scheduler 和 Worker 的具体实现。\n\n而 Android 中的将任务放进主线程当中去执行就是通过向主线程的 Handler 发送消息来实现的。如果按照 `subscribeOn()` 的解释，那么当 A 线程启动 B 线程执行任务，那么 B 执行完自然就到了 A 了。那么为什么 Android 中还需要向主线程中发送消息呢？我们使用下面的图来解释。\n\n![RxJava 线程切换](res/RxJava_Switch2.png)\n\n`subscribeOn()` 是一个向上回调的过程，当 A 线程启动 B 线程执行任务，那么 B 执行完自然就到了 A 了，没有问题。但 `observeOn()` 是一个向下调用的过程，从上面的代码中也可以看出，它直接在线程池当中调用 `onNext()` 的时候会沿着回调相反的路线从上往下执行，因此 `observeOn()` 之后所有的逻辑在它指定的线程中执行。\n\n## 2、总结\n\n在本篇文章中，我们总结了 RxJava 2 的源码。虽然 RxJava 的功能非常强大，但是其核心的实现却仅仅依赖两个设计模式，一个是观察者模式，另一个是装饰器模式。它采用了类似于 Java 的流的设计，每个装饰器负责自己一种任务，这复合单一责任原则；各个装饰器之间相互协作，来完成复杂的功能。从上面的源码分析过程中我们也可以看出，RxJava 的缺点也是非常明显的，大量的自定义类，在完成一个功能的时候各装饰器之间不断包装，导致调用的栈非常长。至于线程的切换，它依赖于自己的装饰器模式，因为一个装饰器可以决定其上游的 Observable 在哪些线程当中执行；两个装饰器处于不同的线程的时候，从一个线程中执行完毕自然进入到另一个线程中执行就完成了线程切换的过程。\n\n以上就是 RxJava 的源码分析，如有疑问，欢迎评论区交流:)\n\n"
  },
  {
    "path": "响应式编程/RxJava系列（1）：一篇的比较全面的RxJava2方法总结.md",
    "content": "# RxJava2 系列 （1）：一篇的比较全面的 RxJava2 方法总结\n\n看了许多讲解RxJava的文章，有些文章讲解的内容是基于第一个版本的，有些文章的讲解是通过比较常用的一些API和基础的概念进行讲解的。\n但是每次看到RxJava的类中的几十个方法的时候，总是感觉心里没底。所以，我打算自己去专门写篇文章来从API的角度系统地梳理一下RxJava的各种方法和用法。\n\n## 1、RxJava 基本\n\n### 1.1 RxJava 简介\n\nRxJava是一个在Java VM上使用可观测的序列来组成异步的、基于事件的程序的库。\n\n虽然，在Android中，我们可以使用AsyncTask来完成异步任务操作，但是当任务的梳理比较多的时候，我们要为每个任务定义一个AsyncTask就变得非常繁琐。\nRxJava能帮助我们在实现异步执行的前提下保持代码的清晰。\n它的原理就是创建一个`Observable`来完成异步任务，组合使用各种不同的链式操作，来实现各种复杂的操作，最终将任务的执行结果发射给`Observer`进行处理。\n当然，RxJava不仅适用于Android，也适用于服务端等各种场景。\n\n我们总结以下RxJava的用途：\n\n1. 简化异步程序的流程；\n2. 使用近似于Java8的流的操作进行编程：因为想要在Android中使用Java8的流编程有诸多的限制，所以我们可以使用RxJava来实现这个目的。\n\n在使用RxJava之前，我们需要先在自己的项目中添加如下的依赖：\n\n    compile 'io.reactivex.rxjava2:rxjava:2.2.0'\n    compile 'io.reactivex.rxjava2:rxandroid:2.0.2'\n\n这里我们使用的是RxJava2，它与RxJava的第一个版本有些许不同。在本文中，我们所有的关于RxJava的示例都将基于RxJava2. \n\n注：如果想了解关于Java8的流编程的内容的内容，可以参考我之前写过的文章[五分钟学习Java8的流编程](https://juejin.im/post/5b07f4536fb9a07ac90da4e5)。\n\n### 1.2 概要\n\n下面是RxJava的一个基本的用例，这里我们定义了一个`Observable`，然后在它内部使用`emitter`发射了一些数据和信息（其实就相当于调用了被观察对象内部的方法，通知所有的观察者）。\n然后，我们用`Consumer`接口的实例作为`subscribe()`方法的参数来观察发射的结果。（这里的接口的方法都已经被使用Lambda简化过，应该学着适应它。）\n\n    Observable<Integer> observable = Observable.create(emitter -> {\n        emitter.onNext(1);\n        emitter.onNext(2);\n        emitter.onNext(3);\n    });\n    observable.subscribe(System.out::println);\n\n这样，我们就完成了一个基本的RxJava的示例。从上面的例子中，你或许没法看出`Observable`中隐藏的流的概念，看下面的例子：\n\n    Observable.range(0, 10).map(String::valueOf).forEach(System.out::println);\n\n这里我们先用`Observable.range()`方法产生一个序列，然后用`map`方法将该整数序列映射成一个字符序列，最后将得到的序列输出来。从上面看出，这种操作和Java8里面的Stream编程很像。但是两者之间是有区别的：\n\n1. 所谓的“推”和“拉”的区别：Stream中是通过从流中读取数据来实现链式操作，而RxJava除了Stream中的功能之外，还可以通过“发射”数据，来实现通知的功能，即RxJava在Stream之上又多了一个观察者的功能。\n2. Java8中的Stream可以通过`parall()`来实现并行，即基于分治算法将任务分解并计算得到结果之后将结果合并起来；而RxJava只能通过`subscribeOn()`方法将所有的操作切换到某个线程中去。\n3. Stream只能被消费一次，但是`Observable`可以被多次进行订阅；\n\nRxJava除了为我们提供了`Observable`之外，在新的RxJava中还提供了适用于其他场景的基础类，它们之间的功能和主要区别如下：\n\n1. `Flowable`: 多个流，响应式流和背压\n2. `Observable`: 多个流，无背压\n3. `Single`: 只有一个元素或者错误的流\n4. `Completable`: 没有任何元素，只有一个完成和错误信号的流\n5. `Maybe`: 没有任何元素或者只有一个元素或者只有一个错误的流\n\n除了上面的几个基础类之外，还有一个`Disposable`。当我们监听某个流的时候，就能获取到一个`Disposable`对象。它提供了两个方法，一个是`isDisposed`，可以被用来判断是否停止了观察指定的流；另一个是`dispose`方法，用来放弃观察指定的流，我们可以使用它在任意的时刻停止观察操作。\n\n### 1.3 总结\n\n上面我们介绍了了关于RxJava的基本的概念和使用方式，在下面的文章中我们会按照以上定义的顺序从API的角度来讲解以下RxJava各个模块的使用方法。\n\n## 2、RxJava 的使用\n\n### 2.1 Observable\n\n从上面的文章中我们可以得知，`Observable`和后面3种操作功能近似，区别在于`Flowable`加入了背压的概念，`Observable`的大部分方法也适用于其他3个操作和`Flowable`。\n因此，我们这里先从`Observable`开始梳理，然后我们再专门对`Flowable`和背压的进行介绍。\n\n`Observable`为我们提供了一些静态的构造方法来创建一个`Observable`对象，还有许多链式的方法来完成各种复杂的功能。\n这里我们按照功能将它的这些方法分成各个类别并依次进行相关的说明。\n\n#### 2.1.1 创建操作\n\n1.interval & intervalRange\n\n下面的操作可以每个3秒的时间发送一个整数，整数从0开始：\n\n    Observable.interval(3, TimeUnit.SECONDS).subscribe(System.out::println);\n\n如果想要设置从指定的数字开始也是可以的，实际上`interval`提供了许多重载方法供我们是使用。下面我们连同与之功能相近的`intervalRange`方法也一同给出：\n\n1. `public static Observable<Long> interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler)`\n2. `public static Observable<Long> interval(long period, TimeUnit unit, Scheduler scheduler)`\n3. `public static Observable<Long> intervalRange(long start, long count, long initialDelay, long period, TimeUnit unit, Scheduler scheduler)`\n\n这里的`initialDelay`参数用来指示开始发射第一个整数的之前要停顿的时间，时间的单位与`peroid`一样，都是通过`unit`参数来指定的；`period`参数用来表示每个发射之间停顿多少时间；`unit`表示时间的单位，是`TimeUnit`类型的；`scheduler`参数指定数据发射和等待时所在的线程。\n\n`intervalRange`方法可以用来将发射的整数序列限制在一个范围之内，这里的`start`用来表示发射的数据的起始值，`count`表示总共要发射几个数字，其他参数与上面的`interval`方法一致。\n\n2.range & rangeLong\n\n下面的操作可以产生一个从5开始的连续10个整数构成的序列：\n\n    Observable.range(5, 10).subscribe(i -> System.out.println(\"1: \" + i));\n\n该方法需要传入两个参数，与之有相同功能的方法还有`rangeLong`：\n\n1. `public static Observable<Integer> range(final int start, final int count)`\n2. `public static Observable<Long> rangeLong(long start, long count)`\n\n这里的两个参数`start`用来指定用于生成的序列的开始值，`count`用来指示要生成的序列总共包含多少个数字，上面的两个方法的主要区别在于一个是用来生成int型整数的，一个是用来生成long型整数的。\n\n3.create\n\n`create`方法用于从头开始创建一个`Observable`，像下面显示的那样，你需要使用`create`方法并传一个发射器作为参数，在该发射器内部调用`onNext`、`onComplete`和`onError`方法就可以将数据发送给监听者。\n\n    Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {\n        observableEmitter.onNext(1);\n        observableEmitter.onNext(2);\n        observableEmitter.onComplete();\n    }).subscribe(System.out::println);\n\n4.defer\n\n`defer`直到有观察者订阅时才创建Observable，并且为每个观察者创建一个新的Observable。`defer`操作符会一直等待直到有观察者订阅它，然后它使用Observable工厂方法生成一个Observable。比如下面的代码两个订阅输出的结果是不一致的：\n\n    Observable<Long> observable = Observable.defer((Callable<ObservableSource<Long>>) () -> Observable.just(System.currentTimeMillis()));\n    observable.subscribe(System.out::print);\n    System.out.println();\n    observable.subscribe(System.out::print);\n\n下面是该方法的定义，它接受一个Callable对象，可以在该对象中返回一个Observable的实例：\n\n`public static <T> Observable<T> defer(Callable<? extends ObservableSource<? extends T>> supplier)`\n\n5.empty & never & error\n\n1. `public static <T> Observable<T> empty()`：创建一个不发射任何数据但是正常终止的Observable；\n2. `public static <T> Observable<T> never()`：创建一个不发射数据也不终止的Observable；\n3. `public static <T> Observable<T> error(Throwable exception)`：创建一个不发射数据以一个错误终止的Observable，它有几个重载版本，这里给出其中的一个。\n\n测试代码：\n\n    Observable.empty().subscribe(i->System.out.print(\"next\"),i->System.out.print(\"error\"),()->System.out.print(\"complete\"));\n    Observable.never().subscribe(i->System.out.print(\"next\"),i->System.out.print(\"error\"),()->System.out.print(\"complete\"));\n    Observable.error(new Exception()).subscribe(i->System.out.print(\"next\"),i->System.out.print(\"error\"),()->System.out.print(\"complete\"));\n\n输出结果：`completeerror`\n\n6.from 系列\n\n`from`系列的方法用来从指定的数据源中获取一个Observable：\n\n1. `public static <T> Observable<T> fromArray(T... items)`：从数组中获取；\n2. `public static <T> Observable<T> fromCallable(Callable<? extends T> supplier)`：从Callable中获取；\n3. `public static <T> Observable<T> fromFuture(Future<? extends T> future)`：从Future中获取，有多个重载版本，可以用来指定线程和超时等信息；\n4. `public static <T> Observable<T> fromIterable(Iterable<? extends T> source)`：从Iterable中获取；\n5. `public static <T> Observable<T> fromPublisher(Publisher<? extends T> publisher)`：从Publisher中获取。\n\n7.just 系列\n\njust系列的方法的一个参数的版本为下面的形式：`public static <T> Observable<T> just(T item)`，它还有许多个重载的版本，区别在于接受的参数的个数不同，最少1个，最多10个。\n\n8.repeat\n\n该方法用来表示指定的序列要发射多少次，下面的方法会将该序列无限次进行发送：\n\n    Observable.range(5, 10).repeat().subscribe(i -> System.out.println(i));\n\n`repeat`方法有以下几个相似方法：\n\n1. `public final Observable<T> repeat()`\n2. `public final Observable<T> repeat(long times)`\n3. `public final Observable<T> repeatUntil(BooleanSupplier stop)`\n4. `public final Observable<T> repeatWhen(Function<? super Observable<Object>, ? extends ObservableSource<?>> handler)`\n\n第1个无参的方法会无限次地发送指定的序列（实际上内部调用了第2个方法并传入了Long.MAX_VALUE），第2个方法会将指定的序列重复发射指定的次数；第3个方法会在满足指定的要求的时候停止重复发送，否则会一直发送。\n\n9.timer\n\ntimer操作符创建一个在给定的时间段之后返回一个特殊值的Observable，它在延迟一段给定的时间后发射一个简单的数字0。比如下面的程序会在500毫秒之后输出一个数字`0`。\n\n    Observable.timer(500, TimeUnit.MILLISECONDS).subscribe(System.out::print);\n\n下面是该方法及其重载方法的定义，重载方法还可以指定一个调度器：\n\n1. `public static Observable<Long> timer(long delay, TimeUnit unit)`\n2. `public static Observable<Long> timer(long delay, TimeUnit unit, Scheduler scheduler)`\n\n#### 2.1.2 变换操作\n\n1.map & cast\n\n1. `map`操作符对原始Observable发射的每一项数据应用一个你选择的函数，然后返回一个发射这些结果的Observable。默认不在任何特定的调度器上执行。\n2. `cast`操作符将原始Observable发射的每一项数据都强制转换为一个指定的类型（多态），然后再发射数据，它是map的一个特殊版本：\n\n下面的第一段代码用于将生成的整数序列转换成一个字符串序列之后并输出；第二段代码用于将Date类型转换成Object类型并进行输出，这里如果前面的Class无法转换成第二个Class就会出现异常：\n\n    Observable.range(1, 5).map(String::valueOf).subscribe(System.out::println);\n    Observable.just(new Date()).cast(Object.class).subscribe(System.out::print);\n\n这两个方法的定义如下：\n\n1. `public final <R> Observable<R> map(Function<? super T, ? extends R> mapper)`\n2. `public final <U> Observable<U> cast(Class<U> clazz)`\n\n这里的`mapper`函数接受两个泛型，一个表示原始的数据类型，一个表示要转换之后的数据类型，转换的逻辑写在该接口实现的方法中即可。\n\n2.flatMap & contactMap\n\n`flatMap`将一个发送事件的上游Observable变换为多个发送事件的Observables，然后将它们发射的事件合并后放进一个单独的Observable里。需要注意的是, flatMap并不保证事件的顺序，也就是说转换之后的Observables的顺序不必与转换之前的序列的顺序一致。比如下面的代码用于将一个序列构成的整数转换成多个单个的`Observable`，然后组成一个`OBservable`，并被订阅。下面输出的结果仍将是一个字符串数字序列，只是顺序不一定是增序的。\n\n    Observable.range(1, 5)\n            .flatMap((Function<Integer, ObservableSource<String>>) i -> Observable.just(String.valueOf(i)))\n            .subscribe(System.out::println);\n\n与`flatMap`对应的方法是`contactMap`，后者能够保证最终输出的顺序与上游发送的顺序一致。下面是这两个方法的定义：\n\n1. `public final <R> Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper)`\n2. `public final <R> Observable<R> concatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper)`\n\n`flatMap`的重载方法数量过多，它们在数据源方面略有不同，有的支持错误等可选参数，具体可以参考源代码。\n\n3.flatMapIterable\n\n`flatMapIterable`可以用来将上流的任意一个元素转换成一个`Iterable`对象，然后我们可以对其进行消费。在下面的代码中，我们先生成一个整数的序列，然后将每个整数映射成一个`Iterable<string>`类型，最后，我们对其进行订阅和消费：\n\n    Observable.range(1, 5)\n            .flatMapIterable((Function<Integer, Iterable<String>>) integer -> Collections.singletonList(String.valueOf(integer)))\n            .subscribe(s -> System.out.println(\"flatMapIterable : \" + s));\n\n下面是该方法及其重载方法的定义：\n\n1. `public final <U> Observable<U> flatMapIterable(Function<? super T, ? extends Iterable<? extends U>> mapper)`\n2. `public final <U, V> Observable<V> flatMapIterable(Function<? super T, ? extends Iterable<? extends U>> mapper, BiFunction<? super T, ? super U, ? extends V> resultSelector)`\n\n4.buffer\n\n该方法用于将整个流进行分组。以下面的程序为例，我们会先生成一个7个整数构成的流，然后使用`buffer`之后，这些整数会被3个作为一组进行输出，所以当我们订阅了`buffer`转换之后的`Observable`之后得到的是一个列表构成的`OBservable`：\n\n    Observable.range(1, 7).buffer(3)\n            .subscribe(integers -> System.out.println(Arrays.toString(integers.toArray())));\n\n下面是这个方法及其重载方法的定义，它的重载方法太多，这里我们只给出其中的两个，其他的可以参考RxJava的源码。这里的buffer应该理解为一个缓冲区，当缓冲区满了或者剩余的数据不够一个缓冲区的时候就将数据发射出去。\n\n1. `public final Observable<List<T>> buffer(int count)`\n2. `public final Observable<List<T>> buffer(int count, int skip)`\n3. ...\n\n5.groupBy\n\n`groupBy`用于分组元素，它可以被用来根据指定的条件将元素分成若干组。它将得到一个`Observable<GroupedObservable<T, M>>`类型的`Observable`。如下面的程序所示，这里我们使用`concat`方法先将两个`Observable`拼接成一个`Observable`，然后对其元素进行分组。这里我们的分组依据是整数的值，这样我们将得到一个`Observable<GroupedObservable<Integer, Integer>>`类型的`Observable`。然后，我们再将得到的序列拼接成一个并进行订阅输出：\n\n    Observable<GroupedObservable<Integer, Integer>> observable = Observable.concat(\n            Observable.range(1,4), Observable.range(1,6)).groupBy(integer -> integer);\n    Observable.concat(observable).subscribe(integer -> System.out.println(\"groupBy : \" + integer));\n\n该方法有多个重载版本，这里我们用到的一个的定义是：\n\n`public final <K> Observable<GroupedObservable<K, T>> groupBy(Function<? super T, ? extends K> keySelector)`\n\n6.scan\n\n`scan`操作符对原始Observable发射的第一项数据应用一个函数，然后将那个函数的结果作为自己的第一项数据发射。它将函数的结果同第二项数据一起填充给这个函数来产生它自己的第二项数据。它持续进行这个过程来产生剩余的数据序列。这个操作符在某些情况下被叫做accumulator。\n\n以下面的程序为例，该程序的输结果是`2 6 24 120 720`，可以看出这里的计算规则是，我们把传入到`scan`中的函数记为`f`，序列记为`x`，生成的序列记为`y`，那么这里的计算公式是`y(0)=x(0); y(i)=f(y(i-1), x(i)), i>0`：\n\n    Observable.range(2, 5).scan((i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + \" \"));\n\n除了上面的这种形式，`scan`方法还有一个重载的版本，我们可以使用这个版本的方法来在生成序列的时候指定一个初始值。以下面的程序为例，它的输出结果是`3 6 18 72 360 2160 `，可以看出它的输出比上面的形式多了1个，这是因为当指定了初始值之后，生成的第一个数字就是那个初始值，剩下的按照我们上面的规则进行的。所以，用同样的函数语言来描述的话，那么它就应该是下面的这种形式：`y(0)=initialValue; y(i)=f(y(i-1), x(i)), i>0`。\n\n    Observable.range(2, 5).scan(3, (i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + \" \"));\n\n以上方法的定义是：\n\n1. `public final Observable<T> scan(BiFunction<T, T, T> accumulator)`\n2. `public final <R> Observable<R> scan(R initialValue, BiFunction<R, ? super T, R> accumulator)`\n\n7.window\n\n`window`Window和Buffer类似，但不是发射来自原始Observable的数据包，它发射的是Observable，这些Observables中的每一个都发射原始Observable数据的一个子集，最后发射一个onCompleted通知。\n\n以下面的程序为例，这里我们首先生成了一个由10个数字组成的整数序列，然后使用`window`函数将它们每3个作为一组，每组会返回一个对应的Observable对象。\n这里我们对该返回的结果进行订阅并进行消费，因为10个数字，所以会被分成4个组，每个对应一个Observable：\n\n    Observable.range(1, 10).window(3).subscribe(\n            observable -> observable.subscribe(integer -> System.out.println(observable.hashCode() + \" : \" + integer)));\n\n除了对数据包进行分组，我们还可以根据时间来对发射的数据进行分组。该方法有多个重载的版本，这里我们给出其中的比较具有代表性的几个：\n\n1. `public final Observable<Observable<T>> window(long count)`\n2. `public final Observable<Observable<T>> window(long timespan, long timeskip, TimeUnit unit)`\n3. `public final <B> Observable<Observable<T>> window(ObservableSource<B> boundary)`\n4. `public final <B> Observable<Observable<T>> window(Callable<? extends ObservableSource<B>> boundary)`\n\n#### 2.1.3 过滤操作\n\n1.filter\n\n`filter`用来根据指定的规则对源进行过滤，比如下面的程序用来过滤整数1到10中所有大于5的数字：\n\n    Observable.range(1,10).filter(i -> i > 5).subscribe(System.out::println);\n\n下面是该方法的定义：\n\n1. `public final Observable<T> filter(Predicate<? super T> predicate)`\n\n2.elementAt & firstElement & lastElement\n\n`elementAt`用来获取源中指定位置的数据，它有几个重载方法，这里我们介绍一下最简单的一个方法的用法。下面是`elementAt`的一个示例，它将获取源数据中索引为1的元素并交给观察者订阅。下面的程序将输出`1`\n\n    Observable.range(1, 10).elementAt(0).subscribe(System.out::print);\n\n这里我们给出`elementAt`及其相关的方法的定义，它们的使用相似。注意一下这里的返回类型：\n\n1. `public final Maybe<T> elementAt(long index)`\n2. `public final Single<T> elementAt(long index, T defaultItem)`\n3. `public final Single<T> elementAtOrError(long index)`\n\n除了获取指定索引的元素的方法之外，RxJava中还有可以用来直接获取第一个和最后一个元素的方法，这里我们直接给出方法的定义：\n\n1. `public final Maybe<T> firstElement()`\n2. `public final Single<T> first(T defaultItem)`\n3. `public final Single<T> firstOrError()`\n4. `public final Maybe<T> lastElement()`\n5. `public final Single<T> last(T defaultItem)`\n6. `public final Single<T> lastOrError()`\n\n3.distinct & distinctUntilChanged\n\n`distinct`用来对源中的数据进行过滤，以下面的程序为例，这里会把重复的数字7过滤掉：\n\n    Observable.just(1,2,3,4,5,6,7,7).distinct().subscribe(System.out::print);\n\n与之类似的还有`distinctUntilChanged`方法，与`distinct`不同的是，它只当相邻的两个元素相同的时候才会将它们过滤掉。比如下面的程序会过滤掉其中的2和5，所以最终的输出结果是`12345676`：\n\n    Observable.just(1,2,2,3,4,5,5,6,7,6).distinctUntilChanged().subscribe(System.out::print);\n\n该方法也有几个功能相似的方法，这里给出它们的定义如下：\n\n1. `public final Observable<T> distinct()`\n2. `public final <K> Observable<T> distinct(Function<? super T, K> keySelector)`\n3. `public final <K> Observable<T> distinct(Function<? super T, K> keySelector, Callable<? extends Collection<? super K>> collectionSupplier)`\n4. `public final Observable<T> distinctUntilChanged()`\n5. `public final <K> Observable<T> distinctUntilChanged(Function<? super T, K> keySelector)`\n6. `public final Observable<T> distinctUntilChanged(BiPredicate<? super T, ? super T> comparer)`\n\n4.skip & skipLast & skipUntil & skipWhile\n\n`skip`方法用于过滤掉数据的前n项，比如下面的程序将会过滤掉前2项，因此输出结果是`345`：\n\n    Observable.range(1, 5).skip(2).subscribe(System.out::print);\n\n与`skip`方法对应的是`take`方法，它用来表示只选择数据源的前n项，该方法的示例就不给出了。这里，我们说一下与之类功能类似的重载方法。`skip`还有一个重载方法接受两个参数，用来表示跳过指定的时间，也就是在指定的时间之后才开始进行订阅和消费。下面的程序会在3秒之后才开始不断地输出数字：\n\n    Observable.range(1,5).repeat().skip(3, TimeUnit.SECONDS).subscribe(System.out::print);\n\n与`skip`功能相反的方法的还有`skipLast`，它用来表示过滤掉后面的几项，以及最后的一段时间不进行发射等。比如下面的方法，我们会在程序开始之前进行计时，然后会不断重复输出数字，直到5秒之后结束。然后，我们用`skipLast`方法表示最后的2秒不再进行发射。所以下面的程序会先不断输出数字3秒，3秒结束后停止输出，并在2秒之后结束程序：\n\n    long current = System.currentTimeMillis();\n    Observable.range(1,5)\n            .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5))\n            .skipLast(2, TimeUnit.SECONDS).subscribe(System.out::print);\n\n与上面的这些方法类似的还有一些，这里我们不再一一列举。因为这些方法的重载方法比较多，下面我们给出其中的具有代表性的一部分：\n\n1. `public final Observable<T> skip(long count)`\n2. `public final Observable<T> skip(long time, TimeUnit unit, Scheduler scheduler)`\n3. `public final Observable<T> skipLast(int count)`\n4. `public final Observable<T> skipLast(long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)`\n5. `public final <U> Observable<T> skipUntil(ObservableSource<U> other)`\n6. `public final Observable<T> skipWhile(Predicate<? super T> predicate)`\n\n5.take & takeLast & takeUntil & takeWhile\n\n与`skip`方法对应的是`take`方法，它表示按照某种规则进行选择操作。我们以下面的程序为例，这里第一段程序表示只发射序列中的前2个数据：\n\n    Observable.range(1, 5).take(2).subscribe(System.out::print);\n\n下面的程序表示只选择最后2秒中输出的数据：\n\n    long current = System.currentTimeMillis();\n    Observable.range(1,5)\n            .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5))\n            .takeLast(2, TimeUnit.SECONDS).subscribe(System.out::print);\n\n下面是以上相关的方法的定义，同样的，我们只选择其中比较有代表性的几个：\n\n1. `public final Observable<T> take(long count)`\n2. `public final Observable<T> takeLast(long count, long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)`\n3. `public final <U> Observable<T> takeUntil(ObservableSource<U> other)`\n4. `public final Observable<T> takeUntil(Predicate<? super T> stopPredicate)`\n5. `public final Observable<T> takeWhile(Predicate<? super T> predicate)`\n\n6.ignoreElements\n\n该方法用来过滤所有源Observable产生的结果，只会把Observable的onComplete和onError事件通知给订阅者。下面是该方法的定义：\n\n1. `public final Completable ignoreElements()`\n\n7.throttleFirst & throttleLast & throttleLatest & throttleWithTimeout\n\n这些方法用来对输出的数据进行限制，它们是通过时间的”窗口“来进行限制的，你可以理解成按照指定的参数对时间进行分片，然后根据各个方法的要求选择第一个、最后一个、最近的等进行发射。下面是`throttleLast`方法的用法示例，它会输出每个500毫秒之间的数字中最后一个数字：\n\n    Observable.interval(80, TimeUnit.MILLISECONDS)\n            .throttleLast(500, TimeUnit.MILLISECONDS)\n            .subscribe(i -> System.out.print(i + \" \"));\n\n其他的几个方法的功能大致列举如下：\n\n1. `throttleFirst`只会发射指定的Observable在指定的事件范围内发射出来的第一个数据；\n2. `throttleLast`只会发射指定的Observable在指定的事件范围内发射出来的最后一个数据；\n3. `throttleLatest`用来发射距离指定的时间分片最近的那个数据;\n5. `throttleWithTimeout`仅在过了一段指定的时间还没发射数据时才发射一个数据，如果在一个时间片达到之前，发射的数据之后又紧跟着发射了一个数据，那么这个时间片之内之前发射的数据会被丢掉，该方法底层是使用`debounce`方法实现的。如果数据发射的频率总是快过这里的`timeout`参数指定的时间，那么将不会再发射出数据来。\n\n下面是这些方法及其重载方法的定义（选择其中一部分）：\n\n1. `public final Observable<T> throttleFirst(long skipDuration, TimeUnit unit, Scheduler scheduler)`\n2. `public final Observable<T> throttleLast(long intervalDuration, TimeUnit unit, Scheduler scheduler)`\n3. `public final Observable<T> throttleLatest(long timeout, TimeUnit unit, Scheduler scheduler, boolean emitLast)`\n4. `public final Observable<T> throttleWithTimeout(long timeout, TimeUnit unit, Scheduler scheduler)`\n\n8.debounce \n\n`debounce`也是用来限制发射频率过快的，它仅在过了一段指定的时间还没发射数据时才发射一个数据。我们通过下面的图来说明这个问题：\n\n![debounce](https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.png)\n\n这里红、绿、蓝三个球发射出来的原因都是因为当反射了这个球之后的一定的时间内没有其他的球发射出来，这个时间是我们可以通过参数来指定的。\n\n该方法的用法与`throttle`之类的方法类似，上面也说过`throttle`那些方法底层用了`debounce`实现，所以，这里我们不再为该方法专门编写相关的测试代码。\n\n9.sample\n\n实际上`throttleLast`的实现中内部调用的就是`sample`。\n\n#### 2.1.4 组合操作\n\n1.startWith & startWithArray\n\n`startWith`方法可以用来在指定的数据源的之前插入几个数据，它的功能类似的方法有`startWithArray`，另外还有几个重载方法。这里我们给出一个基本的用法示例，下面的程序会在原始的数字流1-5的前面加上0，所以最终的输出结果是`012345`：\n\n    Observable.range(1,5).startWith(0).subscribe(System.out::print);\n\n下面是`startWith`及其几个功能相关的方法的定义：\n\n1. `public final Observable<T> startWith(Iterable<? extends T> items)`\n2. `public final Observable<T> startWith(ObservableSource<? extends T> other)`\n3. `public final Observable<T> startWith(T item)`\n4. `public final Observable<T> startWithArray(T... items)`\n\n2.merge & mergeArray\n\n`merge`可以让多个数据源的数据合并起来进行发射，当然它可能会让`merge`之后的数据交错发射。下面是一个示例，这个例子中，我们使用`merge`方法将两个`Observable`合并到了一起进行监听：\n\n    Observable.merge(Observable.range(1,5), Observable.range(6,5)).subscribe(System.out::print);\n\n鉴于`merge`方法及其功能类似的方法太多，我们这里挑选几个比较有代表性的方法，具体的可以查看RxJava的源代码：\n\n1. `public static <T> Observable<T> merge(Iterable<? extends ObservableSource<? extends T>> sources)`\n2. `public static <T> Observable<T> mergeArray(ObservableSource<? extends T>... sources)`\n3. `public static <T> Observable<T> mergeDelayError(Iterable<? extends ObservableSource<? extends T>> sources)`\n4. `public static <T> Observable<T> mergeArrayDelayError(ObservableSource<? extends T>... sources)`\n\n这里的`mergeError`方法与`merge`方法的表现一致，只是在处理由`onError`触发的错误的时候有所不同。`mergeError`方法会等待所有的数据发射完毕之后才把错误发射出来，即使多个错误被触发，该方法也只会发射出一个错误信息。而如果使用`merger`方法，那么当有错误被触发的时候，该错误会直接被抛出来，并结束发射操作。下面是该方法的一个使用的示例，这里我们主线程停顿4秒，然后所有`merge`的Observable中的一个会在线程开始的第2秒的时候触发一个错误，该错误最终会在所有的数据发射完毕之后被发射出来：\n\n    Observable.mergeDelayError(Observable.range(1,5),\n            Observable.range(1,5).repeat(2),\n            Observable.create((ObservableOnSubscribe<String>) observableEmitter -> {\n                Thread.sleep(2000);\n                observableEmitter.onError(new Exception(\"error\"));\n            })\n    ).subscribe(System.out::print, System.out::print);\n    Thread.sleep(4000);\n\n3.concat & concatArray & concatEager\n\n该方法也是用来将多个Observable拼接起来，但是它会严格按照传入的Observable的顺序进行发射，一个Observable没有发射完毕之前不会发射另一个Observable里面的数据。下面是一个程序示例，这里传入了两个Observable，会按照顺序输出`12345678910`：\n\n    Observable.concat(Observable.range(1, 5), Observable.range(6, 5)).subscribe(System.out::print);\n\n下面是该方法的定义，鉴于该方法及其重载方法太多，这里我们选择几个比较有代表性的说明：\n\n1. `public static <T> Observable<T> concat(Iterable<? extends ObservableSource<? extends T>> sources)`\n2. `public static <T> Observable<T> concatDelayError(Iterable<? extends ObservableSource<? extends T>> sources)`\n3. `public static <T> Observable<T> concatArray(ObservableSource<? extends T>... sources)`\n4. `public static <T> Observable<T> concatArrayDelayError(ObservableSource<? extends T>... sources)`\n5. `public static <T> Observable<T> concatEager(ObservableSource<? extends ObservableSource<? extends T>> sources)`\n6. `public static <T> Observable<T> concatArrayEager(ObservableSource<? extends T>... sources)`\n\n对于`concat`方法，我们之前已经介绍过它的用法；这里的`conactArray`的功能与之类似；对于`concatEager`方法，当一个观察者订阅了它的结果，那么就相当于订阅了它拼接的所有`ObservableSource`，并且会先缓存这些ObservableSource发射的数据，然后再按照顺序将它们发射出来。而对于这里的`concatDelayError`方法的作用和前面的`mergeDelayError`类似，只有当所有的数据都发射完毕才会处理异常。\n\n4.zip & zipArray & zipIterable\n\n`zip`操作用来将多个数据项进行合并，可以通过一个函数指定这些数据项的合并规则。比如下面的程序的输出结果是`6 14 24 36 50 `，显然这里的合并的规则是相同索引的两个数据的乘积。不过仔细看下这里的输出结果，可以看出，如果一个数据项指定的位置没有对应的值的时候，它是不会参与这个变换过程的：\n\n    Observable.zip(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2)\n            .subscribe(i -> System.out.print(i + \" \"));\n\n`zip`方法有多个重载的版本，同时也有功能近似的方法，这里我们挑选有代表性的几个进行说明：\n\n1. `public static <T, R> Observable<R> zip(Iterable<? extends ObservableSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper)`\n2. `ublic static <T, R> Observable<R> zipArray(Function<? super Object[], ? extends R> zipper, boolean delayError, int bufferSize, ObservableSource... sources)`\n3. `public static <T, R> Observable<R> zipIterable(Iterable<? extends ObservableSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper, boolean delayError, int bufferSize)`\n\n实际上上面几个方法的用法和功能基本类似，区别在于传入的`ObservableSource`的参数的形式。\n\n5.combineLastest\n\n与`zip`操作类似，但是这个操作的输出结果与`zip`截然不同，以下面的程序为例，它的输出结果是`36 42 48 54 60`：\n\n    Observable.combineLatest(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2)\n            .subscribe(i -> System.out.print(i + \" \"));\n\n利用下面的这张图可以比较容易来说明这个问题：\n\n![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)\n\n上图中的上面的两条横线代表用于拼接的两个数据项，下面的一条横线是拼接之后的结果。`combineLatest`的作用是拼接最新发射的两个数据。下面我们用上图的过程来说明该方法是如何执行的：开始第一条只有1的时候无法拼接，；当第二条出现A的时候，此时最新的数据是1和A，故组合成一个1A；第二个数据项发射了B，此时最新的数据是1和B，故组合成1B；第一条横线发射了2，此时最新的数据是2和B，因此得到了2B，依次类推。然后再回到我们上面的问题，第一个数据项连续发射了5个数据的时候，第二个数据项一个都没有发射出来，因此没有任何输出；然后第二个数据项开始发射数据，当第二个数据项发射了6的时候，此时最新的数据组合是6和6，故得36；然后，第二个数据项发射了7，此时最新的数据组合是6和7，故得42，依次类推。\n\n该方法也有对应的`combineLatestDelayError`方法，用途也是只有当所有的数据都发射完毕的时候才去处理错误逻辑。\n\n#### 2.1.5 辅助操作\n\n1.delay\n\n`delay`方法用于在发射数据之前停顿指定的时间，比如下面的程序会在真正地发射数据之前停顿1秒：\n\n    Observable.range(1, 5).delay(1000, TimeUnit.MILLISECONDS).subscribe(System.out::print);\n    Thread.sleep(1500);\n\n同样`delay`方法也有几个重载的方法，可以供我们用来指定触发的线程等信息，这里给出其中的两个，其他的可以参考源码和文档：\n\n1. `public final Observable<T> delay(long delay, TimeUnit unit)`\n2. `public final Observable<T> delay(long delay, TimeUnit unit, Scheduler scheduler)`\n\n2.do系列\n\nRxJava中还有一系列的方法可以供我们使用，它们共同的特点是都是以`do`开头，下面我们列举一下这些方法并简要说明一下它们各自的用途：\n\n1. `public final Observable<T> doAfterNext(Consumer<? super T> onAfterNext)`，会在`onNext`方法之后触发；\n2. `public final Observable<T> doAfterTerminate(Action onFinally)`，会在Observable终止之后触发；\n3. `public final Observable<T> doFinally(Action onFinally)`，当`onComplete`或者`onError`的时候触发；\n4. `public final Observable<T> doOnDispose(Action onDispose)`，当被dispose的时候触发；\n5. `public final Observable<T> doOnComplete(Action onComplete)`，当complete的时候触发；\n6. `public final Observable<T> doOnEach(final Observer<? super T> observer)`，当每个`onNext`调用的时候触发；\n7. `public final Observable<T> doOnError(Consumer<? super Throwable> onError)`，当调用`onError`的时候触发；\n8. `public final Observable<T> doOnLifecycle(final Consumer<? super Disposable> onSubscribe, final Action onDispose)`\n9. `public final Observable<T> doOnNext(Consumer<? super T> onNext)`，，会在`onNext`的时候触发；\n9. `public final Observable<T> doOnSubscribe(Consumer<? super Disposable> onSubscribe)`，会在订阅的时候触发；\n10. `public final Observable<T> doOnTerminate(final Action onTerminate)`，当终止之前触发。\n\n这些方法可以看作是对操作执行过程的一个监听，当指定的操作被触发的时候会同时触发这些监听方法：\n\n    Observable.range(1, 5)\n            .doOnEach(integerNotification -> System.out.println(\"Each : \" + integerNotification.getValue()))\n            .doOnComplete(() -> System.out.println(\"complete\"))\n            .doFinally(() -> System.out.println(\"finally\"))\n            .doAfterNext(i -> System.out.println(\"after next : \" + i))\n            .doOnSubscribe(disposable -> System.out.println(\"subscribe\"))\n            .doOnTerminate(() -> System.out.println(\"terminal\"))\n            .subscribe(i -> System.out.println(\"subscribe : \" + i));\n\n3.subscribeOn & observeOn\n\n`subscribeOn`用于指定Observable自身运行的线程，`observeOn`用于指定发射数据所处的线程，比如Android中的异步任务需要用`observeOn`指定发射数据所在的线程是非主线程，然后执行完毕之后将结果发送给主线程，就需要用`subscribeOn`来指定。比如下面的程序，我们用这两个方法来指定所在的线程：\n\n     Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {\n        System.out.println(Thread.currentThread());\n        observableEmitter.onNext(0);\n    }).observeOn(Schedulers.newThread()).subscribeOn(Schedulers.computation())\n            .subscribe(integer -> System.out.println(Thread.currentThread()));\n\n最终的输出结果如下所示：\n\n    Thread[RxComputationThreadPool-1,5,main]\n    Thread[RxNewThreadScheduler-1,5,main]\n\n4.timeout\n\n用来设置一个超时时间，如果指定的时间之内没有任何数据被发射出来，那么就会执行我们指定的数据项。如下面的程序所示，我们先为设置了一个间隔200毫秒的数字产生器，开始发射数据之前要停顿1秒钟，因为我们设置的超时时间是500毫秒，因而在第500毫秒的时候会执行我们传入的数据项：\n\n    Observable.interval(1000, 200, TimeUnit.MILLISECONDS)\n            .timeout(500, TimeUnit.MILLISECONDS, Observable.rangeLong(1, 5))\n            .subscribe(System.out::print);\n    Thread.sleep(2000);\n\n`timeout`方法有多个重载方法，可以为其指定线程等参数，可以参考源码或者文档了解详情。\n\n#### 2.1.6 错误处理操作符\n\n错误处理操作符主要用来提供给Observable，用来对错误信息做统一的处理，常用的两个是`catch`和`retry`。\n\n1.catch\n\ncatch操作会拦截原始的Observable的`onError`通知，将它替换为其他数据项或者数据序列，让产生的Observable能够正常终止或者根本不终止。在RxJava中该操作有3终类型：\n\n1. `onErrorReturn`：这种操作会在onError触发的时候返回一个特殊的项替换错误，并调用观察者的onCompleted方法，而不会将错误传递给观察者；\n2. `onErrorResumeNext`：会在onError触发的时候发射备用的数据项给观察者；\n3. `onExceptionResumeNext`：如果onError触发的时候onError收到的Throwable不是Exception，它会将错误传递给观察者的onError方法，不会使用备用的Observable。\n\n下面是`onErrorReturn`和`onErrorResumeNext`的程序示例，这里第一段代码会在出现错误的时候输出`666`，而第二段会在出现错误的时候发射数字`12345`：\n\n        Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {\n            observableEmitter.onError(null);\n            observableEmitter.onNext(0);\n        }).onErrorReturn(throwable -> 666).subscribe(System.out::print);\n\n        Observable.create((ObservableOnSubscribe<Integer>) observableEmitter -> {\n            observableEmitter.onError(null);\n            observableEmitter.onNext(0);\n        }).onErrorResumeNext(Observable.range(1,5)).subscribe(System.out::print);\n\n2.retry\n\n`retry`使用了一种错误重试机制，它可以在出现错误的时候进行重试，我们可以通过参数指定重试机制的条件。以下面的程序为例，这里我们设置了当出现错误的时候会进行2次重试，因此，第一次的时候出现错误会调用`onNext`，重试2次又会调用2次`onNext`，第二次重试的时候因为重试又出现了错误，因此此时会触发`onError`方法。也就是说，下面这段代码会触发`onNext`3次，触发`onError()`1次：\n\n        Observable.create(((ObservableOnSubscribe<Integer>) emitter -> {\n            emitter.onNext(0);\n            emitter.onError(new Throwable(\"Error1\"));\n            emitter.onError(new Throwable(\"Error2\"));\n        })).retry(2).subscribe(i -> System.out.println(\"onNext : \" + i), error -> System.out.print(\"onError : \" + error));\n\n`retry`有几个重载的方法和功能相近的方法，下面是这些方法的定义（选取部分）：\n\n1. `public final Observable<T> retry()`：会进行无限次地重试；\n2. `public final Observable<T> retry(BiPredicate<? super Integer, ? super Throwable> predicate)`\n3. `public final Observable<T> retry(long times)`：指定重试次数；\n4. `public final Observable<T> retry(long times, Predicate<? super Throwable> predicate) `\n5. `public final Observable<T> retryUntil(final BooleanSupplier stop)`\n6. `public final Observable<T> retryWhen(Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler)`\n\n#### 2.1.7 条件操作符和布尔操作符\n\n1.all & any\n\n1. `all`用来判断指定的数据项是否全部满足指定的要求，这里的“要求”可以使用一个函数来指定；\n2. `any`用来判断指定的Observable是否存在满足指定要求的数据项。\n\n在下面的程序中，我们用该函数来判断指定的数据项是否全部满足大于5的要求，显然是不满足的，因此下面的程序将会输出`false`：\n\n    Observable.range(5, 5).all(i -> i>5).subscribe(System.out::println); // false\n    Observable.range(5, 5).any(i -> i>5).subscribe(System.out::println); // true\n\n以下是该方法的定义：\n\n1. `public final Single<Boolean> all(Predicate<? super T> predicate)`\n2. `public final Single<Boolean> any(Predicate<? super T> predicate)`\n\n2.contains & isEmpty\n\n这两个方法分别用来判断数据项中是否包含我们指定的数据项，已经判断数据项是否为空：\n\n    Observable.range(5, 5).contains(4).subscribe(System.out::println); // false\n    Observable.range(5, 5).isEmpty().subscribe(System.out::println); // false\n\n以下是这两个方法的定义：\n\n1. `public final Single<Boolean> isEmpty()`\n2. `public final Single<Boolean> contains(final Object element)`\n\n3.sequenceEqual\n\n`sequenceEqual`用来判断两个Observable发射出的序列是否是相等的。比如下面的方法用来判断两个序列是否相等：\n\n    Observable.sequenceEqual(Observable.range(1,5), Observable.range(1, 5)).subscribe(System.out::println);\n\n4.amb\n\n`amb`作用的两个或多个Observable，但是只会发射最先发射数据的那个Observable的全部数据：\n\n    Observable.amb(Arrays.asList(Observable.range(1, 5), Observable.range(6, 5))).subscribe(System.out::print)\n\n该方法及其功能近似的方法的定义，这里前两个是静态的方法，第二个属于实例方法：\n\n1. `public static <T> Observable<T> amb(Iterable<? extends ObservableSource<? extends T>> sources)`\n2. `public static <T> Observable<T> ambArray(ObservableSource<? extends T>... sources)`\n3. `public final Observable<T> ambWith(ObservableSource<? extends T> other)`\n\n5.defaultIfEmpty\n\n`defaultIfEmpty`用来当指定的序列为空的时候指定一个用于发射的值。下面的程序中，我们直接调用发射器的`onComplete`方法，因此序列是空的，结果输出一个整数`6`：\n\n    Observable.create((ObservableOnSubscribe<Integer>) Emitter::onComplete).defaultIfEmpty(6).subscribe(System.out::print);\n\n下面是该方法的定义：\n\n1. `public final Observable<T> defaultIfEmpty(T defaultItem)`\n\n#### 2.1.8 转换操作符\n\n1.toList & toSortedList\n\n`toList`和`toSortedList`用于将序列转换成列表，后者相对于前者增加了排序的功能：\n\n    Observable.range(1, 5).toList().subscribe(System.out::println);\n    Observable.range(1, 5).toSortedList(Comparator.comparingInt(o -> -o)).subscribe(System.out::println);\n\n下面是它们的定义，它们有多个重载版本，这里选择其中的两个进行说明：\n\n1. `public final Single<List<T>> toList()`\n2. `public final Single<List<T>> toSortedList(final Comparator<? super T> comparator)`\n\n注意一下，这里的返回结果是`Single`类型的，不过这并不妨碍我们继续使用链式操作，因为`Single`的方法和`Observable`基本一致。\n另外还要注意这里的`Single`中的参数是一个`List<T>`，也就是说，它把整个序列转换成了一个列表对象。因此，上面的两个示例程序的输出是：\n\n    [1, 2, 3, 4, 5]\n    [5, 4, 3, 2, 1]\n\n2.toMap & toMultimap\n\n`toMap`用于将发射的数据转换成另一个类型的值，它的转换过程是针对每一个数据项的。以下面的代码为例，它会将原始的序列中的每个数字转换成对应的十六进制。但是，`toMap`转换的结果不一定是按照原始的序列的发射的顺序来的：\n\n    Observable.range(8, 10).toMap(Integer::toHexString).subscribe(System.out::print);\n\n与`toMap`近似的是`toMultimap`方法，它可以将原始序列的每个数据项转换成一个集合类型：\n\n    Observable.range(8, 10).toMultimap(Integer::toHexString).subscribe(System.out::print);\n\n上面的两段程序的输出结果是：\n\n    {11=17, a=10, b=11, c=12, d=13, e=14, f=15, 8=8, 9=9, 10=16}\n    {11=[17], a=[10], b=[11], c=[12], d=[13], e=[14], f=[15], 8=[8], 9=[9], 10=[16]}\n\n上面的两个方法的定义是（多个重载，选择部分）：\n\n1. `public final <K> Single<Map<K, T>> toMap(final Function<? super T, ? extends K> keySelector)`\n2. `public final <K> Single<Map<K, Collection<T>>> toMultimap(Function<? super T, ? extends K> keySelector)`\n\n3.toFlowable\n\n该方法用于将一个Observable转换成Flowable类型，下面是该方法的定义，显然这个方法使用了策略模式，这里面涉及背压相关的内容，我们后续再详细介绍。\n\n    public final Flowable<T> toFlowable(BackpressureStrategy strategy)\n\n4.to\n\n相比于上面的方法，`to`方法的限制更加得宽泛，你可以将指定的Observable转换成任意你想要的类型（如果你可以做到的话），下面是一个示例代码，用来将指定的整数序列转换成另一个整数类型的Observable，只不过这里的每个数据项都是原来的列表中的数据总数的值：\n\n    Observable.range(1, 5).to(Observable::count).subscribe(System.out::println);\n\n下面是该方法的定义：\n\n`public final <R> R to(Function<? super Observable<T>, R> converter)`\n\n### 2.2 线程控制\n\n之前有提到过RxJava的线程控制是通过`subscribeOn`和`observeOn`两个方法来完成的。\n这里我们梳理一下RxJava提供的几种线程调度器以及RxAndroid为Android提供的调度器的使用场景和区别等。\n\n1. `Schedulers.io()`：代表适用于io操作的调度器，增长或缩减来自适应的线程池，通常用于网络、读写文件等io密集型的操作。重点需要注意的是线程池是无限制的，大量的I/O调度操作将创建许多个线程并占用内存。\n2. `Schedulers.computation()`：计算工作默认的调度器，代表CPU计算密集型的操作，与I/O操作无关。它也是许多RxJava方法，比如`buffer()`,`debounce()`,`delay()`,`interval()`,`sample()`,`skip()`，的默认调度器。\n3. `Schedulers.newThread()`：代表一个常规的新线程。\n4. `Schedulers.immediate()`：这个调度器允许你立即在当前线程执行你指定的工作。它是`timeout()`,`timeInterval()`以及`timestamp()`方法默认的调度器。\n5. `Schedulers.trampoline()`：当我们想在当前线程执行一个任务时，并不是立即，我们可以用`trampoline()`将它入队。这个调度器将会处理它的队列并且按序运行队列中每一个任务。它是`repeat()`和`retry()`方法默认的调度器。\n\n以及RxAndroid提供的线程调度器：\n\n`AndroidSchedulers.mainThread()`用来指代Android的主线程\n\n### 2.3 总结\n\n上面的这些操作也基本适用于`Flowable`、`Single`、`Completable`和`Maybe`。\n\n我们花费了很多的时间和精力来梳理了这些方法，按照上面的内容，使用RxJava实现一些基本的或者高级的操作都不是什么问题。\n\n但是，Observable更适用于处理一些数据规模较小的问题，当数据规模比较多的时候可能会出现`MissingBackpressureException`异常。\n因此，我们还需要了解背压和`Flowable`的相关内容才能更好地理解和应用RxJava.\n"
  },
  {
    "path": "响应式编程/RxJava系列（2）：Flowable和背压.md",
    "content": "# RxJava2 系列 （2）：背压和Flowable\n\n背压(Back Pressure)的概念最初并不是在响应式编程中提出的，它最初用在流体力学中，指的是后端的压力，\n通常用于描述系统排出的流体在出口处或二次侧受到的与流动方向相反的压力。\n\n在响应式编程中，我们可以将产生信息的部分叫做上游或者叫生产者，处理产生的信息的部分叫做下游或者消费者。\n试想如果在异步的环境中，生产者的生产速度大于消费者的消费速度的时候，明显会出现生产过剩的情景，这时候就需要消费者对多余的数据进行缓存，\n但如果生产的信息数量过多，以至于超出缓存大小，就会出现缓存溢出，甚至可能造成内存耗尽。\n\n我们可以制定一个数据丢失的规则，来丢失那些“可以丢失的数据”，以减轻缓存的压力。\n在之前我们介绍了一些方法，比如`throttleXXX`、`debounce`、`sample`等，都是用来解决在生产速度过快的情况下的数据过滤的，它们指定了数据取舍的规则。\n而在`Flowable`，我们可以通过`onBackpressureXXX`一系列的方法来制定当数据生产过快情况下的数据取舍的规则，\n\n我们可以把这种处理方式理解成背压，所谓背压，在Rx中就是通过一种下游用来控制上游事件发射频率的机制（就像流体在出口受到了阻力一样）。\n所以，如何理解背压呢？笔者认为，在力学中它是一种现象，在Rx中它是一种机制。\n\n在这篇文章中，我们会先介绍背压的相关内容，然后我们再介绍一下`onBackpressureXXX`系列的方法。\n\n关于RxJava2的基础使用和方法梳理可以参考：[RxJava2 系列 （1）：一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd)\n\n说明：以下文章部分翻译自RxJava官方文档[Backpressure (2.0)](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0))。\n\n## 1、背压机制\n\n如果将生产和消费整体看作一个管道，生成看作上游，消费看作下游；\n那么当异步的应用场景下，当生产者生产过快而消费者消费很慢的时候，可以通过背压来告知上游减慢生成的速度。\n\n通常在进行异步的操作的时候会通过缓存来存储发射出的数据。在早期的RxJava中，这些缓存是无界的。\n这意味着当需要缓存的数据非常多的时候，它们可能会占用非常多的存储空间，并有可能因为虚拟机不断GC而导致程序执行过慢，甚至直接抛出OOM。\n在最新的RxJava中，大多数的异步操作内部都存在一个有界的缓存，当超出这个缓存的时候就会抛出`MissingBackpressureException`异常并结束整个序列。\n\n然而，某些情况下的表现会有所不同，它们不会抛出`MissingBackpressureException`异常。比如下面的`range`操作：\n\n    private static void compute(int i) throws InterruptedException {\n        Thread.sleep(500);\n        System.out.println(\"computing : \" + i);\n    }\n\n    private static void testFlowable() throws InterruptedException {\n        Flowable.range(1, MAX_LENGTH).observeOn(Schedulers.computation()).subscribe(FlowableTest::compute);\n\n        Thread.sleep(500 * MAX_LENGTH);\n    }\n\n在这段代码中我们生成一段整数，然后每隔500毫秒执行依次计算操作。从输出的结果来看，在程序的实际执行过程中，数据的发射是串行的。\n也就是发射完一个数据之后进入`compute`进行计算，等待500毫秒之后才发射下一个。\n因此，在程序的执行过程中没有抛出异常，也没有过多的内存消耗。\n\n而下面的这段代码就会在程序运行的时候立刻抛出`MissingBackpressureException`异常：\n\n    PublishProcessor<Integer> source = PublishProcessor.create();\n    source.observeOn(Schedulers.computation()).subscribe(v -> compute(v), Throwable::printStackTrace);\n    for (int i = 0; i < 1_000_000; i++) source.onNext(i);\n    Thread.sleep(10_000);\n\n这是因为`PublishProcessor`底层会调用`PublishSubscription`，而后者实现了`AtomicLong`，它会通过判断引用的long是否为0来抛出异常，这个long型整数会在调用`PublishSubscription.request()`的时候被改写。前面的一个例子的原理就是当每次调用了观察者的`onNext`之后会调用`PublishSubscription.request()`来请求数据，这样相当于消费者会在消费完事件之后向生产者请求，因此整个序列的执行看上去是串行的，从而不会抛出异常。\n\n## 2、onBackpressureXXX\n\n大多数开发者在遇到`MissingBackpressureException`通常是因为使用`observeOn`方法监听了非背压的`PublishProcessor`, `timer()`， `interval()`或者自定义的`create()`。我们有以下几种方式来解决这个问题:\n\n### 2.1 增加缓存大小\n\n`observeOn`方法的默认缓存大小是16，当生产的速率过快的时候，那么可能很快会超出该缓存大小，从而导致缓存溢出。\n一种简单的解决办法是通过提升该缓存的大小来防止缓存溢出，我们可以使用`observeOn`的重载方法来设置缓存的大小。比如：\n\n    PublishProcessor<Integer> source = PublishProcessor.create();\n    source.observeOn(Schedulers.computation(), 1024 * 1024)\n          .subscribe(e -> { }, Throwable::printStackTrace);\n\n但是这种解决方案只能解决暂时的问题，当生产的速率过快的时候还是有可能造成缓存溢出，所以这不是根本的解决办法。\n\n### 2.2 通过丢弃和过滤来减轻缓存压力\n\n我们可以根据自己的应用的场景和数据的重要性，选择使用一些方法来过滤和丢弃数据。\n比如，丢弃的方式可以选择`throttleFirst`, `throttleLast`, `throttleWithTimeout`等，还可以使用按照时间采样的方式来减少接受的数据。\n\n    PublishProcessor<Integer> source = PublishProcessor.create();\n    source.sample(1, TimeUnit.MILLISECONDS)\n          .observeOn(Schedulers.computation(), 1024)\n          .subscribe(v -> compute(v), Throwable::printStackTrace);\n    \n但是，这种方式仅仅用来减少下游接收的数据，当缓存的数据不断增加的时候还是有可能导致缓存溢出，所以，这也不是一种根本的解决办法。\n\n### 2.3 onBackpressureBuffer()\n\n这种无参的方法会使用一个无界的缓存，只要虚拟机没有抛出OOM异常，它就会把所有的数据缓存起来。\n\n     Flowable.range(1, 1_000_000)\n               .onBackpressureBuffer()\n               .observeOn(Schedulers.computation(), 8)\n               .subscribe(e -> { }, Throwable::printStackTrace);\n\n上面的例子即使使用了很小的缓存也不会有异常抛出，因为`onBackpressureBuffer`会将发射的所有数据缓存起来，只会将一小部分的数据传递给`observeOn`。\n\n这种处理方式实际上是不存在背压的，因为`onBackpressureBuffer`缓存了所有的数据，我们可以使用该方法的4个重载方法来对背压进行个性化设置。\n\n### 2.4 onBackpressureBuffer(int capacity)\n\n这个方法使用一个有界的缓存，当达到了缓存大小的时候会抛出一个`BufferOverflowError`错误。\n通过这种方法可以增加默认的缓存大小，但是通过`observeOn`方法一样可以指定缓存的大小，因此，这个方法的应用变得越来越少。\n\n### 2.5 onBackpressureBuffer(int capacity, Action onOverflow)\n\n这方法除了可以指定一个有界的缓存还提供了一个，当缓存溢出的时候还会回调指定的Action。\n但是这种回调的用途比较有限，因为它除了提供当前回调的栈信息以外提供不了任何有用的信息。\n\n### 2.6 onBackpressureBuffer(int capacity, Action onOverflow, BackpressureOverflowStrategy strategy)\n\n这个重载方法相对比较实用一些，它除了上面的那些功能之外，还指定了当缓存到达指定的缓存时的行为。\n这里的`BackpressureOverflowStrategy`顾名思义是一个策略，它是一个枚举类型，预定义了三种枚举值，最终会在`FlowableOnBackpressureBufferStrategy`中根据指定的枚举类型选择不同的实现策略，因此，我们可以使用它来指定缓存溢出时候的行为。\n\n下面是该枚举类型的三个值及其含义：\n\n1. `ERROR`：当缓存溢出的时候会抛出一个异常；\n2. `DROP_OLDEST`：当缓存发生溢出的时候，会丢弃最老的值，并将新的值插入到缓存中；\n3. `DROP_LATEST`：当缓存发生溢出的时候，最新的值会被忽略，只有比较老的值会被传递给下游使用；\n\n需要注意的地方是，后面的两种策略会造成下游获取到的值是不连续的，因为有一部分值会因为缓存不够被丢弃，但是它们不会抛出`BufferOverflowException`。\n\n### 2.7 onBackpressureDrop()\n\n这个方法会在数据达到缓存大小的时候丢弃最新的数据。可以将其看成是`onBackpressureBuffer`+`0 capacity`+`DROP_LATEST`的组合。\n\n这个方法特别适用于那种可以忽略从源中发射出值的那种场景，比如GPS定位问题，定位数据会不断发射出来，即使丢失当前数据，等会儿一样能拿到最新的数据。\n\n    component.mouseMoves()\n        .onBackpressureDrop()\n        .observeOn(Schedulers.computation(), 1)\n        .subscribe(event -> compute(event.x, event.y));\n\n该方法还存在一个重载方法`onBackpressureDrop(Consumer<? super T> onDrop)`，它允许我们传入一个接口来指定当某个数据被丢失时的行为。\n\n### 2.8 onBackpressureLatest()\n\n对应于`onBackpressureDrop()`的，还有`onBackpressureLatest()`方法，该方法只会保留最新的数据并会覆盖较老、没有分发的数据。\n我们可以将其看成是`onBackpressureBuffer`+`1 capacity`+`DROP_OLDEST`的组合。\n\n与`onBackpressureDrop()`不同的地方在于，当下游消费过慢的时候，这种方式总会存在一个缓存的值。\n这种特别适用于那种数据的生产非常频繁，但是只有最新的数据会被消费的那种情形。比如，当用户点击了屏幕，那么我们倾向于只处理最新按下的位置的事件。\n\n    component.mouseClicks()\n        .onBackpressureLatest()\n        .observeOn(Schedulers.computation())\n        .subscribe(event -> compute(event.x, event.y), Throwable::printStackTrace);\n\n所以，总结一下：\n\n1. `onBackpressureDrop()`：不会缓存任何数据，专注于当下，新来的数据来不及处理就丢掉，以后会有更好的；\n2. `onBackpressureLatest()`：会缓存一个数据，当正在执行某个任务的时候有新的数据过来，会把它缓存起来，如果又有新的数据过来，那就把之前的替换掉，缓存里面的总是最新的。\n\n## 3、总结\n\n以上就是背压机制的一些内容，以及我们介绍了`Flowable`中的几个背压相关的方法。\n实际上，RxJava的官方文档也有说明——`Flowable`适用于数据量比较大的情景，因为它的一些创建方法本身就使用了背压机制。\n这部分方法我们就不再一一进行说明，因为，它们的方法签名和`Observable`基本一致，只是多了一层背压机制。\n\n比较匆匆地整理完了背压的内容，但是我想这块还会有更加丰富的内容值得我们去发现和探索。\n\n以上。"
  },
  {
    "path": "响应式编程/RxJava系列（3）：用RxJava打造EventBus.md",
    "content": "# RxJava2 系列 （3）：使用 Subject\n\n在这篇文章中，我们会先分析一下 RxJava2 中的 Subject ；然后，我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。\n\n在了解本篇文章的内容之前，你需要先了解 RxJava2 中的一些基本的用法，比如 Observable 以及背压的概念，你可以参考我的其他两篇文章来获取这部分内容：[《RxJava2 系列 （1）：一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd)和[《RxJava2 系列 （2）：背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187)。\n\n## 1、Subject\n\n### 1.1 Subject 的两个特性\n\nSubject 可以同时代表 Observer 和 Observable，允许从数据源中多次发送结果给多个观察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外，所有的方法都是线程安全的。此外，你还可以使用 toSerialized() 方法，也就是转换成串行的，将这些方法设置成线程安全的。\n\n如果你已经了解了 Observable 和 Observer ，那么也许直接看 Subject 的源码定义会更容易理解：\n\n```\npublic abstract class Subject<T> extends Observable<T> implements Observer<T> {\n\n    // ...\n}\n```\n\n从上面看出，Subject 同时继承了 Observable 和 Observer 两个接口，说明它既是被观察的对象，同时又是观察对象，也就是可以生产、可以消费、也可以自己生产自己消费。所以，我们可以项下面这样来使用它。这里我们用到的是该接口的一个实现 PublishSubject ：\n\n    public static void main(String...args) {\n        PublishSubject<Integer> subject = PublishSubject.create();\n        subject.subscribe(System.out::println);\n\n        Executor executor = Executors.newFixedThreadPool(5);\n        Disposable disposable = Observable.range(1, 5).subscribe(i ->\n                executor.execute(() -> {\n                    try {\n                        Thread.sleep(i * 200);\n                        subject.onNext(i);\n                    } catch (InterruptedException e) {\n                        e.printStackTrace();\n                    }\n                }));\n    }\n\n根据程序的执行结果，程序在第200, 400, 600, 800, 1000毫秒依次输出了1到5的数字。\n\n在这里，我们用 PublishSubject 创建了一个**主题**并对其监听，然后在线程当中又通知该主题内容变化，整个过程我们都只操作了 PublishSubject 一个对象。显然，使用 Subject 我们可以达到对一个指定类型的值的结果进行监听的目的——我们把值改变之后对应的逻辑写在 subscribe() 方法中，然后每次调用 onNext() 等方法通知结果之后就可以自动调用 subscribe() 方法进行更新操作。\n\n同时，因为 Subject 实现了 Observer 接口，并且在 Observable 等的 subscribe() 方法中存在一个以 Observer 作为参数的方法（如下），所以，Subject 也是可以作为消费者来对事件进行消费的。\n\n    public final void subscribe(Observer<? super T> observer) \n\n以上就是 Subject 的两个主要的特性。\n\n### 1.2 Subject 的实现类\n\n在 RxJava2 ，Subject 有几个默认的实现，下面我们对它们之间的区别做简单的说明：\n\n1. `AsyncSubject`:只有当 Subject 调用 onComplete 方法时，才会将 Subject 中的**最后一个事件**传递给所有的 Observer。\n2. `BehaviorSubject`:该类有创建时需要一个默认参数，该默认参数会在 Subject 未发送过其他的事件时，向注册的 Observer 发送；新注册的 Observer 不会收到之前发送的事件，这点和 PublishSubject 一致。\n3. `PublishSubject`:不会改变事件的发送顺序；在已经发送了一部分事件之后注册的 Observer 不会收到之前发送的事件。\n4. `ReplaySubject`:无论什么时候注册 Observer 都可以接收到任何时候通过该 Observable 发射的事件。\n5. `UnicastSubject`:只允许一个 Observer 进行监听，在该 Observer 注册之前会将发射的所有的事件放进一个队列中，并在 Observer 注册的时候一起通知给它。\n\n对比 PublishSubject 和 ReplaySubject，它们的区别在于新注册的 Observer 是否能够收到在它注册之前发送的事件。这个类似于 EventBus 中的 StickyEvent 即黏性事件，为了说明这一点，我们准备了下面两段代码：\n\n    private static void testPublishSubject() throws InterruptedException {\n        PublishSubject<Integer> subject = PublishSubject.create();\n        subject.subscribe(i -> System.out.print(\"(1: \" + i + \") \"));\n\n        Executor executor = Executors.newFixedThreadPool(5);\n        Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {\n            try {\n                Thread.sleep(i * 200);\n                subject.onNext(i);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }));\n\n        Thread.sleep(500);\n        subject.subscribe(i -> System.out.print(\"(2: \" + i + \") \"));\n\n        Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());\n    }\n\n    private static void testReplaySubject() throws InterruptedException {\n        ReplaySubject<Integer> subject = ReplaySubject.create();\n        subject.subscribe(i -> System.out.print(\"(1: \" + i + \") \"));\n\n        Executor executor = Executors.newFixedThreadPool(5);\n        Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> {\n            try {\n                Thread.sleep(i * 200);\n                subject.onNext(i);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }));\n\n        Thread.sleep(500);\n        subject.subscribe(i -> System.out.print(\"(2: \" + i + \") \"));\n\n        Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown());\n    }\n\n它们的输出结果依次是\n\n    PublishSubject的结果：(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)\n    ReplaySubject的结果： (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5)\n\n从上面的结果对比中，我们可以看出前者与后者的区别在于新注册的 Observer 并没有收到在它注册之前发送的事件。试验的结果与上面的叙述是一致的。\n\n其他的测试代码这不一并给出了，详细的代码可以参考[Github - Java Advanced](https://github.com/Shouheng88/Java-advanced)。\n\n## 2、用 RxJava 打造 EventBus\n\n### 2.1 打造 EventBus\n\n清楚了 Subject 的概念之后，让我们来做一个实践——用 RxJava 打造 EventBus。\n\n我们先考虑用一个全局的 PublishSubject 来解决这个问题，当然，这意味着我们发送的事件不是黏性事件。不过，没关系，只要这种实现方式搞懂了，用 ReplaySubject 做一个发送黏性事件的 EventBus 也非难事。\n\n考虑一下，如果要实现这个功能我们需要做哪些准备：\n\n1. **我们需要发送事件并能够正确地接收到事件。**要实现这个目的并不难，因为 Subject 本身就具有发送和接收两个能力，作为全局的之后就具有了全局的注册和通知的能力。因此，不论你在什么位置发送了事件，任何订阅的地方都能收到该事件。\n2. **首先，我们要在合适的位置对事件进行监听，并在合适的位置取消事件的监听。如果我们没有在适当的时机释放事件，会不会造成内存泄漏呢？这还是有可能的。**所以，我们需要对注册监听的观察者进行记录，并提供注册和取消注册的方法，给它们在指定的生命周期中进行调用。\n\n好了，首先是全局的 Subject 的问题，我们可以实现一个静态的或者单例的 Subject。这里我们选择使用后者，所以，我们需要一个单例的方式来使用 Subject：\n\npublic class RxBus {\n\n    private static volatile RxBus rxBus;\n\n    private final Subject<Object> subject = PublishSubject.create().toSerialized();\n\n    public static RxBus getRxBus() {\n        if (rxBus == null) {\n            synchronized (RxBus.class) {\n                if(rxBus == null) {\n                    rxBus = new RxBus();\n                }\n            }\n        }\n        return rxBus;\n    }\n}\n\n这里我们应用了 DCL 的单例模式提供一个单例的 RxBus，对应一个唯一的 Subject. 这里我们用到了 Subject 的`toSerialized()`，我们上面已经提到过它的作用，就是用来保证 onNext() 等方法的线程安全性。\n\n另外，因为 Observalbe 本身是不支持背压的，所以，我们还需要将该 Observable 转换成 Flowable 来实现背压的效果：\n\n    public <T> Flowable<T> getObservable(Class<T> type){\n        return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type);\n    }\n\n这里我们用到的背压的策略是`BackpressureStrategy.BUFFER`，它会缓存发射结果，直到有消费者订阅了它。而这里的`ofType()`方法的作用是用来过滤发射的事件的类型，只有指定类型的事件会被发布。\n\n然后，我们需要记录订阅者的信息以便在适当的时机取消订阅，这里我们用一个`Map<String, CompositeDisposable>`类型的哈希表来解决。这里的`CompositeDisposable`用来存储 Disposable，从而达到一个订阅者对应多个 Disposable 的目的。`CompositeDisposable`是一个 Disposable 的容器，声称可以达到 O(1) 的增、删的复杂度。这里的做法目的是使用注册观察之后的 Disposable 的 dispose() 方法来取消订阅。所以，我们可以得到下面的这段代码：\n\n    public void addSubscription(Object o, Disposable disposable) {\n        String key = o.getClass().getName();\n        if (disposableMap.get(key) != null) {\n            disposableMap.get(key).add(disposable);\n        } else {\n            CompositeDisposable disposables = new CompositeDisposable();\n            disposables.add(disposable);\n            disposableMap.put(key, disposables);\n        }\n    }\n\n    public void unSubscribe(Object o) {\n        String key = o.getClass().getName();\n        if (!disposableMap.containsKey(key)) {\n            return;\n        }\n        if (disposableMap.get(key) != null) {\n            disposableMap.get(key).dispose();\n        }\n        disposableMap.remove(key);\n    }\n\n最后，对外提供一下 Subject 的订阅和发布方法，整个 EventBus 就制作完成了：\n\n    public void post(Object o){\n        subject.onNext(o);\n    }\n\n    public <T> Disposable doSubscribe(Class<T> type, Consumer<T> next, Consumer<Throwable> error){\n        return getObservable(type)\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(next,error);\n    }\n\n### 2.2 测试效果\n\n我们只需要在最顶层的 Activity 基类中加入如下的代码。这样，我们就不需要在各个 Activity 中取消注册了。然后，就可以使用这些顶层的方法来进行操作了。\n\n    protected void postEvent(Object object) {\n        RxBus.getRxBus().post(object);\n    }\n\n    protected <M> void addSubscription(Class<M> eventType, Consumer<M> action) {\n        Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d);\n        RxBus.getRxBus().addSubscription(this, disposable);\n    }\n\n    protected <M> void addSubscription(Class<M> eventType, Consumer<M> action, Consumer<Throwable> error) {\n        Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error);\n        RxBus.getRxBus().addSubscription(this, disposable);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        RxBus.getRxBus().unSubscribe(this);\n    }\n\n在第一个 Activity 中我们对指定的类型的结果进行监听：\n\n    addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message));\n\n然后，我们在另一个 Activity 中发布事件：\n\n    postEvent(new RxMessage(\"Hello world!\"));\n\n这样当第二个 Activity 中调用指定的发送事件的方法之后，第一个 Activity 就可以接收到发射的事件了。\n\n## 总结\n\n好了，以上就是 Subject 的使用，如果要用一个词来形容它的话，那么只能是“自给自足”了。就是说，它同时做了 Observable 和 Observer 的工作，既可以发射事件又可以对事件进行消费，可谓身兼数职。它在那种想要对某个值进行监听并处理的情形特别有用。因为它不需要你写多个冗余的类，只要它一个就完成了其他两个类来完成的任务，因而代码更加简洁。\n"
  },
  {
    "path": "四大组件/Activity.md",
    "content": "# Android 基础回顾：Activity 基础\n\n## 1、Activity 的生命周期\n\n### 1.1 一般情况下的生命周期\n\n下图是一般情况下一个 Activity 将会经过的生命周期的流程图：\n\n![Activity的生命周期](res/activity_life.png)\n\n关于上图中生命周期方法的说明：\n\n1. **onCreate() / onDestroy()**：onCreate() 表示 Activity 正在被创建，可以用来做初始化工作；onDestroy() 表示 Activity 正在被销毁，可以用来做释放资源的工作；\n2. **onStart() / onStop()**：onStart() 在 Activity 从不可见变成可见的时候被调用；onStop() 在 Activity 从可见变成不可见的时候被调用；\n3. **onRestart()**：在 Activity 从不可见到变成可见的过程中被调用；\n4. **onResume() / onPause()**：onResume() 在 Activity() 可以与用户交互的时候被调用，onPause() 在 Activity 不可与用户交互的时候被调用。\n\n所以根据上面的分析，我们可以将Activity的生命周期概况为：**创建->可见->可交互->不可交互->不可见->销毁**。因此，我们可以得到下面的这张图：\n\n### 1.2 特殊情况下的生命周期\n\n这里我们总结一下在实际的使用过程中可能会遇到的一些 Acitivity 的生命周期过程：\n\n1. **当用户打开新的 Activity 或者切换回桌面**：会经过的生命周期为 `onPause()->onStop()`。因为此时 Activity 已经变成不可见了，当然，如果新打开的 Activity 用了透明主题，那么 onStop() 不会被调用，因此原来的 Activity 只是不能交互，但是仍然可见。\n2. **从新的 Activity 回到之前的 Activity 或者从桌面回到之前的 Activity**：会经过的生命周期为 `onRestart()->onStart()-onResume()`。此时是从 onStop() 经 onRestart() 回到 onResume() 状态。\n3. 如果在上述 1 的情况下，进入后台的 Activity 因为内存不足被销毁了，那么当再次回到该 Activity 的时候，生命周期方法将会从 onCreate() 开始执行到 onResume()。\n4. **当用户按下 Back 键时**：如果当前 Activity 被销毁，那么经过的生命周期将会是 `onPause()->onStop()->onDestroy()`。\n\n具体地，当存在两个 Activity，分别是 A 和 B 的时候，在各种情况下，它们的生命周期将会经过：\n\n1. **Back 键 Home 键**\n    1. 当用户点击 A 中按钮来到 B 时，假设 B 全部遮挡住了 A，将依次执行：`A.onPause()->B.onCreate()->B.onStart()->B.onResume->A.onStop()`。\n    2. 接1，此时如果点击 Back 键，将依次执行：`B.onPause()->A.onRestart()->A.onStart()->A.onResume()->B.onStop()->B.onDestroy()`。\n    3. 接2，此时如果按下 Back 键，系统返回到桌面，并依次执行：`A.onPause()->A.onStop()->A.onDestroy()`。\n    4. 接2，此时如果按下 Home 键（非长按），系统返回到桌面，并依次执行`A.onPause()->A.onStop()`。由此可见，Back 键和 Home 键主要区别在于是否会执行 onDestroy()。\n    5. 接2，此时如果长按 Home 键，不同手机可能弹出不同内容，Activity 生命周期未发生变化。\n2. **横竖屏切换时 Activity 的生命周期**\n    1. 不设置 Activity 的 `android:configChanges` 时，切屏会重新调用各个生命周期，切横屏时会执行一次，切竖屏时会执行两次。\n    2. 设置 Activity 的 `android:configChanges=“orientation”` 时，切屏还是会重新调用各个生命周期，切横、竖屏时只会执行一次。\n    3. 设置 Activity 的 `android:configChanges=“orientation|keyboardHidden”` 时，切屏不会重新调用各个生命周期，只会执行 onConfiguration() 方法。\n\n### 1.3 onSaveInstanceState() 和 onRestoreInstanceState()\n\n当 Activity 被销毁的时候回调用 `onSaveInstanceState()` 方法来存储当前的状态。这样当 Activity 被重建的时候，可以在 `onCreate()` 和 `onRestoreInstanceState()` 中恢复状态。\n\n对于 targetAPI 为 28 及以后的应用，该方法会在 `onStop()` 方法之后调用，对于之前的设备，这方法会在 `onStop()` 之前调用，但是无法确定是在 `onPause()` 之前还是之后调用。\n\n`onRestoreInstanceState()` 方法用来恢复之前存储的状态，它会在 `onStart()` 和 `onPostCreate()` 之间被调用。此外，你也可以直接在 `onCreate()` 方法中进行恢复，但是基于这个方法调用的时机，如果有特别需求，可以在这个方法中进行处理。\n\n## 2、Activity 的启动模式\n\nActivity 共有四种启动模式：\n\n1. **standard**：默认，每次启动的时候会创建一个新的实例，并且被创建的实例所在的栈与启动它的 Activity 是同一个栈。比如，A 启动了 B，那么 B 将会与 A 处在同一个栈。假如，我们使用 Application 的 Context 启动一个 Activity 的时候会抛出异常，这是因为新启动的 Activity 不知道自己将会处于哪个栈。可以在启动 Activity 的时候使用 `FLAG_ACTIVITY_NEW_TASK`。这样新启动的 Acitivyt 将会创建一个新的栈。\n2. **singleTop**：栈顶复用，如果将要启动的 Activity 已经位于栈顶，那么将会复用栈顶的 Activity，并且会调用它的 `onNewIntent()`。常见的应用场景是从通知打开 Activity 时。\n3. **singleTask**：单例，如果启动它的任务栈中存在该 Activity，那么将会复用该 Activity，并且会将栈内的、它之上的所有的 Activity 清理出去，以使得该 Activity 位于栈顶。常见的应用场景是启动页面、购物界面、确认订单界面和付款界面等。\n4. **singleInstance**：这种启动模式会在启动的时候为其指定一个单独的栈来执行。如果用同样的intent 再次启动这个 Activity，那么这个 Activity 会被调到前台，并且会调用其 `onNewIntent()` 方法。\n\n## 3、Activity 的 Flags\n\n1. **FLAG_ACTIVITY_CLEAR_TOP** : 会清理掉该栈中位于 Activity 上面的所有的 Activity，通常与 FLAG_ACTIVITY_NEW_TASK 配合使用；\n2. **FLAG_ACTIVITY_SINGLE_TOP**: 同样等同于 mainfest 中配置的 singleTop;\n3. **FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS**: 对应于 mainfest 中的属性为`android:excludeFromRecents=\"true\"`，当用户按了 “最近任务列表” 时，该任务不会出现在最近任务列表中，可达到隐藏应用的目的。\n4. **FLAG_ACTIVITY_NO_HISTORY**: 对应于 mainfest 中的 `android:noHistory=\"true\"`。这个 FLAG 启动的 Activity，一旦退出，它不会存在于栈中。\n5. **FLAG_ACTIVITY_NEW_TASK**: 等同于 mainfest 中配置的 singleTask.\n\n\n"
  },
  {
    "path": "四大组件/Broadcast.md",
    "content": "# Android 基础回顾：Broadcast 基础\n\n## 1、关于广播\n\n广播是 Android 提供的一种全局通信机制。\n\n### 1.1 分类\n\n1. 按照注册方式：**静态注册和动态注册**两种；\n2. 按照作用范围：**本地广播和普通广播**两种，普通广播是全局的，所有应用程序都可以接收到，容易会引起安全问题。本地广播只能够在应用内传递，广播接收器也只能接收应用内发出的广播；\n3. 按照是否有序：**有序广播和无序广播**两种，无序广播各接收器接收的顺序无法确定，并且在广播发出之后接收器只能接收，不能拦截和进行其他处理，两者的区别主要体现在发送时调用的方法上。\n\n### 1.2 实现\n\n#### 1.2.1 静态广播\n\n注册，这里的 StaticBroadcastReceiver 是自定义类：\n\n```xml\n<receiver android:name=\".StaticBroadcastReceiver\">\n    <intent-filter>\n        <action android:name=\"android.net.conn.CONNECTIVITY_CHANGE\" />\n    </intent-filter>\n</receiver>\n```\n\n我们可以将要实现的逻辑放在这个类的方法中进行执行：\n\n```java\npublic class StaticBroadcastReceiver extends BroadcastReceiver {\n\n\t@Override\n\tpublic void onReceive(Context context, Intent intent) {\n\t\t// Do something\n\t}\n}\n```\n\n需要注意 Andrdoid 8.0 之后系统对广播进行了一些限制（[官方文档](https://developer.android.google.cn/about/versions/oreo/android-8.0)），具体地：\n\n1. 在 Android 8.0 的平台上，应用不能对大部分的广播进行静态注册，也就是说，不能在AndroidManifest 文件对**有些**广播进行静态注册（注意“有些”，因为不是所有的广播都不能注册）。\n2. 当程序运行在后台的时候，静态广播中不能启动服务。比如之前实现闹钟的时候是监听时间变化来实现的，在 8.0 之后就会抛出异常。\n\n解决方式是使用动态注册方式（一般情况下使用动态注册就好了）。\n\n#### 1.2.2 动态广播\n\n与静态广播相似，但是不需要在 Manifest 中进行注册。\n\n```java\n    // 监听广播：一般在 Activity 的 onCreate() 方法中注册\n    netWorkChangReceiver = new StaticBroadcastReceiver();\n    IntentFilter filter = new IntentFilter();\n    filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);\n    registerReceiver(netWorkChangReceiver, filter);\n\n    // 取消监听：然后在 Activity 的 onDestroy() 中取消注册\n    unregisterReceiver(netWorkChangReceiver);\n```\n\n**注意当页面被销毁的时候需要取消注册广播！**\n\n#### 1.2.3 本地广播\n\n本地广播的核心类是 LocalBroadcastManager，使用它的静态方法 `getInstance()` 获取一个单例之后就可以使用该单例的 `registerReceiver()`、`unregisterReceiver()` 和 `sendBroadcast()` 等方法来进行操作了。\n\n```java\n    // 获取单例\n    localBroadcastManager = LocalBroadcastManager.getInstance(this);\n\n    // 注册广播\n    IntentFilter filter = new IntentFilter();\n    filter.addAction(\"me.shouheng.MyBroadcastReceiver\");\n    localReceiver = new LocalReceiver();\n    localBroadcastManager.registerReceiver(localReceiver, filter);\n\n    // 发送广播\n    Intent intent = new Intent(\"me.shouheng.MyBroadcastReceiver\");\n    localBroadcastManager.sendBroadcast(intent);\n\n    // 取消注册\n    localBroadcastManager.unregisterReceiver(localReceiver);\n```\n\n#### 1.2.4 有序广播\n\n在 xml 中进行注册的时候通过 `android:priority` 指定一个范围在 -1000~1000 之间的整数来指定广播的接收顺序。优先级高的会先接收到，优先级相等的话则顺序不确定。并且前面的广播可以在方法中向 Intent 写入数据，后面的广播可以接收到写入的值。\n\n```xml\n    <receiver android:name=\".MyReceiver_1\">\n        <intent-filter android:priority=\"200\">\n            <action android:name=\"com.song.123\"/>\n        </intent-filter>\n    </receiver>\n    <receiver android:name=\".MyReceiver_2\">\n        <intent-filter android:priority=\"1000\">\n            <action android:name=\"com.song.123\"/>\n        </intent-filter>\n    </receiver>\n```\n\n\n\n"
  },
  {
    "path": "四大组件/Fragment.md",
    "content": "# Android 基础回顾：Fragment 基础\n\n## 1、Fragment 的生命周期\n\n### 1.1 Fragment 的生命周期\n\n下面是 Fragment 的生命周期的流程图：\n\n![Fragment的生命周期](res/fragment_lifecycle.png)\n\n从上图可以看出，Fragment 的生命周期相比于 Activity，在创建的过程中增加了 `onAttach()`、`onCreateView()` 和 `onActivityCreated()` 三个方法，在销毁的过程中增加了 `onDestroyView()` 和 `onDetach()` 两个方法。\n\n### 1.2 Activity 与 Fragment 生命周期对应关系\n\n![Activity 与 Fragment 生命周期对应关系](res/activity_fragment_lifecycle.png)\n\n\n"
  },
  {
    "path": "四大组件/Service.md",
    "content": "# Android 基础回顾：Service 基础\n\nService 主要用于在后台处理一些耗时的逻辑，或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下，让 Service 在后台继续保持运行状态。相对于使用线程来实现异步任务的方式，它的安全性更高。（但是 Service 是运行在主线程中的，如果需要实现异步任务，可以单开线程。）\n\n## 1、基础使用示例\n\n### 1.1 使用示例\n\n首先，通过继承 Service 来定义 Service：\n\n```java\n    public class MyService extends Service {\n\n        public static final String TAG = \"MyService\";\n\n        @Override\n        public void onCreate() {\n            super.onCreate();\n            Log.d(TAG, \"onCreate() executed\");\n        }\n\n        @Override\n        public int onStartCommand(Intent intent, int flags, int startId) {\n            Log.d(TAG, \"onStartCommand() executed\");\n            return super.onStartCommand(intent, flags, startId);\n        }\n\n        @Override\n        public void onDestroy() {\n            super.onDestroy();\n            Log.d(TAG, \"onDestroy() executed\");\n        }\n\n        @Override\n        public IBinder onBind(Intent intent) {\n            return new MyBinder();\n        }\n\n        class MyBinder extends Binder {  \n            public void startDownload() {  \n                Log.d(\"TAG\", \"startDownload() executed\");  \n            }   \n        }  \n    }\n```\n\n定义了 Service 之后还要在 `AndroidManifest.xml` 中注册才能正常使用：\n\n```xml\n    <service android:name=\"com.example.servicetest.MyService\"/>  \n```\n\n然后，就可以使用该 Service 了：\n\n```java\n    // 启动 Service\n    Intent startIntent = new Intent(this, MyService.class);\n    startService(startIntent);  \n\n    // 停止 Service\n    Intent stopIntent = new Intent(this, MyService.class);\n    stopService(stopIntent);\n\n    // 关联 Service 和 Activity\n    Intent bindIntent = new Intent(this, MyService.class);  \n    bindService(bindIntent, connection, BIND_AUTO_CREATE);\n\n    // 解除 Service 和 Activity 关联\n    unbindService(connection);\n\n    private ServiceConnection connection = new ServiceConnection() {  \n  \n        @Override  \n        public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ }  \n  \n        @Override  \n        public void onServiceConnected(ComponentName name, IBinder service) {  \n            myBinder = (MyService.MyBinder) service;  \n            myBinder.startDownload();  \n        }  \n    };\n```\n\n以上就是 Service 的基础使用示例。\n\n### 1.2 Service 的使用小结\n\n首先是 Service 的生命周期图\n\n![Service的生命周期图](res/service_life.png)\n\n其他，\n\n1. Service 有绑定模式和非绑定模式，以及这两种模式的混合使用方式。不同的使用方法生命周期方法也不同。 \n    1. **非绑定模式**：当第一次调用 `startService()` 的时候执行的方法依次为 `onCreate()->onStartCommand()`；当 Service 关闭的时候调用 `onDestory()`。\n    2. **绑定模式**：第一次 `bindService()` 的时候，执行的方法为 `onCreate()->onBind()`；解除绑定的时候会执行 `onUnbind()->onDestory()`。\n2. 我们在开发的过程中还必须注意 Service 实例只会有一个，也就是说如果当前要启动的 Service 已经存在了那么就不会再次创建该 Service 当然也不会调用 onCreate() 方法。所以，\n    1. 当第一次执行 `startService(intent)` 的时候，会调用该 Service 中的 `onCreate()` 和`onStartCommand()` 方法。\n    2. 当第二次执行 `startService(intent)` 的时候，只会调用该 Service 中的 `onStartCommand()` 方法。（因此已经创建了服务，所以不需要再次调用 `onCreate()` 方法了）。\n3. `bindService()` 方法的第三个参数是一个标志位，这里传入 `BIND_AUTO_CREATE` 表示在Activity 和 Service 建立关联后自动创建 Service，这会使得 MyService 中的 `onCreate()` 方法得到执行，但 `onStartCommand()` 方法不会执行。所以，在上面的程序中当调用了`bindService()` 方法的时候，会执行的方法有，Service 的 `onCreate()` 方法，以及 ServiceConnection 的 `onServiceConnected()` 方法。\n4. 在 3 中，如果想要停止 Service，需要调用 `unbindService()` 才行。 \n5. 如果我们既调用了 `startService()`，又调用 `bindService()` 会怎么样呢？这时不管你是单独调用 `stopService()` 还是 `unbindService()`，Service 都不会被销毁，必须要将两个方法都调用 Service 才会被销毁。也就是说，`stopService()` 只会让 Service 停止，`unbindService()` 只会让 Service 和 Activity 解除关联，一个 Service 必须要在既没有和任何 Activity 关联又处理停止状态的时候才会被销毁。\n\n## 2、Service 与线程\n\nService 运行在主线程里的，也就是说如果你在 Service 里编写了非常耗时的代码，程序可能会出现ANR。 \n\nService 只意味着不需要前台 UI 的支持，即使 Activity 被销毁，或者程序被关闭，只要进程还在，Service 就可以继续运行。但是我们可以在 Service 中再创建一个子线程，然后在这里去处理耗时逻辑。\n\n虽然也可以在 Activity 中创建线程来执行耗时任务，但是它的缺点在于该线程只能与该 Activity 关联，其他 Activity 无法对其进行控制。\n\n所以，标准的使用是：\n\n```java\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        new Thread(new Runnable() {\n            @Override\n            public void run() {\n                // 开始执行后台任务  \n            }\n        }).start();\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    class MyBinder extends Binder {\n\n        public void startDownload() {\n            new Thread(new Runnable() {\n                @Override\n                public void run() {\n                    // 执行具体的下载任务  \n                }\n            }).start();\n        }\n    }\n```\n\n当然，你也可以使用 RxJava 等封装的线程池来实现异步任务。\n\n## 3、前台 Service\n\n因为 Service 的系统优先级较低，所以当系统出现内存不足情况时，就有可能会回收掉正在后台运行的 Service。我们可以通过使用前台 Service 来解决 Service 可能被回收的问题。它的效果是在系统中显示一个驻留的通知。\n\n前台服务的\n\n```java\n    public class MyService extends Service {\n\n        public static final String TAG = \"MyService\";\n\n        @Override\n        public void onCreate() {\n            super.onCreate();\n            Notification notification = new Notification(R.drawable.ic_launcher, \n                \"Msg\", System.currentTimeMillis());\n            Intent intent = new Intent(this, MainActivity.class);\n            PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);\n            notification.setLatestEventInfo(this, \"Title\", \"Content\", pi);\n            startForeground(1, notification);\n        }  \n    }\n```\n\n## 4、远程 Service\n\n远程 Service 是一种运行在其他进程中的服务。我们可以在 Manifest 中进行注册的时候通过指定进程来讲服务设置成远程的。远程的服务因为运行在另一个进程中，所以涉及跨进程调用的问题。\n\n可以通过在 `AndroidManifest.xml` 中进行如下设置来将一个 Service 设置在非主线程中：\n\n```xml\n    <service  \n        android:name=\"com.example.servicetest.MyService\"  \n        android:process=\":remote\" >  \n    </service>  \n```\n\n也就说当前的 Service 运行在其他的进程了，不会阻碍主进行，从而也不会存在 ANR 了。但是这种方式中的 Service 是无法与 Activity 进行关联的，也就是说调用 `bindService()` 的时候会出现错误。如果我们想要将该 Service 与 Activity 进行关联，就需要使用 AIDL 进行跨进程通信了（IPC）。\n\n要实现跨进程调用，我们可以按照如下步骤来实现：\n\n首先，新建 `MyAIDLService.aidl` 文件：\n\n```java\n    package com.example.servicetest;  \n\n    interface MyAIDLService {  \n        int plus(int a, int b);  \n        String toUpperCase(String str);  \n    }  \n```\n\n然后，我们要修改之前 Service 中的 `bind()` 方法：\n\n```java\n    @Override  \n    public IBinder onBind(Intent intent) {  \n        return mBinder;  \n    }\n\n    MyAIDLService.Stub mBinder = new Stub() {  \n  \n        @Override  \n        public String toUpperCase(String str) throws RemoteException {  \n            if (str != null) {  \n                return str.toUpperCase();  \n            }  \n            return null;  \n        }  \n  \n        @Override  \n        public int plus(int a, int b) throws RemoteException {  \n            return a + b;  \n        }  \n    }; \n```\n\n然后，我们在 ServiceConnection 中进行如下实现：\n\n```java\n    private ServiceConnection connection = new ServiceConnection() {  \n  \n        @Override  \n        public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ }  \n  \n        @Override  \n        public void onServiceConnected(ComponentName name, IBinder service) {  \n            myAIDLService = MyAIDLService.Stub.asInterface(service);  \n            try {  \n                int result = myAIDLService.plus(3, 5);  \n                String upperStr = myAIDLService.toUpperCase(\"hello world\");  \n            } catch (RemoteException e) {  \n                e.printStackTrace();  \n            }  \n        }  \n    };  \n```\n\n也就是使用 `MyAIDLService.Stub.asInterface()` 方法获取 MyAIDLService，并调用 MyAIDLService 的方法。这时候再调用 `bindService()` 就不会出现错误了。\n\n如果我们想要在其他进程（APP）中调用该 Service，我们可以进行如下操作：\n\n首先在 Service 添加 `Intent-Filter`：\n\n```xml\n    <service android:name=\"com.example.servicetest.MyService\"  \n        android:process=\":remote\" >  \n        <intent-filter>  \n            <action android:name=\"com.example.servicetest.MyAIDLService\"/>  \n        </intent-filter>  \n    </service>  \n```\n\n这样，我们就将该 Service 设置成其他程序可访问的了。\n\n然后，在要访问该 Service 的程序中进行如下操作：\n\n1. 将上述定义的 MyAIDLService 连同其包拷贝到当前程序中，即 src 目录下面。\n2. 然后在绑定 Service 的时候按照下面的方式绑定：\n\n```java\n    Intent intent = new Intent(\"com.example.servicetest.MyAIDLService\");\n    bindService(intent, connection, BIND_AUTO_CREATE);\n\n    private ServiceConnection connection = new ServiceConnection() {\n\n        @Override\n        public void onServiceDisconnected(ComponentName name) {}\n\n        @Override\n        public void onServiceConnected(ComponentName name, IBinder service) {\n            myAIDLService = MyAIDLService.Stub.asInterface(service);\n            try {\n                int result = myAIDLService.plus(50, 50);\n                String upperStr = myAIDLService.toUpperCase(\"comes from ClientTest\");\n            } catch (RemoteException e) {\n                e.printStackTrace();\n            }\n        }\n    };\n```\n\n这样我们就将该 Service 设置成了其他进程可访问的。\n\n## 5、IntentService\n\n相比于一般的 Service，IntentService 具有的特征：\n\n1. 会创建独立的线程来处理所有的 `Intent` 请求;。\n2. 会创建独立的线程来处理 `onHandleIntent()` 方法实现的代码，无需处理多线程问题。\n3. 所有请求处理完成后，`IntentService` 会自动停止，无需调用 `stopSelf()` 方法停止 Service;。\n4. 为 Service 的 `onBind()` 提供默认实现，返回 null. \n5. 为 Service 的 `onStartCommand()` 提供默认实现，将请求 Intent 添加到队列中。 \n6. IntentService 内置的是 HandlerThread 作为异步线程，每一个交给 IntentService 的任务都将以队列的方式逐个被执行到，一旦队列中有某个任务执行时间过长，那么就会导致后续的任务都会被延迟处理。正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死，该程序的优先级是介于前台程序与纯后台程序之间的\n\n关于 IntentService 的源码分析，可以参考下面这篇文章：\n\n[Android 多线程编程：IntentService & HandlerThread](https://blog.csdn.net/github_35186068/article/details/83758049)\n\n## 5、Service 保活的问题\n\n我们可以用通过 `setForeground(true)` 来提升 Service 的优先级。当然这并不能保证你得Service 永远不被杀掉，只是提高了他的优先级。这种方式的缺点是会设置一个长期停留的通知，用户体验比较差。\n\n那么如何避免后台进程被杀死？\n\n首先，服务被杀死的情况包含下面三种：\n\n1. 系统根据资源分配情况杀死服务\n2. 用户通过 `settings->Apps->Running->Stop` 方式杀死服务\n3. 用户通过 `settings->Apps->Downloaded->Force Stop` 方式杀死服务\n\n以及对应的解决办法：\n\n1. 调用 `startForegound()`，让你的 Service 所在的线程成为前台进程；\n2. Service 的 `onStartCommond()` 返回 START_STICKY 或 START_REDELIVER_INTENT；\n3. Service 的 `onDestroy()` 里面重新启动自己。\n\n关于 `onStartCommond()` 的返回值的总结：\n\n|No|可选值|含义|\n|:-:|:-:|:-|\n|1|START_STICKY|当 Service 因内存不足而被系统 kill 后，一段时间后内存再次空闲时，系统将会尝试重新创建此 Service，一旦创建成功后将回调 `onStartCommand()` 方法，但其中的 Intent 将是 null，除非有挂起的 Intent，如 pendingintent，这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务|\n|2|START_NOT_STICKY|当 Service 因内存不足而被系统 kill 后，即使系统内存再次空闲时，系统也不会尝试重新创建此 Service。除非程序中再次调用 `startService()` 启动此 Service，这是最安全的选项，可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务|\n|3|START_REDELIVER_INTENT|当 Service 因内存不足而被系统 kill 后，则会重建服务，并通过传递给服务的最后一个 Intent 调用 `onStartCommand()`，任何挂起 Intent 均依次传递。与START_STICKY 不同的是，其中的传递的 Intent 将是非空，是最后一次调用 `startService()` 中的 intent。这个值适用于主动执行应该立即恢复的作业（例如下载文件）的服务|\n\n在 `onDestroy()` 中自启的示例：\n\n```java\n\tpublic void onCreate() {  \n\t    super.onCreate();  \n\t    mBroadcast = new BroadcastReceiver() {  \n\t\t    @Override  \n\t\t    public void onReceive(Context context, Intent intent) {  \n\t\t        Intent a = new Intent(ServiceA.this, ServiceA.class);  \n\t\t        startService(a);  \n\t\t    }  \n\t    };  \n\t    mIF = new IntentFilter();  \n\t    mIF.addAction(\"listener\");  \n\t    registerReceiver(mBroadcast, mIF);  \n\t}\n\t\n\t@Override  \n\tpublic void onDestroy() {  \n\t  super.onDestroy();  \n\t  Intent intent = new Intent();  \n\t  intent.setAction(\"listener\");  \n\t  sendBroadcast(intent);  \n\t  unregisterReceiver(mBroadcast);  \n\t}  \n```\n\n上面的这种启动的实例会因为系统处于后台线程而抛出异常。\n\n参考：\n\n1. [Android Service完全解析，关于服务你所需知道的一切(上)](http://blog.csdn.net/guolin_blog/article/details/11952435)\n2. [Android Service完全解析，关于服务你所需知道的一切(下)](http://blog.csdn.net/guolin_blog/article/details/9797169)\n3. [关于Android Service真正的完全详解，你需要知道的一切](https://blog.csdn.net/javazejian/article/details/52709857)"
  },
  {
    "path": "图片加载/Android相机最佳实践.md",
    "content": "# CameraX：Android 相机库开发实践\n\n## 前言\n\n前段时间因为工作的需要对项目中的相机模块进行了优化，我们项目中的相机模块是基于开源库 CameraView 进行开发的。那次优化主要包括两个方面，一个是相机的启动速度，另一个是相机的拍摄的清晰度的问题。因为时间仓促，那次只是在原来的代码的基础之上进行的优化，然而那份代码本身存在一些问题，导致相机的启动速度无法进一步提升。所以，我准备自己开发一款功能完善，并且可拓展的相机库，于是 [CameraX](https://github.com/Shouheng88/CameraX) 就诞生了。\n\n## Android 相加开源库的现状\n\n要使用 Android 相机实现图片拍照功能本身并不复杂，Camera1 + SurfaceView 就可以搞定。但是如果让相机能够自由拓展，就需要花费很多的功夫。我所接触的开源库包括 Google 非官方的 CameraView，以及 CameraFragment. 两个库的设计有各自的优点和缺点。\n\n|开源库|优点|缺点|\n|:-|:-|:-|\n|CameraView|1.支持基本的拍照、缩放等功能；2.支持自定义图片的宽高比；3.支持多种预览布局方式；|1.每次获取相机支持的尺寸的时候，会先将其组装到一个有序的 Set 中，这个过程会占用一定的启动时间；2.不支持拍摄视频；3.代码堆砌，结构混乱|\n|CameraFragment|1.支持拍摄照片和视频；2.代码结构清晰|1.不支持缩放；2.默认宽高比4:3，无法运行时修改；3.必须基于 Fragment|\n\n以上是两个开源库的优点和缺点，而我们可以结合它们的优缺点实现一个更加完善的相机库，同时对性能的优化和用户自定义配置，我们也提供了更多的可用的接口。\n\n## CameraX 整体结构设计\n\n虽然文章的题目是相机开发实践，但是我们并不打算介绍太多关于如何使用 Camera API 的内容，因为本项目是开源的，读者可以自行 Fork 代码进行阅读。在这里，我们只对项目中的一些关键部分的设计思路进行说明。\n\n![相机整体架构](res/开发.png)\n\n连接：https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049\n\n以上是我们相机库的整体架构的设计图，这里笔者使用了 UML 建模进行基础的架构设计（当然，并非严格遵循 UML 建模的语言规则）。下面，我们介绍下项目的关键部分的设计思路。\n\n### Camera1 还是 Camera2？\n\n了解 Android 相机 API 的同学可能知道，在 LoliPop 上面提出了 Camera2 API. 就笔者个人的实践开发的效果来看，Camera2 相机的性能确实比 Camera1 要好得多，这体现在相机对焦的速率和相机启动的速率上。当然，这和硬件也有一定的关系。Camera2 比 Camera1 使用起来确实复杂得多，但提供的可以调用的 API 也更丰富。Camera2 的另一个问题是国内的很多手机设备对 Camera2 的支持并不好。\n\n对于这个问题，首先，我们可以根据系统的参数来判断该设备是否支持 Camera2：\n\n```java\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public static boolean hasCamera2(Context context) {\n        if (context == null) return false;\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false;\n        try {\n            CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);\n            assert manager != null;\n            String[] idList = manager.getCameraIdList();\n            boolean notNull = true;\n            if (idList.length == 0) {\n                notNull = false;\n            } else {\n                for (final String str : idList) {\n                    if (str == null || str.trim().isEmpty()) {\n                        notNull = false;\n                        break;\n                    }\n                    final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str);\n\n                    Integer iSupportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);\n                    if (iSupportLevel != null && iSupportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {\n                        notNull = false;\n                        break;\n                    }\n                }\n            }\n            return notNull;\n        } catch (Throwable ignore) {\n            return false;\n        }\n    }\n```\n\n不过，即便上面方法返回的结果标明支持 Camera2，但相机仍然可能在启动中出现异常。所以 CameraView 的解决方案是，相机启动的方法返回一个 boolean 类型标明 Camera2 是否启动成功，如果失败了，就降级并使用 Camera1。但是降级的过程会浪费一定的启动时间，因此，有人提出了使用 SharedPreferences 存储降级的记录，下次直接使用 Camera1 的解决方案。\n\n上面两种方案各自有优缺点，使用第二种方案意味着你要修改相机库的源代码，而我们希望以一种更加灵活的方式提供给用户选择相机的权力。没错，就是**策略设计模式**。\n\n因为虽然 Camera1 和 Camera2 的 API 设计和使用不同，但是我们并不需要知道内部如何实现，我们只需要给用户提供切换相机、打开闪光灯、拍照、缩放等的接口即可。在这种情况下，当然使用**门面设计模式**是最好的选择。\n\n另外，对于 TextureView 还是 SurfaceView 的选择，我们也使用了**策略模式+门面模式**的思路。\n\n即。对于相机的选择，我们提供门面 CameraManager 接口，Camera1 的实现类 Camera1Manager 以及 Camera2 的实现类 Camera2Manager. Camera1Manager 和 Camera2Manager 又统一继承自 BaseCameraManager. 这里的 BaseCameraManager 是一个抽象类，用来封装一些通用的相机方法。\n\n所以问题到了是 Camera1Manager 还是 Camera2Manager 的问题。这里我们提供了策略接口 CameraManagerCreator，它返回 CameraManager：\n\n```java\npublic interface CameraManagerCreator {\n\n    CameraManager create(Context context, CameraPreview cameraPreview);\n}\n```\n\n以及一个默认的实现：\n\n```java\npublic class CameraManagerCreatorImpl implements CameraManagerCreator {\n\n    @Override\n    public CameraManager create(Context context, CameraPreview cameraPreview) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && CameraHelper.hasCamera2(context)) {\n            return new Camera2Manager(cameraPreview);\n        }\n        return new Camera1Manager(cameraPreview);\n    }\n}\n```\n\n因此，我们只需要在相机的全局配置中指定自己的 CameraManager 创建策略就可以使用指定的相机了。\n\n### 全局配置\n\n之前考虑指定 CameraManager 创建策略的时候，思路是直接对静态的变量赋值的方式，不过后来考虑到对相机的支持的尺寸进行缓存的问题，所以将其设计了静态单实例的类：\n\n```java\npublic class ConfigurationProvider {\n\n    private static volatile ConfigurationProvider configurationProvider;\n\n    private ConfigurationProvider() {\n        if (configurationProvider != null) {\n            throw new UnsupportedOperationException(\"U can't initialize me!\");\n        }\n        initWithDefaultValues();\n    }\n\n    public static ConfigurationProvider get() {\n        if (configurationProvider == null) {\n            synchronized (ConfigurationProvider.class) {\n                if (configurationProvider == null) {\n                    configurationProvider = new ConfigurationProvider();\n                }\n            }\n        }\n        return configurationProvider;\n    }\n\n    // ... ...\n}\n```\n\n除了指定一些全局的配置之外，我们还可以在 ConfigurationProvider 中缓存一些相机的信息，比如相机支持的尺寸的问题。因为相机所支持的尺寸属于相机属性的一部分，是不变的，我们没有必要获取多次，可以将其缓存起来，下次直接使用。当然，我们还提供了不使用缓存的接口：\n\n```java\npublic class ConfigurationProvider {\n\n    // ...\n    private boolean useCacheValues;\n    private List<Size> pictureSizes;\n\n    public List<Size> getPictureSizes(android.hardware.Camera camera) {\n        if (useCacheValues && pictureSizes != null) {\n            return pictureSizes;\n        }\n        List<Size> sizes = Size.fromList(camera.getParameters().getSupportedPictureSizes());\n        if (useCacheValues) {\n            pictureSizes = sizes;\n        }\n        return sizes;\n    }\n\n}\n```\n\n这样，我们在获取相机支持的图片尺寸信息的时候只需要传入 Camera 即可使用缓存的信息。当然，缓存信息在某些极端的情况下可能会带来问题，比如从 Camera1 切换到 Camera2 的时候，需要清除缓存。\n\n*注：这里缓存的时候应该使用 SoftReference，但是考虑到数据量不大，没有这么设计，以后会考虑修改。*\n\n### 输出媒体文件的尺寸的问题\n\n使用 Android 相机一个让人头疼的地方是计算尺寸的问题：因为相机支持的尺寸有三种，包括相片的支持尺寸、预览的支持尺寸和视频的支持尺寸。预览的尺寸决定了用户看到的画面的清晰程度，但是真正拍摄出图片的清晰度取决于相片的尺寸，同理输出的视频的尺寸取决于视频的尺寸。\n\n在 CameraView 中，它允许你指定一个图片的尺寸，当没有满足的要求的尺寸的时候会 Crash…这样的处理方式是将其不好的，因为用户根本无法确定相机最大的支持尺寸，而 CameraView 甚至没有提供获取相机支持尺寸的接口……\n\n为了解决这个问题，我们首先提供了一系列用户获取相机支持尺寸的接口：\n\n```java\n    Size getSize(@Camera.SizeFor int sizeFor);\n\n    SizeMap getSizes(@Camera.SizeFor int sizeFor);\n```\n\n这里的 SizeFor 是基于注解的枚举，我们通过它来判断用户是希望获取相片、预览还是视频的尺寸信息。这里的 SizeMap 是一个哈希表，从相机的宽高比映射到对应的尺寸列表。跟 CameraView 处理方式不同的是，我们只有在调用上述方法的时候才计算图片的宽高比信息，虽然调用下面的方法的时候会花费一丁点儿时间，但是相机的启动速度大大提升了：\n\n```java\n    @Override\n    public SizeMap getSizes(@Camera.SizeFor int sizeFor) {\n        switch (sizeFor) {\n            case Camera.SIZE_FOR_PREVIEW:\n                if (previewSizeMap == null) {\n                    previewSizeMap = CameraHelper.getSizeMapFromSizes(previewSizes);\n                }\n                return previewSizeMap;\n            case Camera.SIZE_FOR_PICTURE:\n                if (pictureSizeMap == null) {\n                    pictureSizeMap = CameraHelper.getSizeMapFromSizes(pictureSizes);\n                }\n                return pictureSizeMap;\n            case Camera.SIZE_FOR_VIDEO:\n                if (videoSizeMap == null) {\n                    videoSizeMap = CameraHelper.getSizeMapFromSizes(videoSizes);\n                }\n                return videoSizeMap;\n        }\n        return null;\n    }\n```\n\n获取了相机的尺寸信息的目的当然是将其设置到相机上面，所以我们提供了两个用来设置相机尺寸的接口：\n\n```java\n    void setExpectSize(Size expectSize);\n\n    void setExpectAspectRatio(AspectRatio expectAspectRatio);\n```\n\n它们一个用来指定期望的输出文件的尺寸，一个用来指定期望的图片的宽高比。\n\nOK，既然用户可以指定计算参数，那么怎么计算呢？这当然还是用户说了算的，因为我们一样在全局配置中为用户提供了计算的策略接口：\n\n```java\npublic interface CameraSizeCalculator {\n\n    Size getPicturePreviewSize(@NonNull List<Size> previewSizes, @NonNull Size pictureSize);\n\n    Size getVideoPreviewSize(@NonNull List<Size> previewSizes, @NonNull Size videoSize);\n\n    Size getPictureSize(@NonNull List<Size> pictureSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);\n\n    Size getVideoSize(@NonNull List<Size> videoSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize);\n}\n```\n\n当然，我们也会提供一个默认的计算策略。在 CameraManager 内部，我们会在需要的地方调用上述接口的方法以获取最终的相机尺寸信息：\n\n```java\n    private void adjustCameraParameters(boolean forceCalculateSizes, boolean changeFocusMode, boolean changeFlashMode) {\n        Size oldPreview = previewSize;\n        long start = System.currentTimeMillis();\n        CameraSizeCalculator cameraSizeCalculator = ConfigurationProvider.get().getCameraSizeCalculator();\n        android.hardware.Camera.Parameters parameters = camera.getParameters();\n        if (mediaType == Media.TYPE_PICTURE && (pictureSize == null || forceCalculateSizes)) {\n            pictureSize = cameraSizeCalculator.getPictureSize(pictureSizes, expectAspectRatio, expectSize);\n            previewSize = cameraSizeCalculator.getPicturePreviewSize(previewSizes, pictureSize);\n            parameters.setPictureSize(pictureSize.width, pictureSize.height);\n            notifyPictureSizeUpdated(pictureSize);\n        }\n\n        // ... ...\n    }\n```\n\n### 性能优化\n\n为了对相机的性能进行优化，笔者可是花了大量的精力。因为在之前进行优化的时候积累了一些经验，所以这次开发的时候就容易得多。下面是 TraceView 进行分析的图：\n\n![Android 相机 TraceView 分析](res/QQ图片20190423230233.png)\n\n可以看出从相机当中获取支持尺寸的本身会占用一定时间的，而这种属于相机固有的信息，一般是不会发生变化的，所以我们可以通过将其缓存起来来提升下一次打开相机的速率。\n\n整体上，该项目的优化主要体现在几个地方：\n\n1. 使用注解+常量取代枚举：因为枚举占用的内存空间比较大，而单纯使用注解无法约束输入参数的范围。这在 enums 包下面可以看到，这也是 Android 性能优化最常见的手段之一。\n\n2. 延迟初始化：我们为了达到只在使用到某些数据的时候才初始化的目的采用了延迟初始化的解决方案，比如 Size 的宽高比的问题：\n\n```\npublic class Size {\n\n    // ...\n\n    private double ratio;\n\n    public double ratio() {\n        if (ratio == 0 && width != 0) {\n            ratio = (double) height / width;\n        }\n        return ratio;\n    }\n\n}\n```\n\n3. 数据结构的应用和选择：选择合适的数据结构和自定义数据结构往往能起到化腐朽为神奇的作用。比如 SizeMap \n\n```java\npublic class SizeMap extends HashMap<AspectRatio, List<Size>> {\n}\n```\n\n比如在列表数据结构的应用上面，使用 ArrayList 但是提前指定数组大小，减小数组扩容的次数：\n\n```java\n    public static List<Size> fromList(@NonNull List<Camera.Size> cameraSizes) {\n        List<Size> sizes = new ArrayList<>(cameraSizes.size());\n        for (Camera.Size size : cameraSizes) {\n            sizes.add(of(size.width, size.height));\n        }\n        return sizes;\n    }\n```\n\n4. 缓存，这个我们之前已经提到过，除了尺寸信息我们还缓存了一些其他的信息，具体可以参考源码。\n\n5. 异步线程：这个当然是最能提升应用相应速度的方式。它能够让我们不阻塞主线程，从而提升界面相应的速度。但是在相机开发的时候存在一个问题，即通常打开的相机的时候比较耗时，所以放在异步线程中；而开启预览处于主线程，这很容易因为线程执行的顺序的问题导致一些难以预测的异常。在之前，笔者的解决方案是使用一个私有锁来实现线程的控制。\n\n## 总结\n\n本次相机库开发占用的时间其实不多，更多的时间花费在了 UML 建模图的设计和在真正开发之前收集资料信息。不得不说，如果你开发一个小的项目，不需要做什么设计，直接就可以上了，但是如果你设计一个比较复杂的库，花费更多时间在 UML 建模上面是值得的，因为它能让你的开发思路更加清晰。另外，为了开发 Camera2，笔者不仅找遍了开源库，还翻译了相关的官方文档，这在开源项目中会一并奉上。\n\n### 相机目前支持的功能\n\n|编号|功能|\n|:-|:-|\n|1|拍摄照片|\n|2|拍摄视频|\n|3|指定使用 Camera1 还是 Camera2|\n|4|指定使用 TextureView 还是 SurfaceView|\n|5|闪光灯打开和关闭|\n|6|自动对焦的选择|\n|7|前置和后置相机|\n|8|快门声|\n|9|指定缩放的大小|\n|10|指定期望的图片大小|\n|11|指定期望的图片宽高比|\n|12|获取支持的图片、预览和视频的尺寸信息|\n|13|相机尺寸发生变化监听|\n|14|输出视频的文件位置|\n|15|输出视频的时间长度|\n|16|手指界面滑动的监听|\n|17|触摸进行缩放|\n|18|预览自适应和裁剪等|\n|19|缓存相机信息，清除和不适用缓存信息|\n\n### 最后是关于项目的一些小问题\n\n该项目目前所有功能已经开发完毕，不过仍有一些小的问题需要完善：\n\n1. Camera2 预览放大之后拍摄出的图片没有放大效果的问题；\n2. Camera1 拍摄出的图片需要旋转 90 度；\n3. Camera2 在屏幕旋转成横屏之后相机预览需要同时选择 90 度的问题；\n4. Camera1 和 Camera2 切换存在一些问题。\n\n另外，由于时间限制，该相机库目前没有进行严格的测试，所以建议使用的时候进行充分测试之后再使用。\n\n### 是否会继续完善该项目？\n\n是的，包括对相机的功能进行充分测试。只是目前的时间结点，笔者有其他的事务需要处理，所以先把它介绍给读者。当然也希望能够有更多感兴趣的朋友对该项目贡献代码。\n\n## 项目地址：\n\n1. 项目地址：https://github.com/Shouheng88/CameraX\n2. UML 建模图地址：https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049\n3. 笔者翻译的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\n"
  },
  {
    "path": "图片加载/Glide系列：Glide主流程源码分析.md",
    "content": "# Glide 系列-2：主流程源码分析（4.8.0）\n\nGlide 是 Android 端比较常用的图片加载框架，这里我们就不再介绍它的基础的使用方式。你可以通过查看其官方文档学习其基础使用。这里，我们给出一个 Glide 的最基本的使用示例，并以此来研究这个整个过程发生了什么：\n\n```java\nGlide.with(fragment).load(myUrl).into(imageView);\n```\n\n上面的代码虽然简单，但是整个执行过程涉及许多类，其流程也比较复杂。为了更清楚地说明这整个过程，我们将 Glide 的图片加载按照调用的时间关系分成了下面几个部分：\n\n1. `with()` 方法的执行过程\n2. `load()` 方法的执行过程\n3. `into()` 方法的执行过程\n    1. 阶段1：开启 `DecodeJob` 的过程\n    2. 阶段2：打开网络流的过程\n    3. 阶段3：将输入流转换为 `Drawable` 的过程\n    4. 阶段4：将 `Drawable` 展示到 `ImageView` 的过程\n\n即按照上面的示例代码，先分成 `with()`、`load()` 和 `into()` 三个过程，而 `into()` 过程又被细化成四个阶段。\n\n下面我们就按照上面划分的过程来分别介绍一下各个过程中都做了哪些操作。\n\n## 1、with() 方法的执行过程\n\n### 1.1 实例化单例的 Glide 的过程\n\n当调用了 Glide 的 `with()` 方法的时候会得到一个 `RequestManager` 实例。`with()` 有多个重载方法，我们可以使用 `Activity` 或者 `Fragment` 等来获取 `Glide` 实例。它们最终都会调用下面这个方法来完成最终的操作：\n\n```java\npublic static RequestManager with(Context context) {\n    return getRetriever(context).get(context);\n}\n```\n\n在 `getRetriever()` 方法内部我们会先使用 `Glide` 的 `get()` 方法获取一个单例的 Glide 实例，然后从该 Glide 实例中得到一个 `RequestManagerRetriever`:\n\n```java\nprivate static RequestManagerRetriever getRetriever(Context context) {\n    return Glide.get(context).getRequestManagerRetriever();\n}\n```\n\n这里调用了 Glide 的 `get()` 方法，它最终会调用 `initializeGlide()` 方法实例化一个**单例**的 `Glide` 实例。在之前的文中我们已经介绍了这个方法。它主要用来从注解和 Manifest 中获取 GlideModule，并根据各 GlideModule 中的方法对 Glide 进行自定义：\n\n[《Glide 系列-1：预热、Glide 的常用配置方式及其原理》](Glide系列：Glide的配置和使用方式.md)\n\n下面的方法中需要传入一个 `GlideBuilder` 实例。很明显这是一种构建者模式的应用，我们可以使用它的方法来实现对 Glide 的个性化配置：\n\n```java\nprivate static void initializeGlide(Context context, GlideBuilder builder) {\n\n    // ... 各种操作，略\n\n    // 赋值给静态的单例实例\n    Glide.glide = glide;\n}\n```\n\n最终 Glide 实例由 `GlideBuilder` 的 `build()` 方法构建完毕。它会直接调用 Glide 的构造方法来完成 Glide 的创建。在该构造方法中会将各种类型的图片资源及其对应的加载类的映射关系注册到 Glide 中，你可以阅读源码了解这部分内容。\n\n### 1.2 Glide 的生命周期管理\n\n在 `with()` 方法的执行过程还有一个重要的地方是 Glide 的生命周期管理。因为当我们正在进行图片加载的时候，Fragment 或者 Activity 的生命周期可能已经结束了，所以，我们需要对 Glide 的生命周期进行管理。\n\nGlide 对这部分内容的处理也非常巧妙，它使用没有 UI 的 Fragment 来管理 Glide 的生命周期。这也是一种非常常用的生命周期管理方式，比如 `RxPermission` 等框架都使用了这种方式。你可以通过下面的示例来了解它的作用原理：\n\n[示例代码：使用 Fragment 管理 onActivityResult()](https://github.com/Shouheng88/Android-references/tree/master/advanced/src/main/java/me/shouheng/advanced/callback)\n\n在 `with()` 方法中，当我们调用了 `RequestManagerRetriever` 的 `get()` 方法之后，会根据 Context 的类型调用 `get()` 的各个重载方法。\n\n```java\n  public RequestManager get(@NonNull Context context) {\n    if (context == null) {\n      throw new IllegalArgumentException(\"You cannot start a load on a null Context\");\n    } else if (Util.isOnMainThread() && !(context instanceof Application)) {\n      if (context instanceof FragmentActivity) {\n        return get((FragmentActivity) context);\n      } else if (context instanceof Activity) {\n        return get((Activity) context);\n      } else if (context instanceof ContextWrapper) {\n        return get(((ContextWrapper) context).getBaseContext());\n      }\n    }\n\n    return getApplicationManager(context);\n  }\n```\n\n我们以 Activity 为例。如下面的方法所示，当当前位于后台线程的时候，会使用 Application 的 Context 获取 `RequestManager`，否则会使用无 UI 的 Fragment 进行管理：\n\n```java\n  public RequestManager get(@NonNull Activity activity) {\n    if (Util.isOnBackgroundThread()) {\n      return get(activity.getApplicationContext());\n    } else {\n      assertNotDestroyed(activity);\n      android.app.FragmentManager fm = activity.getFragmentManager();\n      return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));\n    }\n  }\n```\n\n然后就调用到了 `fragmentGet()` 方法。这里我们从 `RequestManagerFragment` 中通过 `getGlideLifecycle()` 获取到了 `Lifecycle` 对象。`Lifecycle` 对象提供了一系列的、针对 Fragment 生命周期的方法。它们将会在 Fragment 的各个生命周期方法中被回调。\n\n```java\n  private RequestManager fragmentGet(Context context, FragmentManager fm, \n    Fragment parentHint, boolean isParentVisible) {\n    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);\n    RequestManager requestManager = current.getRequestManager();\n    if (requestManager == null) {\n      Glide glide = Glide.get(context);\n      requestManager =\n          factory.build(\n              glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);\n      current.setRequestManager(requestManager);\n    }\n    return requestManager;\n  }\n```\n\n然后，我们将该 `Lifecycle` 传入到 `RequestManager` 中，以 `RequestManager` 中的两个方法为例，`RequestManager` 会对 `Lifecycle` 进行监听，从而达到了对 Fragment 的生命周期进行监听的目的：\n\n```java\n  public void onStart() {\n    resumeRequests();\n    targetTracker.onStart();\n  }\n\n  public void onStop() {\n    pauseRequests();\n    targetTracker.onStop();\n  }\n```\n\n### 1.3 小结\n\n经过上述分析，我们可以使用下面的流程图总结 Glide 的 `with()` 方法的执行过程：\n\n![Glide 的 with() 方法的执行过程](res/glide_with.jpg)\n\n## 2、load() 方法的执行过程\n\n### 2.1 load() 的过程\n\n当我们拿到了 `RequestManager` 之后就可以使用它来调用 `load()` 方法了。在我们的示例中传入的是一个 url 对象。`load()` 方法也是重载的，我们可以传入包括 Bitmap, Drawable, Uri 和 String 等在内的多种资源类型。示例中会调用下面的这个方法得到一个 `RequestBuilder` 对象，显然这是一种构建者模式的应用。我们可以使用 `RequestBuilder` 的其他方法来继续构建图片加载请求，你可以通过查看它的源码了解 Glide 都为我们提供了哪些构建方法：\n\n```java\n  public RequestBuilder<TranscodeType> load(@Nullable String string) {\n    return loadGeneric(string);\n  }\n```\n\n在 `RequestBuilder` 的构造方法中存在一个 `apply()` 方法值得我们一提，其定义如下。从下面的方法定义中可以看出，我们可以通过为 `RequestBuilder` 指定 `RequestOptions` 来配置当前图片加载请求。比如，指定磁盘缓存的策略，指定占位图，指定图片加载出错时显示的图片等等。那么我们怎么得到 `RequestOptions` 呢？在 Glide 4.8.0 中的类 `RequestOptions` 为我们提供了一系列的静态方法，我们可以这些方法来得到 `RequestOptions` 的实例：\n\n```java\n  public RequestBuilder<TranscodeType> apply(RequestOptions requestOptions) {\n    Preconditions.checkNotNull(requestOptions);\n    this.requestOptions = getMutableOptions().apply(requestOptions);\n    return this;\n  }\n```\n\n回过头来，我们可以继续跟踪 `load()` 方法。其实，不论我们使用了 `load()` 的哪个重载方法，最终都会调用到下面的方法。它的逻辑也比较简单，就是将我们的图片资源信息赋值给 `RequestBuilder` 的局部变量就完事了。至于图片如何被加载和显示，则在 `into()` 方法中进行处理。\n\n```java\n  public RequestBuilder<TranscodeType> load(@Nullable String string) {\n    return loadGeneric(string);\n  }\n\n  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {\n    this.model = model;\n    isModelSet = true;\n    return this;\n  }\n```\n\n### 2.2 小结\n\n所以，我们可以总结 Glide 的 `load()` 方法的执行过程如下。也就是使用 `RequestManger` 得到一个 `RequestBuilder` 的过程：\n\n![Glide 的 load() 方法执行过程](res/glide_load.jpg)\n\n## 3、into() 方法的执行过程\n\n考虑到 `into()` 方法流程比较长、涉及的类比较多，我们按照图片加载的过程将其分成四个阶段来进行介绍。\n\n第一个阶段是开启 `DecodeJob` 的过程。`DecodeJob` 负责从缓存或者从原始的数据源中加载图片资源，对图片进行变换和转码，是 Glide 图片加载过程的核心。`DecodeJob` 继承了 `Runnable`，实际进行图片加载的时候会将其放置到线程池当中执行。这个阶段我们重点介绍的是从 `RequestBuilder` 构建一个 `DecodeJob` 并开启 `DecodeJob` 任务的过程。即构建一个 `DecodeJob` 并将其丢到线程池里的过程。\n\n第二个阶段是打开网络流的过程。这个阶段会根据我们的图片资源来从数据源中加载图片数据。以我们的示例为例，在默认情况下会从网络当中加载图片，并得到一个 `InputStream`. \n\n第三个阶段是将输入流转换为 `Drawable` 的过程。得到了 `InputStream` 之后还要调用 `BitmapFactory` 的 `decodeStream()` 方法来从 `InputStream` 中得到一个 `Drawable`. \n\n第四个阶段是将 `Drawable` 显示到 `ImageView` 上面的过程。\n\n### 3.1 阶段1：开启 DecodeJob 的过程\n\n#### 3.1.1 流程分析\n\n我们继续沿着 `into()` 方法进行分析。\n\n`into()` 方法也定义在 `RequestBuilder` 中，并且也是重载的。不论我们调用哪个重载方法都会将要用来显示图片的对象封装成一个 `Target` 类型。`Target` 主要用来对用来显示图片的对象的生命周期进行管理。当我们要将图片加载到 ImageView 的时候，最终会调用下面的 `buildTarget()` 方法来讲我们的 ImageView 封装成一个 `ViewTarget`，然后调用 `into()` 的重载方法进行后续处理：\n\n```java\n  public <Z> ViewTarget<ImageView, Z> buildTarget(ImageView view, Class<Z> clazz) {\n    if (Bitmap.class.equals(clazz)) {\n      return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view);\n    } else if (Drawable.class.isAssignableFrom(clazz)) {\n      return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view);\n    } else {\n      throw new IllegalArgumentException(\n          \"Unhandled class: \" + clazz + \", try .as*(Class).transcode(ResourceTranscoder)\");\n    }\n  }\n\n  private <Y extends Target<TranscodeType>> Y into(Y target,\n      RequestListener<TranscodeType> targetListener,\n      RequestOptions options) {\n\n    options = options.autoClone();\n    Request request = buildRequest(target, targetListener, options); // 1\n\n    Request previous = target.getRequest();\n    if (request.isEquivalentTo(previous)\n        && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {\n      request.recycle();\n      if (!Preconditions.checkNotNull(previous).isRunning()) {\n        previous.begin();\n      }\n      return target;\n    }\n\n    requestManager.clear(target);\n    target.setRequest(request);\n    requestManager.track(target, request); // 2\n\n    return target;\n  }\n```\n\n在上面的 `into()` 方法的 `1` 处最终会调用到下面的方法来构建一个请求对象。（这里我们忽略掉具体的参数，只给看构建请求的逻辑）。简而言之，该方法会根据我们是否调用过 `RequestBuilder` 的 `error()` 方法设置过图片加载出错时候显示的图片来决定返回 `mainRequest` 还是 `errorRequestCoordinator`。因为我们没有设置该参数，所以会直接返回 `mainRequest`。\n\t\n```java\n  private Request buildRequestRecursive(/*各种参数*/) {\n\n    ErrorRequestCoordinator errorRequestCoordinator = null;\n    if (errorBuilder != null) {\n      errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator);\n      parentCoordinator = errorRequestCoordinator;\n    }\n\n    Request mainRequest = buildThumbnailRequestRecursive(/*各种参数*/); // 1\n\n    if (errorRequestCoordinator == null) {\n      return mainRequest;\n    }\n\n    // ... 略\n\n    Request errorRequest = errorBuilder.buildRequestRecursive(/*各种参数*/);\n    errorRequestCoordinator.setRequests(mainRequest, errorRequest);\n    return errorRequestCoordinator;\n  }\n```\n\n上面是根据是否设置加载失败时显示的图片来决定返回的请求对象的。如果你使用过 Glide 的话，那么一定记得除了设置加载失败时的图片，我们还会先加载一张小图，即 `Thumbnail`。所以，在上面方法的 `1` 处会根据设置调用过 `RequestBuilder` 的 `thumbnail()` 方法来决定返回 `Thumbnail` 的请求还是真实图片的请求。同样因为我们没有设置过该方法，所以最终会调用下面的方法来构建最终的图片加载请求。\n\n```java\n  private Request obtainRequest(/*各种参数*/) {\n    return SingleRequest.obtain(/*各种参数*/);\n  }\n```\n\n在 `SingleRequest` 的 `obtain()` 方法中会先尝试从请求的池中取出一个请求，当请求不存在的时候就会实例化一个 `SingleRequest`，然后调用它的 `init()` 方法完成请求的初始化工作。这里的请求池使用了 Android 的 support v4 包中的 `Pool` 相关的 API. 它被设计用来构建基于数组的请求池，具体如何使用可以参考相关的文档和源码。\n\n```java\n  public static <R> SingleRequest<R> obtain(/*各种参数*/) {\n    SingleRequest<R> request = (SingleRequest<R>) POOL.acquire();\n    if (request == null) {\n      request = new SingleRequest<>();\n    }\n    request.init(/*各种参数*/);\n    return request;\n  }\n```\n\n得到了请求之后会用 `RequestManager` 的 `track()` 方法：\n\n```java\n  void track(@NonNull Target<?> target, @NonNull Request request) {\n    targetTracker.track(target);\n    requestTracker.runRequest(request);\n  }\n```\n\n该方法的主要作用有两个：\n\n1. 调用 `TargetTracker` 的 `track()` 方法对对当前 `Target` 的生命周期进行管理；\n2. 调用 `RequestTracker` 的 `runRequest()` 方法对当前请求进行管理，当 Glide 未处于暂停状态的时候，会直接使用 `Request` 的 `begin()` 方法开启请求。\n\n下面是 `SingeleRequest` 的 `begin()` 方法。它会根据当前加载的状态来判断应该调用哪个方法。因为我们之前图片加载的过程可能因为一些意想不到的原因被终止，所以当重启的时候就需要根据之前的状态进行恢复。对于我们第一次加载的情况，则会直接进入到下方 1 处的 `onSizeReady()` 方法中：\n\n```java\n  public void begin() {\n    if (model == null) {\n      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {\n        width = overrideWidth;\n        height = overrideHeight;\n      }\n      int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;\n      onLoadFailed(new GlideException(\"Received null model\"), logLevel);\n      return;\n    }\n\n    if (status == Status.RUNNING) {\n      throw new IllegalArgumentException(\"Cannot restart a running request\");\n    }\n\n    // 如果我们在完成之后重新启动（通常通过诸如 notifyDataSetChanged() 之类的方法，\n    // 在相同的目标或视图中启动相同的请求），我们可以使用我们上次检索的资源和大小\n    // 并跳过获取新的大小。所以，如果你因为 View 大小发生了变化而想要重新加载图片\n    // 就需要在开始新加载之前清除视图 (View) 或目标 (Target)。\n    if (status == Status.COMPLETE) {\n      onResourceReady(resource, DataSource.MEMORY_CACHE);\n      return;\n    }\n\n    status = Status.WAITING_FOR_SIZE;\n    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {\n      onSizeReady(overrideWidth, overrideHeight); // 1\n    } else {\n      target.getSize(this);\n    }\n\n    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)\n        && canNotifyStatusChanged()) {\n      target.onLoadStarted(getPlaceholderDrawable()); // 2\n    }\n  }\n```\n\n下面是 `onSizeReady()` 方法，我们可以看出它会先判断当前是否处于 `Status.WAITING_FOR_SIZE` 状态，并随后将状态更改为 `Status.RUNNING` 并调用 `engine` 的 `load()` 方法。显然，更改完状态之后继续回到上面的方法，在 2 处即调用了 `Target` 的 `onLoadStarted()` 方法。这样 `Target` 的第一个生命周期就被触发了。\n\n```java\n  public void onSizeReady(int width, int height) {\n    if (status != Status.WAITING_FOR_SIZE) {\n      return;\n    }\n    status = Status.RUNNING;\n\n    float sizeMultiplier = requestOptions.getSizeMultiplier();\n    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);\n    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);\n\n    loadStatus = engine.load(/*各种参数*/);\n\n    if (status != Status.RUNNING) {\n      loadStatus = null;\n    }\n  }\n```\n\n然后，让我们将重点放到 `Engine` 的 `load()` 方法。该方法虽然不长，但是却包含了许多重要的内容。我们在下篇文章中将要研究的 Glide 的缓存就是在这里实现的。该方法大致的逻辑上，先尝试从内存缓存当中查找指定的资源，当内存中不存在的时候就准备使用 `DecodeJob` 来加载图片。\n\n```java\n  public <R> LoadStatus load(/*各种参数*/) {\n    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,\n        resourceClass, transcodeClass, options);\n\n    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);\n    if (active != null) {\n      cb.onResourceReady(active, DataSource.MEMORY_CACHE);\n      return null;\n    }\n\n    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);\n    if (cached != null) {\n      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);\n      return null;\n    }\n\n    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);\n    if (current != null) {\n      current.addCallback(cb);\n      return new LoadStatus(cb, current);\n    }\n\n    EngineJob<R> engineJob = engineJobFactory.build(\n            key,\n            isMemoryCacheable,\n            useUnlimitedSourceExecutorPool,\n            useAnimationPool,\n            onlyRetrieveFromCache);\n\n    DecodeJob<R> decodeJob = decodeJobFactory.build(/*各种参数*/);\n\n    jobs.put(key, engineJob);\n\n    engineJob.addCallback(cb);\n    engineJob.start(decodeJob);\n\n    return new LoadStatus(cb, engineJob);\n  }\n```\n\n上面方法中涉及两个类，一个是 `DecodeJob`、一个是 `EngineJob`。它们之间的关系是，`EngineJob` 内部维护了线程池，用来管理资源加载，已经当资源加载完毕的时候通知回调。 `DecodeJob` 继承了 `Runnable`，是线程池当中的一个任务。就像上面那样，我们通过调用 `engineJob.start(decodeJob)` 来开始资源加载。\n\n#### 3.1.2 小结\n\n![阶段1：开启 DecodeJob 的过程](res/glide_into_stage1.jpg)\n\n根据上文中的分析，我们不难得出上面的流程图。不考虑缓存的问题，这个部分的逻辑还是比较清晰的，即：当调用了 `into()` 之后，首先构建一个请求对象 `SingleRequest`，然后调用 `RequestManager` 的 `track()` 方法对 `Request` 和 `Target` 进行管理；随后，使用 `Request` 的 `begin()` 方法来启动请求；该方法中会使用 `Engine` 的 `load()` 方法决定是从缓存当中获取资源还是从数据源中加载数据；如果是从数据源中加载数据的话，就构建一个 `DecodeJob` 交给 `EngineJob` 来执行即可。\n\n### 3.2 阶段2：打开网络流的过程\n\n#### 3.2.1 打开网络流的过程\n\n在上面的分析中，将 `DecodeJob` 交给 `EngineJob` 就完事了。因为 `DecodeJob` 是一个任务，会在线程池当中进行执行。所以，如果我们继续追踪的话，就应该从 `DecodeJob` 的 `run()` 方法开始：\n\n所以，如果想要找到加载资源和解码的逻辑，就应该查看 DecodeJob 的 `run()` 方法。下面就是这个方法的定义：\n\n```java\n  public void run() {\n    DataFetcher<?> localFetcher = currentFetcher;\n    try {\n      if (isCancelled) {\n        notifyFailed();\n        return;\n      }\n      runWrapped();\n    } catch (Throwable t) {\n      if (stage != Stage.ENCODE) {\n        throwables.add(t);\n        notifyFailed();\n      }\n      if (!isCancelled) {\n        throw t;\n      }\n    } finally {\n      if (localFetcher != null) {\n        localFetcher.cleanup();\n      }\n      GlideTrace.endSection();\n    }\n  }\n```\n\n`DecodeJob` 的执行过程使用了状态模式，它会根据当前的状态决定将要执行的方法。在上面的方法中，当当前任务没有被取消的话，会进入到 `runWrapped()` 方法。该方法中会使用 `runReason` 作为当前的状态决定要执行的逻辑：\n\n```java\n  private void runWrapped() {\n    switch (runReason) {\n      case INITIALIZE:\n        stage = getNextStage(Stage.INITIALIZE);\n        currentGenerator = getNextGenerator();\n        runGenerators();\n        break;\n      case SWITCH_TO_SOURCE_SERVICE:\n        runGenerators();\n        break;\n      case DECODE_DATA:\n        decodeFromRetrievedData();\n        break;\n      default:\n        throw new IllegalStateException(\"Unrecognized run reason: \" + runReason);\n    }\n  }\n```\n\n这里的 `runReason` 是一个枚举类型，它包含的枚举值即为上面的三种类型。当我们在一个过程执行完毕之后会回调 `DecodeJob` 中的方法修改 `runReason`，然后根据新的状态值执行新的逻辑。\n\n除了 `runReason`，`DecodeJob` 中还有一个变量 `stage` 也是用来决定 `DecodeJob` 状态的变量。同样，它也是一个枚举，用来表示将要加载数据的数据源以及数据的加载状态。它主要在加载数据的时候在 `runGenerators()`、`runWrapped()` 和 `getNextStage()` 三个方法中被修改。通常它的逻辑是，先从（大小、尺寸等）转换之后的缓存中拿数据，如果没有的话再从没有转换过的缓存中拿数据，最后还是拿不到的话就从原始的数据源中加载数据。\n\n以上就是 `DecodeJob` 中的状态模式运行的原理。\n\n对于一个新的任务，会在 `DecodeJob` 的 `init()` 方法中将 `runReason` 置为 `INITIALIZE`，所以，我们首先会进入到上述 `switch` 中的 `INITIALIZE` 中执行。然后，因为我们没有设置过磁盘缓存的策略，因此会使用默认的 `AUTOMATIC` 缓存方式。于是，我们将会按照上面所说的依次从各个缓存中拿数据。由于我们是第一次加载，并且暂时我们不考虑缓存的问题，所以，最终数据的加载会交给 `SourceGenerator` 进行。\n\n不知道你是否还记得上一篇文章中我们在讲解在 Glide 中使用 OkHttp 时提到的相关的类。它们真正作用的地方就在下面的这个方法中。这是 `SourceGenerator` 的 `startNext()` 方法，它会：\n\n1. 先使用 `DecodeHelper` 的 `getLoadData()` 方法从注册的映射表中找出当前的图片类型对应的 `ModelLoader`；\n2. 然后使用它的 `DataFetcher` 的 `loadData()` 方法从原始的数据源中加载数据。\n\n```java\n  public boolean startNext() {\n    if (dataToCache != null) {\n      Object data = dataToCache;\n      dataToCache = null;\n      cacheData(data);\n    }\n\n    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {\n      return true;\n    }\n    sourceCacheGenerator = null;\n\n    loadData = null;\n    boolean started = false;\n    while (!started && hasNextModelLoader()) {\n      loadData = helper.getLoadData().get(loadDataListIndex++);\n      if (loadData != null\n          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())\n          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {\n        started = true;\n        loadData.fetcher.loadData(helper.getPriority(), this);\n      }\n    }\n    return started;\n  }\n```\n\n由于我们的图片时网络中的资源，在默认情况下会使用 Glide 内部的 `HttpUrlFetcher` 从网络中加载数据。其 `loadData()` 方法定义如下：\n\n```java\n  public void loadData(Priority priority, DataCallback<? super InputStream> callback) {\n    try {\n      InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());\n      callback.onDataReady(result);\n    } catch (IOException e) {\n      callback.onLoadFailed(e);\n    } finally {\n    }\n  }\n```\n\n很明显，这里从网络中打开输入流之后得到了一个 `InputStream` 之后就使用回调将其返回了。至于 `loadDataWithRedirects()` 方法的实现，就是使用 `HttpURLConnection` 打开网络流的过程，这里我们不进行详细的说明了。\n\n#### 3.2.2 小结\n\n![阶段2：打开网络流的过程](res/glide_into_stage2.jpg)\n\n这样，`into()` 方法的第二个阶段，即从网络中获取一个输入流的过程就分析完毕了。整个过程并不算复杂，主要是在 `DecodeJob` 中的状态模式可能一开始看不太懂，还有就是其中涉及到的一些类不清楚其作用。如果你存在这两个疑惑的话，那么建议你：1).耐心思考下状态模式的转换过程；2).翻下上一篇文章了解自定义 Glide 图片加载方式的几个类的设计目的；3).最重要的，多看源码。\n\n### 3.3 阶段3：将输入流转换为 Drawable 的过程\n\n#### 3.3.1 转换 Drawable 的过程\n\n在上面的小节中我们已经打开了网络流，按照 Android 自身提供的 `BitmapFactory`，我们可以很容易地从输入流中得到 Drawable 不是？那么为什么这个转换的过程还要单独分为一个阶段呢？\n\n实际上，这里的转换过程并不比上面打开输入流的过程简单多少。这是因为它涉及转码和将图片转换成适合控件大小的过程。好了，下面就让我们来具体看一下这个过程都发生了什么吧！\n\n首先，从上面的 `loadData()`，我们可以看出当得到了输入流之后会回调 `onDataReady()` 方法。这个方法会一直从 `HttpUrlFetcher` 中一直回调到 `SourceGenerator` 中。这里它会使用默认的磁盘缓存策略判断数据是否可以缓存，并决定对数据进行缓存还是继续回调。\n\n```java\n  public void onDataReady(Object data) {\n    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();\n    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {\n      dataToCache = data;\n      cb.reschedule(); // 1\n    } else {\n      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,\n          loadData.fetcher.getDataSource(), originalKey);\n    }\n  }\n```\n\n因为我们的数据是使用 `HttpUrlFetcher` 加载的，所以将会进入到 1 处继续进行处理。此时，`DecodeJob` 将会根据当前的状态从 `run()` 方法开始执行一遍，并再次调用 `DataCacheGenerator` 的 `startNext()` 方法。但是，此次与上一次不同的地方在于，这次已经存在可以用于缓存的数据了。所以，下面的方法将会被触发：\n\n```java\n  private void cacheData(Object dataToCache) {\n    try {\n      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);\n      DataCacheWriter<Object> writer =\n          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());\n      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());\n      helper.getDiskCache().put(originalKey, writer);\n    } finally {\n      loadData.fetcher.cleanup();\n    }\n\n    sourceCacheGenerator =\n        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);\n  }\n```\n\n这里的主要逻辑是构建一个用于将数据缓存到磁盘上面的 `DataCacheGenerator`。`DataCacheGenerator` 的流程基本与 `SourceGenerator` 一致，也就是根据资源文件的类型找到 `ModelLoader`，然后使用 `DataFetcher` 加载缓存的资源。与之前不同的是，这次是用 `DataFecher` 来加载 `File` 类型的资源。也就是说，当我们从网络中拿到了数据之后 Glide 会先将其缓存到磁盘上面，然后再从磁盘上面读取图片并将其显示到控件上面。所以，当从网络打开了输入流之后 `SourceGenerator` 的任务基本结束了，而后的显示的任务都由 `DataCacheGenerator` 来完成。\n\n与 `HttpUrlFetcher` 一样，File 类型的资源将由 `ByteBufferFetcher` 来加载，当它加载完毕之后也也会回调 `onDataReady()` 方法。此时，将会调用 `DataCacheGenerator` 的 `onDataReady()`：\n\n```java\n  public void onDataReady(Object data) {\n    cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey);\n  }\n```\n\n该方法会继续回调到 `DecodeJob` 的 `onDataFetcherReady()` 方法，后续的逻辑比较清晰，只是在不断继续调用方法，我们依次给出这些方法：\n\n```java\n  // DecodeJob#onDataFetcherReady()\n  public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher<?> fetcher,\n      DataSource dataSource, Key attemptedKey) {\n    // ... 赋值，略\n    if (Thread.currentThread() != currentThread) {\n      runReason = RunReason.DECODE_DATA;\n      callback.reschedule(this);\n    } else {\n      try {\n        // decode 数据以得到期待的资源类型\n        decodeFromRetrievedData();\n      } finally {\n        GlideTrace.endSection();\n      }\n    }\n  }\n\n  // DecodeJob#decodeFromRetrievedData()\n  private void decodeFromRetrievedData() {\n    Resource<R> resource = null;\n    try {\n      resource = decodeFromData(currentFetcher, currentData, currentDataSource);\n    } catch (GlideException e) {\n      // ... 异常处理\n    }\n    // ... 释放资源和错误重试等\n  }\n\n  // DecodeJob#decodeFromData()\n  private <Data> Resource<R> decodeFromData(DataFetcher<?> fetcher, Data data,\n      DataSource dataSource) throws GlideException {\n    try {\n      // ... 略\n      Resource<R> result = decodeFromFetcher(data, dataSource);\n      return result;\n    } finally {\n      fetcher.cleanup();\n    }\n  }\n\n  // DecodeJob#decodeFromFetcher()\n  private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)\n      throws GlideException {\n    LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());\n    return runLoadPath(data, dataSource, path);\n  }\n\n  // DecodeJob#runLoadPath()\n  private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource,\n      LoadPath<Data, ResourceType, R> path) throws GlideException {\n    // ... 获取参数信息\n    try {\n      // 使用 LoadPath 继续处理\n      return path.load(\n          rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));\n    } finally {\n      rewinder.cleanup();\n    }\n  }\n\n  // LoadPath#load()\n  public Resource<Transcode> load(DataRewinder<Data> rewinder, @NonNull Options options, int width,\n      int height, DecodePath.DecodeCallback<ResourceType> decodeCallback) throws GlideException {\n    try {\n      // 继续加载\n      return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables);\n    } finally {\n      listPool.release(throwables);\n    }\n  }\n\n  // LoadPath#loadWithExceptionList()\n  private Resource<Transcode> loadWithExceptionList(/*各种参数*/) throws GlideException {\n    Resource<Transcode> result = null;\n    for (int i = 0, size = decodePaths.size(); i < size; i++) {\n      DecodePath<Data, ResourceType, Transcode> path = decodePaths.get(i);\n      try {\n        // 使用 DecodePath 继续处理\n        result = path.decode(rewinder, width, height, options, decodeCallback);\n      } catch (GlideException e) {\n        exceptions.add(e);\n      }\n      if (result != null) {\n        break;\n      }\n    }\n    return result;\n  }\n```\n\n经过了上面的一系列猛如虎的操作之后，我们进入了 `loadWithExceptionList()` 方法，这里会对 `DecodePath` 进行过滤，以得到我们期望的图片的类型。这个方法中调用了 `DecodePath` 的 `decode()` 方法。这个方法比较重要，它像一个岔路口：1 处的代码是将数据转换成我们期望的图片的过程；2 处的代码是当得到了期望的图片之后对处理继续处理并显示的过程。\n\n```java\n  // DecodePath#decode()\n  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,\n      Options options, DecodeCallback<ResourceType> callback) throws GlideException {\n    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1\n    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2\n    return transcoder.transcode(transformed, options);\n  }\n```\n\n然后，让我们继续沿着 `decodeResource()` 走。它会调用下面的这个循环对当前的数据类型和期望的、最终的图片类型匹配从而决定用来继续处理的 `ResourceDecoder`。\n\n```java\n  private Resource<ResourceType> decodeResourceWithList(/*各种参数*/) throws GlideException {\n    Resource<ResourceType> result = null;\n    for (int i = 0, size = decoders.size(); i < size; i++) {\n      ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);\n      try {\n        DataType data = rewinder.rewindAndGet();\n        if (decoder.handles(data, options)) {\n          data = rewinder.rewindAndGet();\n          result = decoder.decode(data, width, height, options);\n        }\n      } catch (IOException | RuntimeException | OutOfMemoryError e) {\n        exceptions.add(e);\n      }\n\n      if (result != null) {\n        break;\n      }\n    }\n    return result;\n  }\n```\n\n`ResourceDecoder` 具有多个实现类，比如 `BitmapDrawableDecoder`、`ByteBufferBitmapDecoder`等。从名字也可以看出来是用来将一个类型转换成另一个类型的。\n\n在我们的程序中会使用 `ByteBufferBitmapDecoder` 来将 `ByteBuffer` 专成 `Bitmap`。它最终会在 `Downsampler` 的 `decodeStream()` 方法中调用 `BitmapFactory` 的 `decodeStream()` 方法来从输入流中得到 Bitmap。（我们的 `ByteBuffer` 在  `ByteBufferBitmapDecoder` 中先被转换成了输入流。）\n\n```java\n  private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options,\n      DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {\n    // ... 略\n    TransformationUtils.getBitmapDrawableLock().lock();\n    try {\n      result = BitmapFactory.decodeStream(is, null, options);\n    } catch (IllegalArgumentException e) {\n      // ... 错误处理，略\n    } finally {\n      TransformationUtils.getBitmapDrawableLock().unlock();\n    }\n\n    if (options.inJustDecodeBounds) {\n      is.reset();\n    }\n    return result;\n  }\n```\n\n这样剩下的就只有不断继续向上回调或者返回，最终回到了我们上面所说的岔路口。这样从输入流中加载图片的逻辑就结束了:)\n\n#### 3.3.2 小结\n\n![阶段3：将输入流转换为 Drawable 的过程](res/glide_into_stage3.jpg)\n\n怎么样，是不是觉得这个过程比打开输入流的过程复杂多了？毕竟这个部分涉及到了从缓存当中取数据以及向缓存写数据的过程，算的上是核心部分了。整体而言，这部分的设计还是非常巧的，即使用了状态模式，根据当前的状态来决定下一个 `Generator`。从网络中拿到输入流之后又使用 `DataCacheGenerator` 从缓存当中读取数据，这个过程连我第一次读源码的时候都没发现，以至于后来调试验证了推理之后才确信这部分是这样设计的……\n\n### 3.4 阶段4：将 Drawable 展示到 ImageView 的过程\n\n根据上面的分析，我们已经从网络中得到了图片数据，并且已经将其放置到了缓存中，又从缓存当中取出数据进行准备进行显示。上面的过程比较复杂，下面将要出场的这个阶段也并不轻松……\n\n#### 3.4.1 最终展示图片的过程\n\n在上面分析中，我们已经进入到了之前所谓的岔路口，这里我们再给出这个方法的定义如下。上面的分析到了代码 1 处，现在我们继续从代码 2 处进行分析。\n\n```java\n  // DecodePath#decode()\n  public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, int height,\n      Options options, DecodeCallback<ResourceType> callback) throws GlideException {\n    Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options); // 1\n    Resource<ResourceType> transformed = callback.onResourceDecoded(decoded); // 2\n    return transcoder.transcode(transformed, options); // 3\n  }\n```\n\n这里会调用 `callback` 的方法进行回调，它最终会回调到 `DecodeJob` 的 `onResourceDecoded()` 方法。其主要的逻辑是根据我们设置的参数进行变化，也就是说，如果我们使用了 `centerCrop` 等参数，那么这里将会对其进行处理。这里的 `Transformation` 是一个接口，它的一系列的实现都是对应于 `scaleType` 等参数的。\n\n```java\n  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {\n    Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();\n    Transformation<Z> appliedTransformation = null;\n    Resource<Z> transformed = decoded;\n    // 对得到的图片资源进行变换\n    if (dataSource != DataSource.RESOURCE_DISK_CACHE) {\n      appliedTransformation = decodeHelper.getTransformation(resourceSubClass);\n      transformed = appliedTransformation.transform(glideContext, decoded, width, height);\n    }\n    if (!decoded.equals(transformed)) {\n      decoded.recycle();\n    }\n\n    // ... 缓存相关的逻辑，略\n    return result;\n  }\n```\n\n在上面的方法中对图形进行变换之后还会根据图片的缓存策略决定对图片进行缓存。然后这个方法就直接返回了我们变换之后的图象。这样我们就又回到了之前的岔路口。程序继续执行就到了岔路口方法的第 3 行。这里还会使用 `BitmapDrawableTranscoder` 的 `transcode()` 方法返回 `Resouces<BitmapDrawable>`。只是这里会使用 `BitmapDrawableTranscoder` 包装一层，即做了延迟初始化处理。\n\n这样，当第 3 行方法也执行完毕，我们的岔路口方法就分析完了。然后就是不断向上 `return` 进行返回。所以，我们又回到了 `DecodeJob` 的 `decodeFromRetrievedData()` 方法如下。这里会进入到下面方法的 1 处来完成最终的图片显示操作。\n\n```java\n  private void decodeFromRetrievedData() {\n    Resource<R> resource = null;\n    try {\n      resource = decodeFromData(currentFetcher, currentData, currentDataSource);\n    } catch (GlideException e) {\n      throwables.add(e);\n    }\n    if (resource != null) {\n      notifyEncodeAndRelease(resource, currentDataSource); // 1\n    } else {\n      runGenerators();\n    }\n  }\n```\n\n接着程序会达到 `DecodeJob` 的 `onResourceReady()` 方法如下。因为达到下面的方法的过程的逻辑比较简单，我们就不贴出这部分的代码了。\n\n```java\n  public void onResourceReady(Resource<R> resource, DataSource dataSource) {\n    this.resource = resource;\n    this.dataSource = dataSource;\n    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();\n  }\n```\n\n这里会获取到一个消息并将其发送到 `Handler` 中进行处理。当 `Handler` 收到消息之后会调用 `EncodeJob` 的 `handleResultOnMainThread()` 方法继续处理：\n\n```java\n  void handleResultOnMainThread() {\n    // ... 略\n    engineResource = engineResourceFactory.build(resource, isCacheable);\n    hasResource = true;\n\n    engineResource.acquire();\n    listener.onEngineJobComplete(this, key, engineResource);\n\n    for (int i = 0, size = cbs.size(); i < size; i++) {\n      ResourceCallback cb = cbs.get(i);\n      if (!isInIgnoredCallbacks(cb)) {\n        engineResource.acquire();\n        cb.onResourceReady(engineResource, dataSource); // 1\n      }\n    }\n    engineResource.release();\n\n    release(false /*isRemovedFromQueue*/);\n  }\n```\n\n经过一系列的判断之后程序进入到代码 1 处，然后继续进行回调。这里的 `cb` 就是 `SingeleRequest`。\n\n程序到了 `SingleRequest` 的方法中之后在下面的代码 1 处回调 `Target` 的方法。而这里的 `Target` 就是我们之前所说的 `ImageViewTarget`.\n\n```java\n  private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {\n    boolean isFirstResource = isFirstReadyResource();\n    status = Status.COMPLETE;\n    this.resource = resource;\n\n    isCallingCallbacks = true;\n    try {\n      // ... 略\n\n      if (!anyListenerHandledUpdatingTarget) {\n        Transition<? super R> animation =\n            animationFactory.build(dataSource, isFirstResource);\n        target.onResourceReady(result, animation); // 1\n      }\n    } finally {\n      isCallingCallbacks = false;\n    }\n\n    notifyLoadSuccess();\n  }\n```\n\n当程序到了 `ImageViewTarget` 之后会使用 `setResource()` 方法最终调用 `ImageView` 的方法将 `Drawable` 显示到控件上面。\n\n```java\n  protected void setResource(@Nullable Drawable resource) {\n    view.setImageDrawable(resource);\n  }\n```\n\n这样，我们的 Glide 的加载过程就结束了。\n\n#### 3.4.2 小结\n\n![阶段4：将 Drawable 展示到 ImageView 的过程](res/glide_into_stage4.jpg)\n\n上面是我们将之前得到的 `Drawable`  显示到控件上面的过程。这个方法包含了一定的逻辑，涉及的代码比较多，但是整体的逻辑比较简单，所以这部分的篇幅并不长。\n\n### 4、总结\n\n以上的内容便是我们的 Glide 加载图片的整个流程。从文章的篇幅和涉及的代码也可以看出，整个完整的过程是比较复杂的。从整体来看，Glide 之前启动和最终显示图片的过程比较简单、逻辑也比较清晰。最复杂的地方也是核心的地方在于 `DecodeJob` 的状态切换。\n\n上面的文章中，我们重点梳理图片加载的整个流程，对于图片缓存和缓存的图片的加载的过程我没有做过多的介绍。我们会在下一篇文章中专门来介绍这部分内容。\n\n以上。\n"
  },
  {
    "path": "图片加载/Glide系列：Glide的缓存的实现原理.md",
    "content": "# Glide 系列-3：Glide 缓存的实现原理（4.8.0）\n\n## 1、在 Glide 中配置缓存的方式\n\n首先，我们可以在自定义的 GlideModule 中制定详细的缓存策略。即在 `applyOptions()` 中通过直接调用 `GlideBuilder` 的方法来指定缓存的信息：\n\n```java\n    @Override\n    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {\n        builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE));\n        builder.setMemoryCache(...);\n        builder.setDiskCache(...);\n        // ... 略\n    }\n```\n\n另外，我们在每个图片加载请求中自定义当前图片加载请求的缓存策略，\n\n```java\n    Glide.with(getContext())\n        .load(\"https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png\")\n        .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC))\n        .apply(RequestOptions.skipMemoryCacheOf(false))\n        .into(getBinding().iv);\n```\n\n以上是两个比较常用的缓存的配置方式，具体的 API 可以查看相关的源码了解. \n\n不论 Glide 还是其他的框架的缓存无非就是基于内存的缓存和基于磁盘的缓存两种，而且缓存的管理算法基本都是 LRU. 针对内存缓存，Android 中提供了 `LruCache`，笔者在之前的文章中曾经分析过这个框架：\n\n[《Android 内存缓存框架 LruCache 的源码分析》](https://juejin.im/post/5bea581be51d451402494af2)\n\n至于磁盘缓存， Glide 和 OkHttp 都是基于 [DiskLruCache](https://github.com/JakeWharton/DiskLruCache) 进行了封装。这个框架本身的逻辑并不复杂，只是指定了一系列缓存文件的规则，读者可以自行查看源码学习。本文中涉及上述两种框架的地方不再详细追究缓存框架的源码。\n\n## 2、Glide 缓存的源码分析\n\n### 2.1 缓存配置\n\n首先, 我们在 `applyOptions()` 方法中的配置会在实例化单例的 Glide 对象的时候被调用. 所以, 这些方法的作用范围是全局的, 对应于整个 Glide.  下面的方法是 `RequestBuilder` 的 `build()` 方法, 也就是我们最终完成构建 Glide 的地方. 我们可以在这个方法中了解 `RequestBuilder` 为我们提供了哪些与缓存相关的方法. 以及默认的缓存配置.\n\n```java\n  Glide build(@NonNull Context context) {\n    // ... 无关代码, 略\n\n    if (diskCacheExecutor == null) {\n      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();\n    }\n\n    if (memorySizeCalculator == null) {\n      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();\n    }\n\n    if (bitmapPool == null) {\n      int size = memorySizeCalculator.getBitmapPoolSize();\n      if (size > 0) {\n        bitmapPool = new LruBitmapPool(size);\n      } else {\n        bitmapPool = new BitmapPoolAdapter();\n      }\n    }\n\n    if (arrayPool == null) {\n      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());\n    }\n\n    if (memoryCache == null) { // 默认的缓存配置\n      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());\n    }\n\n    if (diskCacheFactory == null) {\n      diskCacheFactory = new InternalCacheDiskCacheFactory(context);\n    }\n\n    if (engine == null) {\n      engine = new Engine(/*各种参数*/);\n    }\n\n    return new Glide(/*各种方法*/);\n  }\n```\n\n这里我们对 `MemorySizeCalculator` 这个参数进行一些说明. 顾名思义, 它是缓存大小的计算器, 即用来根据当前设备的环境计算可用的缓存空间 (主要针对的时基于内存的缓存).\n\n```java\n  MemorySizeCalculator(MemorySizeCalculator.Builder builder) {\n    this.context = builder.context;\n\n    arrayPoolSize =\n        isLowMemoryDevice(builder.activityManager)\n            ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR\n            : builder.arrayPoolSizeBytes;\n    // 计算APP可申请最大使用内存，再乘以乘数因子，内存过低时乘以0.33，一般情况乘以0.4\n    int maxSize =\n        getMaxSize(\n            builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);\n\n    // ARGB_8888 ,每个像素占用4个字节内存\n    // 计算屏幕这么大尺寸的图片占用内存大小\n    int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;\n    // 计算目标位图池内存大小\n    int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);\n    // 计算目标Lrucache内存大小，也就是屏幕尺寸图片大小乘以2\n    int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);\n    // 最终APP可用内存大小\n    int availableSize = maxSize - arrayPoolSize;\n    if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {\n      // 如果目标位图内存大小+目标Lurcache内存大小小于APP可用内存大小，则OK\n      memoryCacheSize = targetMemoryCacheSize;\n      bitmapPoolSize = targetBitmapPoolSize;\n    } else {\n      // 否则用APP可用内存大小等比分别赋值\n      float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);\n      memoryCacheSize = Math.round(part * builder.memoryCacheScreens);\n      bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);\n    }\n  }\n```\n\n### 2.2 内存缓存\n\n对于, 每个加载请求时对应的 `DiskCacheStrategy` 的设置, 我们之前的文章中已经提到过它的作用位置, 你可以参考之前的文章了解,\n\n[《Glide 系列-2：主流程源码分析（4.8.0）》](https://juejin.im/post/5c31fbdff265da610e803d4e)\n\n `DiskCacheStrategy` 的作用位置恰好也是 Glide 的缓存最初发挥作用的地方, 即 Engine 的 `load()` 方法. 这里我们只保留了与缓存相关的逻辑, 从下面的方法中也可以看出, 当根据各个参数构建了用于缓存的键之后先后从两个缓存当中加载数据, 拿到了数据之后就进行回调, 否则就需要从原始的数据源中加载数据. \n\n```java\n  public <R> LoadStatus load(/*各种参数*/) {\n    // 根据请求参数得到缓存的键\n    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,\n        resourceClass, transcodeClass, options);\n\n    // 检查内存中弱引用是否有目标图片\n    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); // 1\n    if (active != null) {\n      cb.onResourceReady(active, DataSource.MEMORY_CACHE);\n      return null;\n    }\n\n    // 检查内存中Lrucache是否有目标图片\n    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); // 2\n    if (cached != null) {\n      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);\n      return null;\n    }\n\n    // ...内存中没有图片构建任务往下执行, 略\n\n    return new LoadStatus(cb, engineJob);\n  }\n```\n\n这里存在两个方法，即 1 处的从弱引用中获取缓存数据，以及 2 处的从内存缓存中获取缓存数据。它们两者之间有什么区别呢？\n\n1. 弱引用的缓存会在内存不够的时候被清理掉，而基于 LruCache 的内存缓存是强引用的，因此不会因为内存的原因被清理掉。LruCache 只有当缓存的数据达到了缓存空间的上限的时候才会将最近最少使用的缓存数据清理出去。\n2. 两个缓存的实现机制都是基于哈希表的，只是 LruCahce 除了具有哈希表的数据结构还维护了一个链表。而弱引用类型的缓存的键与 LruCache 一致，但是值是弱引用类型的。\n3. 除了内存不够的时候被释放，弱引用类型的缓存还会在 Engine 的资源被释放的时候清理掉。\n4. 基于弱引用的缓存是一直存在的，无法被用户禁用，但用户可以关闭基于 LruCache 的缓存。\n5. 本质上基于弱引用的缓存与基于 LruCahce 的缓存针对于不同的应用场景，弱引用的缓存算是缓存的一种类型，只是这种缓存受可用内存的影响要大于 LruCache. \n\n接下来让我们先看下基于弱引用的缓存相关的逻辑，从上面的 1 处的代码开始：\n\n```java\n  // Engine#loadFromActiveResources\n  private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {\n    if (!isMemoryCacheable) {\n      return null;\n    }\n    EngineResource<?> active = activeResources.get(key); // 1\n    if (active != null) {\n      active.acquire(); // 2\n    }\n    return active;\n  }\n\n  // ActiveResources#get()\n  EngineResource<?> get(Key key) {\n    ResourceWeakReference activeRef = activeEngineResources.get(key);\n    if (activeRef == null) {\n      return null;\n    }\n    EngineResource<?> active = activeRef.get();\n    if (active == null) {\n      cleanupActiveReference(activeRef); // 3\n    }\n    return active;\n  }\n\n  // ActiveResources#cleanupActiveReference()\n  void cleanupActiveReference(@NonNull ResourceWeakReference ref) {\n    activeEngineResources.remove(ref.key);\n    if (!ref.isCacheable || ref.resource == null) { // 4\n      return;\n    }\n    EngineResource<?> newResource =\n        new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false);\n    newResource.setResourceListener(ref.key, listener);\n    listener.onResourceReleased(ref.key, newResource); // 5\n  }\n\n  // Engine#onResourceReleased()\n  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {\n    Util.assertMainThread();\n    activeResources.deactivate(cacheKey);\n    if (resource.isCacheable()) {\n      cache.put(cacheKey, resource); // 将数据缓存到 LruCahce\n    } else {\n      resourceRecycler.recycle(resource);\n    }\n  }\n```\n\n这里的 1 处会先调用 ActiveResources 的 `get()` 从弱引用中拿数据。当拿到了数据之后调用 `acquire()` 方法将 `EngineResource` 的引用计数加 1. 当这个资源被释放的时候，又会将引用计数减 1（参考 EngineResource 的 `release()` 方法）. \n \n当发现了弱引用中引用的 `EngineResource` 不存在的时候会在 3 处执行一次清理的逻辑。并在 5 处调用回调接口将弱引用中缓存的数据缓存到 LruCache 里面。\n\n这里在将数据缓存之前会先在 4 处判断缓存是否可用。这里使用到了 `isCacheable` 这个字段。通过查看源码我们可以追踪到这个字段最初传入的位置是在 `RequestOptions` 里面。也就是说，这个字段是针对一次请求的，我们可以在构建 Glide 请求的时候通过 `apply()` 设置这个参数的值（这个字段默认是 `true`，也就是默认是启用内存缓存的）。\n\n```java\n  Glide.with(getContext())\n    .load(\"https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png\")\n    .apply(RequestOptions.skipMemoryCacheOf(false)) // 不忽略内存缓存，即启用\n    .into(getBinding().iv);\n```\n\n### 2.3 磁盘缓存\n\n上面介绍了内存缓存，下面我们分析一下磁盘缓存。\n\n正如我们最初的示例那样，我们可以通过在构建请求的时候指定缓存的策略。我们的图片加载请求会得到一个 `RequestOptions`，我们通过查看该类的代码也可以看出，默认的缓存策略是 `AUTOMATIC` 的。\n\n这里的 `AUTOMATIC` 定义在 `DiskCacheStrategy` 中，除了 `AUTOMATIC` 还有其他几种缓存策略，那么它们之间又有什么区别呢？\n\n1. `ALL`：既缓存原始图片，也缓存转换过后的图片；对于远程图片，缓存 `DATA` 和 `RESOURCE`；对于本地图片，只缓存 `RESOURCE`。\n2. `AUTOMATIC` (默认策略)：尝试对本地和远程图片使用最佳的策略。当你加载远程数据（比如，从 `URL` 下载）时，`AUTOMATIC` 策略仅会存储未被你的加载过程修改过 (比如，变换、裁剪等) 的原始数据（`DATA`），因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据，`AUTOMATIC` 策略则会仅存储变换过的缩略图（`RESOURCE`），因为即使你需要再次生成另一个尺寸或类型的图片，取回原始数据也很容易。\n3. `DATA`：只缓存未被处理的文件。我的理解就是我们获得的 `stream`。它是不会被展示出来的，需要经过装载 `decode`，对图片进行压缩和转换，等等操作，得到最终的图片才能被展示。\n4. `NONE`：表示不缓存任何内容。\n5. `RESOURCE`：表示只缓存转换过后的图片（也就是经过decode，转化裁剪的图片）。\n\n那么这些缓存的策略是在哪里使用到的呢？回顾上一篇文章，首先，我们是在 `DecodeJob` 的状态模式中用到了磁盘缓存策略：\n\n```java\n  private Stage getNextStage(Stage current) {\n    switch (current) {\n      case INITIALIZE:\n        // 是否解码缓存的转换图片，就是只做过变换之后的缓存数据\n        return diskCacheStrategy.decodeCachedResource() ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);\n      case RESOURCE_CACHE:\n        // 是否解码缓存的原始数据，就是指缓存的未做过变换的数据\n        return diskCacheStrategy.decodeCachedData() ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);\n      case DATA_CACHE:\n        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;\n      case SOURCE:\n      case FINISHED:\n        return Stage.FINISHED;\n      default:\n        throw new IllegalArgumentException(\"Unrecognized stage: \" + current);\n    }\n  }\n\n  private DataFetcherGenerator getNextGenerator() {\n    switch (stage) {\n      case RESOURCE_CACHE:\n        return new ResourceCacheGenerator(decodeHelper, this);\n      case DATA_CACHE:\n        return new DataCacheGenerator(decodeHelper, this);\n      case SOURCE:\n        return new SourceGenerator(decodeHelper, this);\n      case FINISHED:\n        return null;\n      default:\n        throw new IllegalStateException(\"Unrecognized stage: \" + stage);\n    }\n  }\n```\n\n首先会根据当前所处的阶段 `current` 以及缓存策略判断应该使用哪个 `DataFetcherGenerator` 加载数据。我们分别来看一下它们：\n\n首先是 `ResourceCacheGenerator`，它用来从缓存中得到变换之后数据。当从缓存中拿数据的时候会调用到它的 `startNext()` 方法如下。从下面的方法也可以看出，当从缓存中拿数据的时候会先在代码 1 处构建一个用于获取缓存数据 key。在构建这个 key 的时候传入了图片大小、变换等各种参数，即根据各种变换后的条件获取缓存数据。因此，这个类是用来获取变换之后的缓存数据的。\n\n```java\n  public boolean startNext() {\n    List<Key> sourceIds = helper.getCacheKeys();\n    if (sourceIds.isEmpty()) {\n      return false;\n    }\n    List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();\n    if (resourceClasses.isEmpty()) {\n      if (File.class.equals(helper.getTranscodeClass())) {\n        return false;\n      }\n    }\n    while (modelLoaders == null || !hasNextModelLoader()) {\n      resourceClassIndex++;\n      if (resourceClassIndex >= resourceClasses.size()) {\n        sourceIdIndex++;\n        if (sourceIdIndex >= sourceIds.size()) {\n          return false;\n        }\n        resourceClassIndex = 0;\n      }\n\n      Key sourceId = sourceIds.get(sourceIdIndex);\n      Class<?> resourceClass = resourceClasses.get(resourceClassIndex);\n      Transformation<?> transformation = helper.getTransformation(resourceClass);\n      currentKey =\n          new ResourceCacheKey( // 1 构建获取缓存信息的键\n              helper.getArrayPool(),\n              sourceId,\n              helper.getSignature(),\n              helper.getWidth(),\n              helper.getHeight(),\n              transformation,\n              resourceClass,\n              helper.getOptions());\n      cacheFile = helper.getDiskCache().get(currentKey); // 2 从缓存中获取缓存信息\n      if (cacheFile != null) {\n        sourceKey = sourceId;\n        modelLoaders = helper.getModelLoaders(cacheFile);\n        modelLoaderIndex = 0;\n      }\n    }\n\n    loadData = null;\n    boolean started = false;\n    while (!started && hasNextModelLoader()) {\n      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++); // 3 使用文件方式从缓存中读取缓存数据\n      loadData = modelLoader.buildLoadData(cacheFile,\n          helper.getWidth(), helper.getHeight(), helper.getOptions());\n      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {\n        started = true;\n        loadData.fetcher.loadData(helper.getPriority(), this);\n      }\n    }\n\n    return started;\n  }\n```\n\n当找到了缓存的值之后会使用 `File` 类型的 `ModelLoader` 加载数据。这个比较容易理解，因为数据存在磁盘上面，需要用文件的方式打开。\n\n另外，我们再关注下 2 处的代码，它会使用 `helper` 的 `getDiskCache()` 方法获取 `DiskCache` 对象。我们一直追踪这个对象就会找到一个名为 `DiskLruCacheWrapper` 的类，它内部包装了 `DiskLruCache`。所以，最终从磁盘加载数据是使用 `DiskLruCache` 来实现的。对于最终使用 `DiskLruCache` 获取数据的逻辑我们不进行说明了，它的逻辑并不复杂，都是单纯的文件读写，只是设计了一套缓存的规则。\n\n上面是从磁盘读取数据的，那么数据又是在哪里向磁盘缓存数据的呢？\n\n在之前的文章中我们也分析过这部分内容，即当从网络中打开输入流之后会回到 `DecodeJob` 中，进入下一个阶段，并再次调用 `SourceGenerator` 的 `startNext()` 方法。此时会进入到 `cacheData()` 方法，并将数据缓存到磁盘上：\n\n```java\n  private void cacheData(Object dataToCache) {\n    long startTime = LogTime.getLogTime();\n    try {\n      Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);\n      DataCacheWriter<Object> writer =\n          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());\n      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());\n      helper.getDiskCache().put(originalKey, writer); // 将数据缓存到磁盘上面\n    } finally {\n      loadData.fetcher.cleanup();\n    }\n\n    sourceCacheGenerator =\n        new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);\n  }\n```\n\n然后构建一个 `DataCacheGenerator` 再从磁盘上面读取出缓存的数据，显示到控件上面。\n\n还有一个问题，从上文中我们也可以看出 Glide 在进行缓存的时候可以缓存转换之后的数据，也可以缓存原始的数据。我们可以通过构建的用于获取缓存的键看出这一点：在 `ResourceCacheGenerator` 中获取转换之后的缓存数据的时候，我们使用 `ResourceCacheKey` 并传入了各种参数构建了缓存的键；在将数据存储到磁盘上面的时候我们使用的是 `DataCacheKey`，并且没有传入那么多参数。这说明获取的和存储的并不是同一份数据，那么转换之后的数据是在哪里缓存的呢？\n\n我们通过查找类 `ResourceCacheKey` 将位置定位在了 `DecodeJob` 的 `onResourceDecoded()` 方法中：\n\n```java\n  <Z> Resource<Z> onResourceDecoded(DataSource dataSource, Resource<Z> decoded) {\n    // ... 略\n\n    Resource<Z> result = transformed;\n    boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);\n    if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource,\n        encodeStrategy)) {\n      if (encoder == null) {\n        throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());\n      }\n      final Key key;\n      // 根据缓存的此略使用不同的缓存的键\n      switch (encodeStrategy) {\n        case SOURCE:\n          key = new DataCacheKey(currentSourceKey, signature);\n          break;\n        case TRANSFORMED:\n          key =\n              new ResourceCacheKey(\n                  decodeHelper.getArrayPool(),\n                  currentSourceKey,\n                  signature,\n                  width,\n                  height,\n                  appliedTransformation,\n                  resourceSubClass,\n                  options);\n          break;\n        default:\n          throw new IllegalArgumentException(\"Unknown strategy: \" + encodeStrategy);\n      }\n\n      LockedResource<Z> lockedResult = LockedResource.obtain(transformed);\n      // 将缓存的键和数据信息设置到 deferredEncodeManager 中，随后会将其缓存到磁盘上面\n      deferredEncodeManager.init(key, encoder, lockedResult);\n      result = lockedResult;\n    }\n    return result;\n  }\n```\n\n显然，这里会根据缓存的策略构建两种不同的 key，并将其传入到 `deferredEncodeManager` 中。然后将会在 `DecodeJob` 的 `notifyEncodeAndRelease()` 方法中调用 `deferredEncodeManager` 的 `encode()` 方法将数据缓存到磁盘上:\n\n```java\n    void encode(DiskCacheProvider diskCacheProvider, Options options) {\n      try {\n        // 将数据缓存到磁盘上面\n        diskCacheProvider.getDiskCache().put(key,\n            new DataCacheWriter<>(encoder, toEncode, options));\n      } finally {\n        toEncode.unlock();\n      }\n    }\n```\n\n以上就是 Glide 的磁盘缓存的实现原理。\n\n### 3、总结\n\n在这篇文中我们在之前的两篇文章的基础之上分析了 Glide 的缓存的实现原理。\n\n首先 Glide 存在两种内存缓存，一个基于弱引用的，一个是基于 LruCache 的。两者存在一些不同，在文中我们已经总结了这部分内容。\n\n然后，我们分析了 Glide 的磁盘缓存的实现原理。Glide 的磁盘缓存使用了策略模式，存在 4 种既定的缓存策略。Glide 不仅可以原始的数据缓存到磁盘上面，还可以将做了转换之后的数据缓存到磁盘上面。它们会基于自身的缓存方式构建不同的 key 然后底层使用 DiskLruCache 从磁盘种获取数据。这部分的核心代码在 `DecodeJob` 和三个 `DataFetcherGenerator` 中。\n\n以上就是 Glide 缓存的所有实现原理。\n"
  },
  {
    "path": "图片加载/Glide系列：Glide的配置和使用方式.md",
    "content": "# Glide 系列-1：预热、Glide 的常用配置方式及其原理\n\n在接下来的几篇文章中，我们会对 Android 中常用的图片加载框架 Glide 进行分析。在本篇文章中，我们先通过介绍 Glide 的几种常用的配置方式来了解 Glide 的部分源码。后续的文中，我们会对 Glide 的源码进行更详尽的分析。\n\n对于 Glide，相信多数 Android 开发者并不陌生，在本文中，我们不打算对其具体使用做介绍，你可以通过查看官方文档进行学习。Glide 的 API 设计非常人性化，上手也很容易。\n\n在这篇文中中我们主要介绍两种常用的 Glide 的配置方式，并以此为基础来分析 Glide 的工作原理。在本文中我们将会介绍的内容有：\n\n1. 通过自定义 GlideModule 指定 Glide 的缓存路径和缓存空间的大小；\n2. 带有时间戳的图片的缓存命中问题的解决；\n3. 在 Glide 中使用 OkHttp 作为网络中的图片资源加载方式的实现。\n\n## 1、自定义图片加载方式\n\n有时候，我们需要对 Glide 进行配置来使其能够对特殊类型的图片进行加载和缓存。考虑这么一个场景：图片路径中带有时间戳。这种情形比较场景，即有时候我们通过为图片设置时间戳来让图片链接在指定的时间过后失效，从而达到数据保护的目的。\n\n在这种情况下，我们需要解决几个问题：1).需要配置缓存的 key，不然缓存无法命中，每次都需要从网络中进行获取；2).根据正确的链接，从网络中获取图片并展示。\n\n我们可以使用自定义配置 Glide 的方式来解决这个问题。\n\n### 1.1 带时间戳图片加载的实现\n\n#### 1.1.1 MyAppGlideModule\n\n首先，按照下面的方式自定义 `GlideModule`，\n\n```java\n    @GlideModule\n    public class MyAppGlideModule extends AppGlideModule {\n\n        /**\n        * 配置图片缓存的路径和缓存空间的大小\n        */\n        @Override\n        public void applyOptions(Context context, GlideBuilder builder) {\n            builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20));\n        }\n\n        /**\n        * 注册指定类型的源数据，并指定它的图片加载所使用的 ModelLoader\n        */\n        @Override\n        public void registerComponents(Context context, Glide glide, Registry registry) {\n            glide.getRegistry().append(CachedImage.class, InputStream.class, new ImageLoader.Factory());\n        }\n\n        /**\n        * 是否启用基于 Manifest 的 GlideModule，如果没有在 Manifest 中声明 GlideModule，可以通过返回 false 禁用\n        */\n        @Override\n        public boolean isManifestParsingEnabled() {\n            return false;\n        }\n    }\n```\n\n在上面的代码中，我们通过覆写 `registerComponents()` 方法，并调用 Glide 的 `Registry` 的 `append()` 方法来向 Glide **增加**我们的自定义图片类型的加载方式。（如果替换某种资源加载方式则需要使用 `replace()` 方法，此外 `Registry` 还有其他的方法，可以通过查看源码进行了解。）\n\n在上面的方法中，我们新定义了两个类，分别是 `CachedImage` 和 `ImageLoader`。`CachedImage` 就是我们的自定义资源类型，`ImageLoader` 是该资源类型的加载方式。当进行图片加载的时候，会根据资源的类型找到该图片加载方式，然后使用它来进行图片加载。\n\n#### 1.1.2 CachedImage\n\n我们通过该类的构造方法将原始的图片的链接传入，并通过该类的 `getImageId()` 方法来返回图片缓存的键，在该方法中我们从图片链接中过滤掉时间戳：\n\n```java\n    public class CachedImage {\n\n        private final String imageUrl;\n\n        public CachedImage(String imageUrl) {\n            this.imageUrl = imageUrl;\n        }\n\n        /**\n        * 原始的图片的 url，用来从网络中加载图片\n        */\n        public String getImageUrl() {\n            return imageUrl;\n        }\n\n        /**\n        * 提取时间戳之前的部分作为图片的 key，这个 key 将会被用作缓存的 key，并用来从缓存中找缓存数据\n        */\n        public String getImageId() {\n            if (imageUrl.contains(\"?\")) {\n                return imageUrl.substring(0, imageUrl.lastIndexOf(\"?\"));\n            } else {\n                return imageUrl;\n            }\n        }\n    }\n```\n\n#### 1.1.3 ImageLoader\n\n`CachedImage` 的加载通过 `ImageLoader` 实现。正如上面所说的，我们将 `CachedImage` 的 `getImageId()` 方法得到的字符串作为缓存的键，然后使用默认的 `HttpUrlFetcher` 作为图片的加载方式。\n\n```java\n    public class ImageLoader implements ModelLoader<CachedImage, InputStream> {\n\n        /**\n        * 在这个方法中，我们使用 ObjectKey 来设置图片的缓存的键\n        */\n        @Override\n        public LoadData<InputStream> buildLoadData(CachedImage cachedImage, int width, int height, Options options) {\n            return new LoadData<>(new ObjectKey(cachedImage.getImageId()),\n                    new HttpUrlFetcher(new GlideUrl(cachedImage.getImageUrl()), 15000));\n        }\n\n        @Override\n        public boolean handles(CachedImage cachedImage) {\n            return true;\n        }\n\n        public static class Factory implements ModelLoaderFactory<CachedImage, InputStream> {\n\n            @Override\n            public ModelLoader<CachedImage, InputStream> build(MultiModelLoaderFactory multiFactory) {\n                return new ImageLoader();\n            }\n\n            @Override\n            public void teardown() { /* no op */ }\n        }\n    }\n```\n\n#### 1.1.4 使用\n\n当我们按照上面的方式配置完毕之后就可以在项目中使用 `CachedImage` 来加载图片了：\n\n```java\n    GlideApp.with(getContext())\n        .load(new CachedImage(user.getAvatarUrl()))\n        .into(getBinding().ivAccount);\n```\n\n这里，当有加载图片需求的时候，都会把原始的图片链接使用 `CachedImage` 包装一层之后再进行加载，其他的步骤与 Glide 的基本使用方式一致。\n\n### 1.2 原理分析\n\n当我们启用了 `@GlideModule` 注解之后会在编译期间生成 `GeneratedAppGlideModuleImpl`。从下面的代码中可以看出，它实际上就是对我们自定义的 `MyAppGlideModule` 做了一层包装。这么去做的目的就是它可以通过反射来寻找 `GeneratedAppGlideModuleImpl`，并通过调用 `GeneratedAppGlideModuleImpl` 的方法来间接调用我们的 `MyAppGlideModule`。本质上是一种代理模式的应用：\n\n```java\n    final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule {\n        private final MyAppGlideModule appGlideModule;\n\n        GeneratedAppGlideModuleImpl() {\n            appGlideModule = new MyAppGlideModule();\n        }\n\n        @Override\n        public void applyOptions(Context context, GlideBuilder builder) {\n            appGlideModule.applyOptions(context, builder);\n        }\n\n        @Override\n        public void registerComponents(Context context, Glide glide, Registry registry) {\n            appGlideModule.registerComponents(context, glide, registry);\n        }\n\n        @Override\n        public boolean isManifestParsingEnabled() {\n            return appGlideModule.isManifestParsingEnabled();\n        }\n\n        @Override\n        public Set<Class<?>> getExcludedModuleClasses() {\n            return Collections.emptySet();\n        }\n\n        @Override\n        GeneratedRequestManagerFactory getRequestManagerFactory() {\n            return new GeneratedRequestManagerFactory();\n        }\n    }\n```\n\n下面就是 `GeneratedAppGlideModuleImpl` 被用到的地方：\n\n当我们实例化单例的 Glide 的时候，会调用下面的方法来通过反射获取该实现类（所以对生成类的混淆就是必不可少的）：\n\n```java\n    Class<GeneratedAppGlideModule> clazz = (Class<GeneratedAppGlideModule>)\n            Class.forName(\"com.bumptech.glide.GeneratedAppGlideModuleImpl\");\n```\n\n当得到了之后会调用 `GeneratedAppGlideModule` 的各个方法。这样我们的自定义 `GlideModule` 的方法就被触发了。（下面的方法比较重要，我们自定义 Glide 的时候许多的配置都能够从下面的源码中寻找到答案，后文中我们仍然会提到这个方法）\n\n```java\n  private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) {\n    Context applicationContext = context.getApplicationContext();\n    // 利用反射获取 GeneratedAppGlideModuleImpl\n    GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();\n    // 从 Manifest 中获取 GlideModule\n    List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();\n    if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {\n      manifestModules = new ManifestParser(applicationContext).parse();\n    }\n\n    // 获取被排除掉的 GlideModule\n    if (annotationGeneratedModule != null\n        && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {\n      Set<Class<?>> excludedModuleClasses = annotationGeneratedModule.getExcludedModuleClasses();\n      Iterator<com.bumptech.glide.module.GlideModule> iterator = manifestModules.iterator();\n      while (iterator.hasNext()) {\n        com.bumptech.glide.module.GlideModule current = iterator.next();\n        if (!excludedModuleClasses.contains(current.getClass())) {\n          continue;\n        }\n        iterator.remove();\n      }\n    }\n\n    // 应用 GlideModule，我们自定义 GlideModuel 的方法会在这里被调用\n    RequestManagerRetriever.RequestManagerFactory factory = annotationGeneratedModule != null\n        ? annotationGeneratedModule.getRequestManagerFactory() : null;\n    builder.setRequestManagerFactory(factory);\n    for (com.bumptech.glide.module.GlideModule module : manifestModules) {\n      module.applyOptions(applicationContext, builder);\n    }\n    if (annotationGeneratedModule != null) {\n      annotationGeneratedModule.applyOptions(applicationContext, builder);\n    }\n    // 构建 Glide 对象\n    Glide glide = builder.build(applicationContext);\n    for (com.bumptech.glide.module.GlideModule module : manifestModules) {\n      module.registerComponents(applicationContext, glide, glide.registry);\n    }\n    if (annotationGeneratedModule != null) {\n      annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);\n    }\n    applicationContext.registerComponentCallbacks(glide);\n    Glide.glide = glide;\n  }\n```\n\n再回到之前的自定义 GlideModule 部分代码中：\n\n```java\npublic void applyOptions(Context context, GlideBuilder builder) {\n    builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20));\n}\n```\n\n这里的 `applyOptions()` 方法允许我们对 Glide 进行自定义。从 `initializeGlide()` 方法中，我们也看出，这里的 `GlideBuilder` 也就是 `initializeGlide()` 方法中传入的 `GlideBuilder`。这里使用了构建者模式，`GlideBuilder` 是构建者的实例。所以，我们可以通过调用 `GlideBuilder` 的方法来对 Glide 进行自定义。\n\n在上面的自定义 GlideModule 中，我们通过构建者来指定了 Glide 的缓存大小和缓存路径。 `GlideBuilder` 还提供了一些其他的方法，我们可以通过查看源码了解，并调用这些方法来自定义 Glide.\n\n## 2、在 Glide 中使用 OkHttp\n\nGlide 默认使用 `HttpURLConnection` 实现网络当中的图片的加载。我们可以通过对 Glide 进行配置来使用 OkHttp 进行网络图片加载。\n\n首先，我们需要引用如下依赖：\n\n```groovy\n    api ('com.github.bumptech.glide:okhttp3-integration:4.8.0') {\n        transitive = false\n    }\n```\n\n该类库中提供了基于 OkHttp 的 `ModelLoader` 和 `DataFetcher` 实现。它们是 Glide 图片加载环节中的重要组成部分，我们会在后面介绍源码和 Glide 的架构的时候介绍它们被设计的意图及其作用。\n\n然后，我们需要在自定义的 `GlideModule` 中注册网络图片加载需要的组件，即在 `registerComponents()` 方法中替换 `GlideUrl` 的加载的默认实现：\n\n```java\n    @GlideModule\n    @Excludes(value = {com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule.class})\n    public class MyAppGlideModule extends AppGlideModule {\n\n        private static final String DISK_CACHE_DIR = \"Glide_cache\";\n\n        private static final long DISK_CACHE_SIZE = 100 << 20; // 100M\n\n        @Override\n        public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {\n            builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE));\n        }\n\n        @Override\n        public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {\n            OkHttpClient okHttpClient = new OkHttpClient.Builder()\n                    .connectTimeout(10, TimeUnit.SECONDS)\n                    .writeTimeout(10, TimeUnit.SECONDS)\n                    .readTimeout(15, TimeUnit.SECONDS)\n                    .eventListener(new EventListener() {\n                        @Override\n                        public void callStart(Call call) {\n                            // 输出日志，用于确认使用了我们配置的 OkHttp 进行网络请求\n                            LogUtils.d(call.request().url().toString());\n                        }\n                    })\n                    .build();\n            registry.replace(GlideUrl.class, InputStream.class, new Factory(okHttpClient));\n        }\n\n        @Override\n        public boolean isManifestParsingEnabled() {\n            // 不使用 Manifest 中的 GlideModule\n            return false;\n        }\n    }\n```\n\n这样我们通过自己的配置指定网络中图片加载需要使用 OkHttp. 并且自定义了 OkHttp 的超时时间等参数。按照上面的方式我们可以在 Glide 中使用 OkHttp 来加载网络中的图片了。\n\n不过，当我们在项目中引用了 `okhttp3-integration` 的依赖之后，不进行上述配置一样可以使用 OkHttp 来进行网络图片加载的。这是因为上述依赖的包中已经提供了一个自定义的 GlideModule，即 `OkHttpLibraryGlideModule`。该类使用了 `@GlideModule` 注解，并且已经指定了网络图片加载使用 OkHttp。所以，当我们不自定义 GlideModule 的时候，只使用它一样可以在 Glide 中使用 OkHttp. \n\n如果我们使用了自定义的 GlideModule，当我们编译的时候会看到 `GeneratedAppGlideModuleImpl` 中的 `registerComponents()` 方法定义如下：\n\n```java\n  @Override\n  public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {\n    new OkHttpLibraryGlideModule().registerComponents(context, glide, registry);\n    appGlideModule.registerComponents(context, glide, registry);\n  }\n```\n\n这里先调用了 `OkHttpLibraryGlideModule` 的 `registerComponents()` 方法，然后调用了我们自定义的 GlideModule 的 `registerComponents()` 方法，只是，我们的 GlideModule 的 `registerComponents()` 方法会覆盖掉 `OkHttpLibraryGlideModule` 中的实现。（因为我们的 GlideModule 的 `registerComponents()` 方法中调用的是 `Registry` 的 `replace()` 方法，会替换之前的效果。） \n\n如果不希望多此一举，我们可以直接在自定义的 GlideModule 中使用 `@Excludes` 注解，并指定 `OkHttpLibraryGlideModule` 来直接排除该类。这样 `GeneratedAppGlideModuleImpl` 中的 `registerComponents()` 方法将只使用我们自定义的 GlideModule. 以下是排除之后生成的类中 `registerComponents()` 方法的实现：\n\n```java\n  @Override\n  public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {\n    appGlideModule.registerComponents(context, glide, registry);\n  }\n```\n\n## 3、总结\n\n在本文中，我们通过介绍 Glide 的两种常见的配置方式来分析了 Glide 的部分源码实现。在这部分中，我们重点介绍了初始化 Glide 的并获取 `GlideModule` 的过程，以及与图片资源的时候相关的 `ModelLoader` 等的源码。了解这部分内容是比较重要的，因为它们是暴露给用户的 API 接口，比较常用；并且对这些类简单了解之后能够不至于在随后分析 Glide 整个加载流程的时候迷路。\n\n这里我们对上面两种配置方式中涉及到的类进行一个分析。如下图所示\n\n![Glide源码](res/glide_configuration.png)\n\n当我们初始化 Glide 的时候会使用 `Registry` 的 `append()` 等一系列的方法构建`资源类型-加载方式-输出类型` 的一个映射，然后当我们使用 Glide 进行记载的时候，会先根据资源类型找到对应的加载方式，然后使用该加载方式从指定的数据源中加载数据，并将其转换成指定的输出类型。\n\n以上面我们自定义图片加载方式的过程为例，这里我们自定义了一个资源类型 `CacheImage`，并通过自定义 GlideModule 指定了它的加载实现是我们自定义的 `ImageLoader` 类。然后，在我们自定义的 ImageLoader 中，我们指定了获取该资源的缓存的键的方式和从数据源中记载数据的具体实现 `HttpUrlFetcher`。这样，当 Glide 要加载某个 CacheImage 的时候，会先使用该缓存的键尝试从缓存中获取，拿不到结果之后使用 `HttpUrlFetcher` 从网络当中获取数据。从网络中获取数据的时候会得到 InputStream，最后，再调用一个回调类，使用 BitmapFactory 从 InputStream 中获取 Bitmap 并将其显示到 ImageView 上面，这样就完成了整个图片加载的流程。\n\n从上文的分析中，我们可以总结出 Glide 的几个设计人性的地方：\n\n1. 使用代理类包装自定义 GlideModule，然后可以使用发射获取该代理类，并通过调用代理类的方法来间接调用我们的 GlideModuel；\n2. 构建`资源类型-加载方式-输出类型`映射的时候使用工厂方法而不是通过某个类建立一对一映射。\n\n上面我们通过 Glide 的几种配置方式简单介绍了 Glide 的图片加载流程。其实际的执行过程远比我们上述过程更加复杂。在下文中我们会对 Glide 的图片加载的主流程进行分析。欢迎继续关注和阅读！\n"
  },
  {
    "path": "图片加载/图片压缩框架封装.md",
    "content": "# 开源一个 Android 图片压缩框架\n\n在我们的业务场景中，需要使用客户端采集图片，上传服务器，然后对图片信息进行识别。为了提升程序的性能，我们需要保证图片上传服务器的速度的同时，保证用于识别图片的质量。整个优化包括两个方面的内容：\n\n1. 相机拍照的优化：包括相机参数的选择、预览、启动速度和照片质量等；\n2. 图片压缩的优化：基于拍摄的图片和从相册中选择的图片进行压缩，控制图片大小和尺寸。\n\n在本文中，我们主要介绍图片压缩优化，后续我们会介绍如何对 Android 的相机进行封装和优化。本项目主要基于 Android 自带的图片压缩 API 进行封装，结合了 Luban 和 Compressor 的优点，同时提供了用户自定义压缩策略的接口。该项目的主要目的在于，统一图片压缩框库的实现，集成常用的两种图片压缩算法，让你以更低的成本集成图片压缩功能到自己的项目中。\n\n## 1、图片压缩的基础知识\n\n对于一般业务场景，当我们展示图片的时候，Glide 会帮我们处理加载的图片的尺寸问题。但在把采集来的图片上传到服务器之前，为了节省流量，我们需要对图片进行压缩。\n\n在 Android 平台上，默认提供的压缩有三种方式：质量压缩和两种尺寸压缩，邻近采样以及双线性采样。下面我们简单介绍下者三种压缩方式都是如何使用的：\n\n### 1.1 质量压缩\n\n所谓的质量压缩就是下面的这行代码，它是 Bitmap 的方法。当我们得到了 Bitmap 的时候，即可使用这个方法来实现质量压缩。它一般位于我们所有压缩方法的最后一步。\n\n```java\n// android.graphics。Bitmap\ncompress(CompressFormat format, int quality, OutputStream stream)\n```\n\n该方法接受三个参数，其含义分别如下：\n\n1. format：枚举，有三个选项 `JPEG`, `PNG` 和 `WEBP`，表示图片的格式；\n2. quality：图片的质量，取值在 `[0,100]` 之间，表示图片质量，越大，图片的质量越高；\n3. stream：一个输出流，通常是我们压缩结果输出的文件的流\n\n### 1.2 邻近采样\n\n邻近采样基于临近点插值算法，用像素代替周围的像素。邻近采样的核心代码只有下面三行，\n\n```java\nBitmapFactory.Options options = new BitmapFactory.Options();\noptions.inSampleSize = 1;\nBitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);\n```\n\n邻近采样核心的地方在于 `inSampleSize` 的计算。它通常是我们使用的压缩算法的第一步。我们可以通过设置 inSampleSize 来得到原始图片采样之后的结果，而不是将原始的图片全部加载到内存中，以防止 OOM。标准使用姿势如下：\n\n```java\n    // 获取原始图片的尺寸\n    BitmapFactory.Options options = new BitmapFactory.Options();\n    options.inJustDecodeBounds = true;\n    options.inSampleSize = 1;\n    BitmapFactory.decodeStream(srcImg.open(), null, options);\n    this.srcWidth = options.outWidth;\n    this.srcHeight = options.outHeight;\n\n    // 进行图片加载，此时会将图片加载到内存中\n    options.inJustDecodeBounds = false;\n    options.inSampleSize = calInSampleSize();\n    Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);\n```\n\n这里主要分成两个步骤，它们各自的含义是：\n\n1. 先通过设置 Options 的 `inJustDecodeBounds` 为 true，来加载图片，以得到图片的尺寸信息。此时图片不会被加载到内存中，所以不会造成 OOM，同时我们可以通过 Options 得到原图的尺寸信息。\n2. 根据上一步中得到的图片的尺寸信息，计算一个 inSampleSize，然后将 inJustDecodeBounds 设置为 false，以加载采样之后的图片到内存中。\n\n关于 inSampleSize 需要简单说明一下：inSampleSize 代表压缩后的图像一个像素点代表了原来的几个像素点，例如 inSampleSize 为 4，则压缩后的图像的宽高是原来的 1/4，像素点数是原来的 1/16，inSampleSize 一般会选择 2 的指数，如果不是 2 的指数，内部计算的时候也会向 2 的指数靠近。所以，实际使用过程中，我们会通过明确指定 inSampleSize 为 2 的指数，来避免内部计算导致的不确定性。\n\n### 1.3 双线性采样\n\n邻近采样可以对图片的尺寸进行有效的控制，但是它存在几个问题。比如，当我需要把图片的宽度压缩到 1200 左右的时候，如果原始的图片的宽度压是 3200，那么我只能通过设置 inSampleSize 将采样率设置为 2 来将其压缩到 1600. 此时图片的尺寸比我们的要求要大。就是说，邻近采样无法对图片的尺寸进行更加精准的控制。如果需要对图片尺寸进行更加精准的控制，那么就需要使用双线性压缩了。\n\n双线性采样采用双线性插值算法，相比邻近采样简单粗暴的选择一个像素点代替其他像素点，双线性采样参考源像素相应位置周围 2x2 个点的值，根据相对位置取对应的权重，经过计算得到目标图像。\n\n它在 Android 中的使用也比较简单，\n\n```java\nBitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);\nMatrix matrix = new Matrix();\nmatrix.setScale(0.5f, 0.5f);\nBitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);\n```\n\n也就是对得到的 Bitmap 应用 `createBitmap()` 进行处理，并传入 Matrix 指定图片尺寸放缩的比例。该方法返回的 Bitmap 就是双线性压缩之后的结果。\n\n### 1.4 图片压缩算法总结\n\n在实际使用过程中，我们通常会结合三种压缩方式使用，一般使用的步骤如下，\n\n1. 使用邻近采样对原始的图片进行采样，将图片控制到比目标尺寸稍大的大小，防止 OOM；\n2. 使用双线性采样对图片的尺寸进行压缩，控制图片的尺寸为目标的大小；\n3. 对上述两个步骤之后得到的图片 Bitmap 进行质量压缩，并将其输出到磁盘上。\n\n当然，本质上 Android 图片的编码是由 [Skia](https://skia.org/index_zh) 库来完成的，所以，除了使用 Android 自带的库进行压缩，我们还可以调用外部的库进行压缩。为了追求更高的压缩效率，通常我们会在 Native 层对图片进行处理，这将涉及 JNI 的知识。笔者曾在之前的文章 [《在 Android 中使用 JNI 的总结》](https://juejin.im/post/5c79f5d0518825347a56275f) 中介绍过 Android 平台上 JNI 的调用的常规思路，感兴趣的同学可以参考下。\n\n## 2、Github 上的开源的图片压缩库\n\n现在 Github 上的图片压缩框架主要有 Luban 和 Compressor 两个。Star 的数量也比较高，一个 9K，另一个 4K. 但是，这两个图片压缩的库有各自的优点和缺点。下面我们通过一个表格总结一下：\n\n|框架|优点|缺点|\n|:-:|:-|:-|\n|Luban|据说是根据微信图片压缩逆推的算法|1.只适用于一般的图片展示的场景，无法对图片的尺寸进行精准压缩；2.内部封装 AsyncTaks 来进行异步的图片压缩，对于 RxJava 支持不好。|\n|Compressor|1.可以对图片的尺寸进行压缩；2.支持 RxJava。|1.尺寸压缩的场景有限，如果有特别的需求，则需要手动修改源代码；2.图片压缩采样的时候计算有问题，导致采样后的图片尺寸总是小于我们指定的尺寸|\n\n上面的图表已经总结得很详细了。所以，根据上面的两个库各自的优缺点，我们打算开发一个新的图片压缩框架。它满足下面的功能：\n\n1. 支持 RxJava：我们可以像使用 Compressor 的时候那样，指定图片压缩的线程和结果监听的线程；\n2. 支持 Luban 压缩算法：Luban 压缩算法核心的部分只在于 inSampleSize 的计算，因此，我们可以很容易得将其集成到我们的新的库中。之所以加入 Luban，是为了让我们的库可以适用于一般图片展示的场景。用户无需指定图片的尺寸，用起来省心省力。\n3. 支持 Compressor 压缩算法同时指定更多的参数：Compressor 压缩算法就是我们上述提到的三种压缩算法的总和。不过，当要压缩的宽高比与原始图片的宽高比不一致的时候，它只提供了一种情景。下文中介绍我们框架的时候会说明进行更详细的说明。当然，你可以在调用框架的方法之前主动去计算出一个宽高比，但是你需要把图片压缩的第一个阶段主动走一遍，费心费力。\n4. 提供用户自定义压缩算法的接口：我们希望设计的库可以允许用户自定义压缩策略。在想要替换图片压缩算法的时候，通过链式调用的一个方法直接更换策略即可。即，我们希望能够让用户以最低的成本替换项目中的图片压缩算法。\n\n## 3、项目整体架构\n\n以下是我们的图片压缩框架的整体架构，这里我们只列举除了其中核心的部分代码。这里的 Compress 是我们的链式调用的起点，我们可以用它来指定图片压缩的基本参数。然后，当我们使用它的 `strategy()` 方法之后，方法将进入到图片压缩策略中，此时，我们继续链式调用压缩策略的自定义方法，个性化地设置各压缩策略自己的参数：\n\n![项目整体架构](res/QQ截图20190312223345.png)\n\n这里的所有的压缩策略都继承自抽线的基类 AbstractStrategy，它提供了两个默认的实现 Luban 和 Compressor. 接口  CompressListener 和 CacheNameFactory 分别用来监听图片压缩进度和自定义压缩的图片的名称。下面的三个是图片相关的工具类，用户可以调用它们来实现自己压缩策略。\n\n## 4、使用\n\n首先，在项目的 Gradle 中加入我的 Maven 仓库的地址：\n\n    maven { url \"https://dl.bintray.com/easymark/Android\" }\n\n然后，在你的项目的依赖中，添加该库的依赖：\n\n    implementation 'me.shouheng.compressor:compressor:0.0.1'\n\n然后，就可以在项目中使用了。你可以参考 Sample 项目的使用方式。不过，下面我们还是对它的一些 API 做简单的说明。\n\n### 4.1 Luban 的使用\n\n下面是 Luban 压缩策略的使用示例，它与 Luban 库的使用类似。只是在 Luban 的库的基础上，我们增加了一个 copy 的选项，用来表示当图片因为小于指定的大小而没有被压缩之后，是否将原始的图片拷贝到指定的目录。因为，比如当你使用回调获取图片压缩结果的时候，如果按照 Luban 库的逻辑，你得到的是原始的图片，所以，此时你需要额外进行判断。因此，我们增加了这个布尔类型的参数，你可以通过它指定将原始文件进行拷贝，这样你就不需要在回调中对是否是原始图片进行判断了。\n\n```kotlin\n    // 在 Compress 的 with() 方法中指定 Context 和 要压缩文件 File\n    val luban = Compress.with(this, file)\n        // 这里添加一个回调，如果你不使用 RxJava，那么可以用它来处理压缩的结果\n        .setCompressListener(object : CompressListener{\n            override fun onStart() {\n                LogUtils.d(Thread.currentThread().toString())\n                Toast.makeText(this@MainActivity, \"Compress Start\", Toast.LENGTH_SHORT).show()\n            }\n\n            override fun onSuccess(result: File?) {\n                LogUtils.d(Thread.currentThread().toString())\n                displayResult(result?.absolutePath)\n                Toast.makeText(this@MainActivity, \"Compress Success : $result\", Toast.LENGTH_SHORT).show()\n            }\n\n            override fun onError(throwable: Throwable?) {\n                LogUtils.d(Thread.currentThread().toString())\n                Toast.makeText(this@MainActivity, \"Compress Error ：$throwable\", Toast.LENGTH_SHORT).show()\n            }\n        })\n        // 压缩图片的名称工厂方法，用来指定压缩结果的文件名\n        .setCacheNameFactory { System.currentTimeMillis().toString() }\n        // 图片的质量\n        .setQuality(80)\n        // 上面基本的配置完了，下面指定图片的压缩策略为 Luban\n        .strategy(Strategies.luban())\n        // 指定如果图片小于等于 100K 就不压缩了，这里的参数 copy 表示，如果不压缩的话要不要拷贝文件\n        .setIgnoreSize(100, copy)\n\n        // 按上面那样得到了 Luban 实例之后有下面两种方式启动图片压缩\n        // 启动方式 1：使用 RxJava 进行处理\n        val d = luban.asFlowable()\n            .subscribeOn(Schedulers.io())\n            .observeOn(AndroidSchedulers.mainThread())\n            .subscribe { displayResult(it.absolutePath) }\n    \n        // 启动方式 2：直接启动，此时使用内部封装的 AsyncTask 进行压缩，压缩结果只能在上面的回调中进行处理了\n        luban.launch()\n```\n\n### 4.2 Compressor 的使用\n\n下面是 Compressor 压缩策略的基本的使用，在调用 `strategy()` 方法指定压缩策略之前，你的任务与 Luban 一致。所以，如果你需要更换图片压缩算法的时候，直接使用 `strategy()` 方法更换策略即可，前面部分的逻辑无需改动，因此，可以降低你更换压缩策略的成本。\n\n```kotlin\n    val compressor = Compress.with(this, file)\n        .setQuality(60)\n        .setTargetDir(\"\")\n        .setCompressListener(object : CompressListener {\n            override fun onStart() {\n                LogUtils.d(Thread.currentThread().toString())\n                Toast.makeText(this@MainActivity, \"Compress Start\", Toast.LENGTH_SHORT).show()\n            }\n\n            override fun onSuccess(result: File?) {\n                LogUtils.d(Thread.currentThread().toString())\n                displayResult(result?.absolutePath)\n                Toast.makeText(this@MainActivity, \"Compress Success : $result\", Toast.LENGTH_SHORT).show()\n            }\n\n            override fun onError(throwable: Throwable?) {\n                LogUtils.d(Thread.currentThread().toString())\n                Toast.makeText(this@MainActivity, \"Compress Error ：$throwable\", Toast.LENGTH_SHORT).show()\n            }\n        })\n        .strategy(Strategies.compressor())\n        .setMaxHeight(100f)\n        .setMaxWidth(100f)\n        .setScaleMode(Configuration.SCALE_SMALLER)\n        .launch()\n```\n\n这里的 `setMaxHeight(100f)` 和 `setMaxWidth(100f)` 用来表示图片压缩的目标大小。具体的大小是如何计算的呢？在 Compressor 库中你是无法确定的，但是在我们的库中，你可以通过 `setScaleMode()` 方法来指定。这个方法接收一个整数类型的枚举，它的取值范围有 4 个，即 `SCALE_LARGER`, `SCALE_SMALLER`, `SCALE_WIDTH` 和 `SCALE_HEIGHT`，它们具体的含义我们会进行详细说明。这里我们默认的压缩方式是 SCALE_LARGER，也就是 Compressor 库的压缩方式。那么这四个参数分别是什么含义呢？\n\n这里我们以一个例子来说明，假设有一个图片的宽度是 1000，高度是 500，简写作 (W:1000, H:500)，通过 `setMaxHeight()` 和 `setMaxWidth()` 指定的参数均为 100，那么，就称目标图片的尺寸，宽度是 100，高度是 100，简写作 (W:100, H:100)。那么按照上面的四种压缩方式，最终的结果将是：\n\n- **SCALE_LARGER**：对高度和长度中较大的一个进行压缩，另一个自适应，因此压缩结果是 (W:100, H:50). 也就是说，因为原始图片宽高比 2:1，我们需要保持这个宽高比之后再压缩。而目标宽高比是 1:1. 而原图的宽度比较大，所以，我们选择将宽度作为压缩的基准，宽度缩小 10 倍，高度也缩小 10 倍。这是 Compressor 库的默认压缩策略，显然它只是优先使得到的图片更小。这在一般情景中没有问题，但是当你想把短边控制在 100 就无计可施了（需要计算之后再传参），此时可以使用 SCALE_SMALLER。\n- **SCALE_SMALLER**：对高度和长度中较大的一个进行压缩，另一个自适应，因此压缩结果是 (W:200, H:100). 也就是，高度缩小 5 倍之后，达到目标 100，然后宽度缩小 5 倍，达到 200. \n- **SCALE_WIDTH**：对宽度进行压缩，高度自适应。因此得到的结果与 SCALE_LARGER 一致。\n- **SCALE_HEIGHT**：对高度进行压缩，宽度自适应，因此得到的结果与 SCALE_HEIGHT 一致。\n\n### 4.3 自定义策略\n\n自定义一个图片压缩策略也是很简单的，你可以通过继承 SimpleStrategy 或者直接继承 AbstractStrategy 来实现：\n\n```kotlin\nclass MySimpleStrategy: SimpleStrategy() {\n\n    override fun calInSampleSize(): Int {\n        return 2\n    }\n\n    fun myLogic(): MySimpleStrategy {\n        return this\n    }\n\n}\n```\n\n注意下，如果想要实现链式的调用，自定义压缩策略的方法需要返回自身。\n\n## 5、最后\n\n因为我们的项目中，需要把图片的短边控制到 1200，长变只适应，只通过改变 Luban 来改变采样率只能把边长控制到一个范围中，无法精准压缩。所以，我们想到了 Compressor，并提出了 SCALE_SMALLER 的压缩模式. 但是 Luban 也不是用不到，一般用来展示的图片的压缩，它用起来更加方便。因此，我们在库中综合了两个框架，其实代码量并不大。当然，为了让我们的库功能更加丰富，因此我们提出了自定义压缩策略的接口，也是用来降低压缩策略的更换成本吧。\n\n最后项目开源在 Github，地址是：https://github.com/Shouheng88/Compressor. 欢迎 Star 和 Fork，为该项目贡献代码或者提出 issue :)\n\n后续，笔者会对 Android 端的相机优化和 JNI 操作 OpenCV 进行图片处理进行讲解，感兴趣的关注作者呦 :)\n"
  },
  {
    "path": "工作空间/OOM优化.md",
    "content": "\n### Oom\n\nOOM就是所谓的内存溢出（Out Of Memory），也就是说内存占有量超过了VM所分配的最大\n\n### 出现OOM的原因\n\n1. 加载对象过大\n2. 相应资源过多，来不及释放\n\n### 如何解决\n\n1. 在内存引用上做些处理，常用的有软引用、强化引用、弱引用\n2. 在内存中加载图片时直接在内存中作处理，如边界压缩\n3. 动态回收内存\n4. 优化Dalvik虚拟机的堆内存分配\t\n5. 自定义堆内存大小"
  },
  {
    "path": "工作空间/Tinker.md",
    "content": "# Tinker 热补丁的源码分析\n\nTinker是微信官方的Android热补丁解决方案，它支持动态下发代码、So库以及资源，让应用能够在不需要重新安装的情况下实现更新。当然，你也可以使用Tinker来更新你的插件。\n\nGithub https://github.com/Tencent/tinker\n\n它主要包括以下几个部分：\n\n1. gradle编译插件: `tinker-patch-gradle-plugin`\n2. 核心sdk库: `tinker-android-lib`\n3. 非gradle编译用户的命令行版本: `tinker-patch-cli.jar`\n\n\n\n"
  },
  {
    "path": "工作空间/URL编码问题.md",
    "content": "今天在使用URL访问别人的接口的时候，因为要查询的数据中包含中文就出现了问题。\n\n    http://api.map.baidu.com/telematics/v3/weather?location=北京&output=json&ak=XXXX\n\n使用\n\n    URL url = new URL(address);\n    connection = (HttpURLConnection) url.openConnection();\n\n来打开数据连接，但是始终获取不到数据。这是因为要访问的URL中包含中文“北京”，因而无法查询。\n如何修改这个问题呢？\n\n实际上，将上面的链接复制到浏览器进行访问是没有问题的。这时，如果细心的话就会发现，粘贴\n之后被访问的连接并非粘贴过去的那串文字，其中的“北京”两个字变成了\n\n    http://api.map.baidu.com/telematics/v3/weather?location=%E5%8C%97%E4%BA%AC&output=json&ak=XXXX\n\n就是将“北京”连个字做了变化。所以，要想使用中文的URL的话，也应该将其中的中文进行转码之后再访问。\n\n那么如何转码呢？\n\n    String city = mEditText.getText().toString();\n    try {\n        city = URLEncoder.encode(city, \"utf-8\");\n    } catch (UnsupportedEncodingException e) {\n        e.printStackTrace();\n    }\n\n使用ERLEncoder.encode即可。这样中文就变成上面那串带有%的字符串了。\n"
  },
  {
    "path": "工作空间/文章暂时存放.md",
    "content": "\n[ Android ̱Ҫ֪һ](https://mp.weixin.qq.com/s/FudvsrZEEVnAbtMokpjP0Q)\n\n[Androidڴй©ĵ](https://mp.weixin.qq.com/s/Ag1QtUgIS5WSemyY2pJ8-A)\n\n[ŵAndroidͻںϼܹݽ֮·](https://mp.weixin.qq.com/s/4mI5p6oVmEO3Q1v0SaH0TQ)\n\n\n"
  },
  {
    "path": "工作空间/百度定位API.md",
    "content": "\n### 百度语音识别API异常：\n\n异常1：\n\n     java.lang.UnsatisfiedLinkError: No implementation found for void com.baidu.speech.core.BDSSDKLoader.SetLogLevel(int) (tried Java_com_baidu_speech_core_BDSSDKLoader_SetLogLevel and Java_com_baidu_speech_core_BDSSDKLoader_SetLogLevel__I)\n\nso文件存在冲突，只要保证libs文件夹下面只有armeabi一个文件夹即可。\n可能之前因为想要兼容各不同的系统所以在使用百度定位的时候创建了多个文件夹，这里需要只留下armeabi一个文件夹。\n\n## 使用百度API进行定位（Android Studio）\n\n### 1.步骤1：获取密钥\n\n### 2.步骤2：下载API开发包\n\n### 3.步骤3：配置环境\n\n#### 3.1 导入jar文件\n\n切换Android Studio的工作目录为Project，在lib文件夹下面导入解压后的API开发包。注意要将.so文件和jar文件同时导入，对jar文件还要在其上面右键单击，选择Add as Library，将其作为库导入进来。\n\n#### 3.2 配置gradle \n\n对使用Android Studio开发的同学，仅仅做到这些还是不够的，还要在build.gradle(Modile:app)中添加\n    \n    sourceSets {\n        main {\n            jniLibs.srcDirs = ['libs']\n        }\n    }\n\n这里的作用是添加.so文件，没有添加.so文件或者添加了.so文件而没有在gradle中进行配置的同学，都无法正常使用百度进行定位。\n\n#### 3.3 在AndroidManifest.xml中进行配置\n\n1.声明要使用的权限\n\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n    <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />\n    <uses-permission android:name=\"android.permission.READ_LOGS\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n    <uses-permission android:name=\"android.permission.WRITE_SETTINGS\" />\n\n2.在application中声明service组件\n\n每个app拥有自己单独的定位\n\n    <service android:name=\"com.baidu.location.f\"\n         android:enabled=\"true\"\n         android:process=\":remote\">\n    </service>\n\n3.在application中声明APP KEY：\n\n    <meta-data android:name=\"com.baidu.lbsapi.API_KEY\"\n         android:value=\"你的APP KEY\" />\n\n### 步骤4：添加代码\n\n我们将与定位相关的代码全部放在一个工具类中\n\n\tpublic class LocationUtils {\n\t\n\t    private static LocationUtils sInstance;\n\t\n\t    private LocationClient mLocationClient;\n\t\n\t    private BDLocationListener bdLocationListener;\n\t\n\t    public static LocationUtils getInstance(Context mContext){\n\t        if (sInstance == null){\n\t            sychronized(LocationUtils.class) {\n                    if (sInstance == null) {\n                         sInstance = new LocationUtils(mContext.getApplicationContext());\n                    }\n                }\n\t        }\n\t        return sInstance;\n\t    }\n\t\n\t    private LocationUtils(Context mContext){\n\t        mLocationClient = new LocationClient(mContext);\n\t    }\n\t\n\t    public void locate(BDLocationListener mListener){\n\t        bdLocationListener = mListener;\n\t        mLocationClient.registerLocationListener(bdLocationListener);\n\t        LocationClientOption option = new LocationClientOption();\n\t        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);\n\t        option.setIsNeedAddress(true);\n\t        mLocationClient.setLocOption(option);\n\t        mLocationClient.requestLocation();\n\t        mLocationClient.start();\n\t    }\n\t\n\t    public void stop() {\n\t        if (mLocationClient.isStarted()) {\n\t            mLocationClient.stop();\n\t            if (bdLocationListener != null) {\n\t                mLocationClient.unRegisterLocationListener(bdLocationListener);\n\t                bdLocationListener = null;\n\t            }\n\t        }\n\t    }\n\t}\n\n这样每当我们需要定位的时候就可以这样调用：\n\n    ToastUtils.showShortToast(getContext(), R.string.trying_to_get_location);\n    LocationUtils.getInstance(getContext()).locate(new BDLocationListener() {\n        @Override\n        public void onReceiveLocation(BDLocation bdLocation) {\n            // ... 然后当bdLocation不为空的时候，直接从bdLocation上面获取位置信息就可以了\n        }\n    });\n\n### 常见错误：\n\n#### 1.无法获取位置信息：在监听器当中没有获取到位置的详细信息\n\n原因可能是：\n\n1. 没有添加\n\n        option.setIsNeedAddress(true);\n这行代码是用来设置用户需不需要获取返回信息的，老版本的方法是使用\n\n        option.setAddrType(“all”)\n\n实际上，根据源代码这两个代码的功能是相同的，只是后者现在被抛弃了，使用前者即可。\n\n2. 没有在AndroidManifest.xml文件中添加\n\n        <service  .........>\n\n出现上面的这种情况也是无法获取的。\n\n3. 没有添加.so文件或者添加了.so文件但是没有在gralde文件中进行声明。前面提到过关于.so文件在gralde中声明的方法。\n\n\n\n"
  },
  {
    "path": "工作空间/第三方库整理.md",
    "content": "# \n\n## 1\n\n### 1.1 ܣܺʾ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ٿ|[FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)|MVP,EventBus,ʽ,ORM,|\n|2||[ArmsComponent](https://github.com/JessYanCoding/ArmsComponent)|Arms,MVP,|\n|3|MVPٻ|[MVPArms](https://github.com/JessYanCoding/MVPArms)|Arms,MVP,ÿģ|\n|4|MVVM|[Android-mvvm-architecture](https://github.com/MindorksOpenSource/android-mvvm-architecture)|MVVMʾ|\n|5|MVVM|[AndroidArchitecture](https://github.com/iammert/AndroidArchitecture)|LiveData, Room Persistence, Dagger 2, Retrofit, MVVM and DataBinding|\n\n### 1.2 \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|HTTPͻ|[Retrofit](https://github.com/square/retrofit)|Type-safe HTTP client for Android and Java by Square, Inc. |\n|2|첽Http|[Android-async-http](https://github.com/loopj/android-async-http)||\n|3|װOKHttp|[OKHttpUtils](https://github.com/duzechao/OKHttpUtils)||\n|4|װOKHttp|[OkGo](https://github.com/jeasonlzy/okhttp-OkGo)|| \n|5|OKHttp|[OKHttp](https://github.com/square/okhttp)||\n|6|װOKHttp|[Okhttputils](https://github.com/hongyangAndroid/okhttputils)||\n\n### 1.3 ־û\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|̻|[DiskLruCache](https://github.com/JakeWharton/DiskLruCache)||\n|2|ݿ|[WCDB](https://github.com/Tencent/wcdb)|ƽ̨ݿ|\n|3|ݿ|[GreenDAO](https://github.com/greenrobot/greenDAO)||\n\n### 1.4 &&\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[Android-Debug-Database](https://github.com/amitshekhariitbhu/Android-Debug-Database)||\n|2||[GT](https://github.com/Tencent/GT)|ѶAPPƽ̨|\n|3||[Stetho](https://github.com/facebook/stetho)||\n|4||[ACRA](https://github.com/ACRA/acra)||\n\n### 1.5 ޸\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|޸|[AndFix](https://github.com/alibaba/AndFix)||\n|2|޸|[tinker](https://github.com/Tencent/tinker)||\n\n### \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|UI|[RapidView](https://github.com/Tencent/RapidView)|UI|\n|2|Ϸ|[Libgdx](https://github.com/libgdx/libgdx)|ƽ̨JavaϷ|\n|3|ȸʾ|[Android-architecture-components](https://github.com/googlesamples/android-architecture-components)||\n|4|ע|[Dagger](https://github.com/square/dagger)||\n|5|¼ͨ|[EventBus](https://github.com/greenrobot/EventBus)||\n\n## 2ý\n\n### 2.1 RecyelerView\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|ͨAdapterװȽʹ|\n|2||[FastAdapter](https://github.com/mikepenz/FastAdapter)||\n|3|RecyclerView|[XRecyclerView](https://github.com/XRecyclerView/XRecyclerView)|ˢºͼظ|\n|4|RecyclerView|[SwipeRecyclerView](https://github.com/yanzhenjie/SwipeRecyclerView)|໬˵|\n|5|RecyclerView|[EasyRecyclerView](https://github.com/Jude95/EasyRecyclerView)|More,Header/Footer,EmptyView,ProgressView,ErrorView|\n|6|RecyclerView|[MaterialScrollBar](https://github.com/turing-tech/MaterialScrollBar)|ٻ|\n|7|RecyclerView|[sticky-headers-recyclerview](https://github.com/timehop/sticky-headers-recyclerview)|̶|\n|8|RecyclerView|[RecyclerViewSnap](https://github.com/rubensousa/RecyclerViewSnap)||\n|9|RecyclerView|[RecyclerViewFastScroller](https://github.com/danoz73/RecyclerViewFastScroller)|ٻ|\n|10|RecyclerView|[RecyclerView-FastScroll](https://github.com/timusus/RecyclerView-FastScroll)||\n|11|RecyclerView|[android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)|ں˶RecyclerView|\n|12|LayoutManager|[vlayout](https://github.com/alibaba/vlayout)|,LayoutManager|\n\n### 2.2 ͼƬ\n\n#### 2.2.1 ͼƬģ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ͼƬģ|[StackBlur](https://github.com/kikoso/android-stackblur)||\n|2|ͼƬģ|[GaussianBlur](https://github.com/jrvansuita/GaussianBlur)||\n\n#### 2.2.2 ͼƬ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ͼƬ༭|[PhotoEditDemo](https://github.com/jarlen/PhotoEditDemo)||\n|2|ͼƬ༭|[PhotoEdit](https://github.com/jarlen/PhotoEdit)||\n|3|άɨ|[Barcodescanner](https://github.com/dm77/barcodescanner)||\n|4|ͼƬѹ|[Luban](https://github.com/Curzibn/Luban)||\n\n#### 2.2.3 ͼƬ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ͼƬ|[Universal-Image-Loader](https://github.com/nostra13/Android-Universal-Image-Loader)||\n|2|ͼƬ|[Picasso](https://github.com/square/picasso)||\n|3|ͼƬ|[Fresco](https://github.com/facebook/fresco)||\n|4|ͼƬ|[Glide](https://github.com/bumptech/glide)||\n\n#### 2.2.4 ͼƬչʾ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ԲͼƬ|[CircleImageView](https://github.com/hdodenhof/CircleImageView)||\n|2|ͼƬ|[Subsampling-scale-image-view)](https://github.com/davemorrissey/subsampling-scale-image-view)||\n|3|ͼƬ|[PhotoView](https://github.com/bm-x/PhotoView)||\n|4|ԲͼƬ|[RoundedImageView](https://github.com/vinc3m1/RoundedImageView)||\n\n#### 2.2.4 ͼƬѡ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ͼƬѡ|[ImagePicker](https://github.com/martin90s/ImagePicker)||\n|2|ͼƬѡ|[ImagePicker](https://github.com/jeasonlzy/ImagePicker)||\n|3|ͼƬѡ|[ImagePicker](https://github.com/martin90s/ImagePicker)||\n\n### 2.3 Ƶ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n\n### 2.4 Ƶ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|Ƶ|[NiceVieoPlayer](https://github.com/xiaoyanger0825/NiceVieoPlayer)||\n|2|Ƶ|[VideoPlayerManager](https://github.com/danylovolokh/VideoPlayerManager)||\n|3|Ƶ¼|[RecordVideoDemo](https://github.com/szitguy/RecordVideoDemo)||\n\n### 2.5 \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ؼ|[Cameraview](https://github.com/google/cameraview)||\n|2||[CameraFragment](https://github.com/florent37/CameraFragment)||\n|3|&ͼƬ|[MagicCamera](https://github.com/wuhaoyu1990/MagicCamera)|յ40ʵʱ˾ա¼ͼƬ޸|\n\n### 2.6 ͼ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ͼ|[Android-maps-utils](https://github.com/googlemaps/android-maps-utils)||\n\n## 3\n\n### 3.1 Android߿\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[RxTool](https://github.com/vondear/RxTool)|AndroidԱòռĹ༯|\n|2||[AndroidUtils](https://github.com/Blizzard-liu/AndroidUtils)|׿ʨر|\n|3||[XUtils3](https://github.com/wyouflf/xUtils3)||\n|4||[AndroidDeviceNames](https://github.com/jaredrummler/AndroidDeviceNames)|ȡAndroid豸|\n|5||[Android-proguards](https://github.com/yongjhih/android-proguards)||\n|6|Gitļ|[Gitignore](https://github.com/github/gitignore)||\n|7|Ȩ|[AndPermission](https://github.com/yanzhenjie/AndPermission)||\n|8||[Dex2jar](https://github.com/pxb1988/dex2jar)||\n|9|Lambda|[Retrolambda](https://github.com/orfjackal/retrolambda)|Backport of Java 8's lambda expressions to Java 7, 6 and 5|\n|10|Lambda|[Gradle-retrolambda](https://github.com/evant/gradle-retrolambda)|A gradle plugin for getting java lambda support in java 6, 7 and android|\n\n### 3.2 Java\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|Java|[Guava](https://github.com/google/guava)|Google Java Ŀ|\n|2|Java㷨|[TheAlgorithms-Java](https://github.com/TheAlgorithms/Java)||\n|3|ģʽ|[Java-design-patterns](https://github.com/iluwatar/java-design-patterns)||\n|4|ʱAPI|[Joda-time](https://github.com/JodaOrg/joda-time)||\n\n### 3.3 Java\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[Encrypt](https://github.com/GcsSloop/encrypt)||\n|2|Markdown|[Flexmark-java](https://github.com/vsch/flexmark-java)||\n|3|ѧ|[Calci-kernel](https://github.com/Iraka-C/Calci-kernel)|A complex calculation kernel in Java (for Calci calculator)|\n|4|תƴ|[TinyPinyin](https://github.com/promeG/TinyPinyin)||\n\n## 4Ԫ\n\n### 4.1 л\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[BGASwipeBackLayout-Android](https://github.com/bingoogolapple/BGASwipeBackLayout-Android)||\n|2|л|[Transitions](https://github.com/rubensousa/Transitions)|ʾ|\n|3||[Pull-back-layout](https://github.com/oxoooo/pull-back-layout)||\n\n### 4.2 沼\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[MaterialDrawer](https://github.com/mikepenz/MaterialDrawer)||\n|2|·ҳ|[android-vertical-slide-view](https://github.com/xmuSistone/VerticalSlideFragment)|Ʒҳ϶ʱԼһҳ|\n|3|ҷҳ|[MaterialViewPager](https://github.com/florent37/MaterialViewPager)||\n|4|ײ|[AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel)||\n|5|ViewPagerָʾ|[ViewPagerIndicator](https://github.com/JakeWharton/ViewPagerIndicator)||\n|6|ViewPagerָʾ|[MagicIndicator](https://github.com/hackware1993/MagicIndicator)||\n|7|бؼViewPager|[DiscreteScrollView](https://github.com/yarolegovich/DiscreteScrollView)||\n|8|ԽЧ|[OverScroll-Everywhere](https://github.com/Mixiaoxiao/OverScroll-Everywhere)|ߵײʱЧ|\n|9|ٻ|[FastScroll-Everywhere](https://github.com/Mixiaoxiao/FastScroll-Everywhere)|ΪɻViewӿٻ|\n\n### 4.3 Ч\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ˮɢЧ|[WaveView](https://github.com/hackware1993/WaveView)||\n|2|Ч|[LikeAnimation](https://github.com/frogermcs/LikeAnimation)||\n|3||[Backboard](https://github.com/tumblr/Backboard)||\n|4|ؼ|[AndroidViewAnimations](https://github.com/daimajia/AndroidViewAnimations)||\n|5|ûʾ|[MaterialTapTargetPrompt](https://github.com/sjwall/MaterialTapTargetPrompt)||\n\n### 4.4 ҳ棺ټ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ӭҳ|[AppIntro](https://github.com/apl-devs/AppIntro)||\n|2|ҳ|[MaterialAbout](https://github.com/jrvansuita/MaterialAbout)||\n\n### 4.5 Ի\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ײ|[BottomDialog](https://github.com/shaohui10086/BottomDialog)||\n|2|ײ|[bottomsheet](https://github.com/Flipboard/bottomsheet)||\n|3|ײ|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)||\n|4|ײ|[BottomSheetBuilder](https://github.com/rubensousa/BottomSheetBuilder)||\n|5|ѡ|[AndroidPicker](https://github.com/gzu-liyujiang/AndroidPicker)||\n|6|ֶԻ|[material-dialogs](https://github.com/afollestad/material-dialogs)||\n|7|ɫѡ|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)||\n|8|ںʱѡ|[datetimepicker](https://github.com/flavienlaurent/datetimepicker)||\n\n### 4.6 ʱ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[CalendarView](https://github.com/huanghaibin-dev/CalendarView)||\n|2||[Android-Week-View](https://github.com/alamkanak/Android-Week-View)|1죬3죬7|\n|3||[Calendar](https://github.com/xiaojianglaile/Calendar)||\n|4|С|[Android-MonthCalendarWidget](https://github.com/romannurik/Android-MonthCalendarWidget)||\n|5||[AgendaCalendarView](https://github.com/Tibolte/AgendaCalendarView)||\n|6|ʱ|[GAHonorClock](https://github.com/Ajian-studio/GAHonorClock)||\n\n### 4.7 ؼϼ\n\n#### 4.7.1 ͼ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)||\n|2||[hellocharts](https://github.com/lecho/hellocharts-android)||\n\n#### 4.7.2 ť\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|FAB|[FloatingToolbar](https://github.com/rubensousa/FloatingToolbar)|FABתToolbar|\n|2|FAB|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)||\n\n#### 4.7.3 \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[ActionBarSherlock](https://github.com/JakeWharton/ActionBarSherlock)||\n\n#### 4.7.4 \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[MaterialProgressBar](https://github.com/DreaminginCodeZH/MaterialProgressBar)||\n|2||[verticalseekbar](https://github.com/h6ah4i/android-verticalseekbar)||\n|3||[BubbleSeekBar](https://github.com/woxingxiao/BubbleSeekBar)||\n|4||[ProgressWheel](https://github.com/Todd-Davies/ProgressWheel)||\n|5||[PreviewSeekBar](https://github.com/rubensousa/PreviewSeekBar)|ƵԤ|\n|6||[discreteSeekBar](https://github.com/AnderWeb/discreteSeekBar)||\n|7|״̬|[AVLoadingIndicatorView](https://github.com/81813780/AVLoadingIndicatorView)||\n|8|ʶ|[SpeechRecognitionView](https://github.com/zagum/SpeechRecognitionView)||\n\n#### 4.7.5 ؼԴ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ʸпؼϼ|[material-components-android](https://github.com/material-components/material-components-android)||\n|2|Material|[MaterialDesignLibrary](https://github.com/navasmdc/MaterialDesignLibrary)||\n\n#### 4.7.6 ༭\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|MarkdownԤ|[MarkdownView](https://github.com/tiagohm/MarkdownView)||\n|2|MarkdownԤ|[RxMarkdown](https://github.com/yydcdut/RxMarkdown)||\n|3|ͼƬͿѻ|[Graffiti](https://github.com/1993hzw/Graffiti)||\n\n#### 4.7.7 ť\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ť|[RaiflatButton](https://github.com/rubensousa/RaiflatButton)||\n\n#### 4.7.8 \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[PinLockView](https://github.com/aritraroy/PinLockView)||\n\n#### \n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView)||\n|2|α|[AnchorImageView](https://github.com/jcodeing/AnchorImageView)||\n\n## 5Դ\n\n### 5.1 ѧϰ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|Android ѧϰ|[AndroidNote](https://github.com/GcsSloop/AndroidNote)||\n|2|Android ѧϰ|[CoreLink](https://github.com/lizhangqu/CoreLink)||\n|3|Android ѧϰ|[Android_Data](https://github.com/Freelander/Android_Data)||\n|4|Android ʾ|[android-examples](https://github.com/nisrulz/android-examples)||\n|5|WebView ȫʹ|[WebViewStudy](https://github.com/youlookwhat/WebViewStudy)||\n|6|RxJava ѧϰ|[RxJava-Android-Samples](https://github.com/kaushikgopal/RxJava-Android-Samples)||\n|7|WebView |[WebviewCapture](https://github.com/hsk256/WebviewCapture)|Webviewļֽʽ|\n|8|Android |[Android-interview-questions](https://github.com/MindorksOpenSource/android-interview-questions)||\n\n### 5.2 Դ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|Android Դ|[Android-open-project](https://github.com/Trinea/android-open-project)||\n|2|Android Դ|[Android-open-project-analysis](https://github.com/android-cn/android-open-project-analysis)||\n|3|Android Դ|[Open-source-android-apps](https://github.com/pcqpcq/open-source-android-apps)||\n|4|Android Դ|[Android-Develop-Resources](https://github.com/zmywly8866/Android-Develop-Resources)||\n|5|Android ؼ|[Awesome-android-ui](https://github.com/wasabeef/awesome-android-ui)||\n|6|Android |[Android-discuss](https://github.com/android-cn/android-discuss)||\n|7|Android |[Android-jobs](https://github.com/android-cn/android-jobs)||\n\n### 5.3 UIԴ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1||[awesome-design-cn](https://github.com/jobbole/awesome-design-cn)|ʦԴ|\n|2|ͼ|[Android-Iconics](https://github.com/mikepenz/Android-Iconics)||\n\n### 5.4 ԴĿ\n\n|||ƺ͵ַ|ע|\n|:-:|:-:|:-:|:-:|\n|1|ʼ|[EverMemo](https://github.com/daimajia/EverMemo)||\n|2||[MVVM-JueJin](https://github.com/fashare2015/MVVM-JueJin)||\n|3|ֽɻ|[PaperPlane](https://github.com/TonnyL/PaperPlane)|kotlin|\n|4|ʫ|[Android-poetry](https://github.com/VinsonGuo/android-poetry)|ͻ+|\n|5|΢Yue|[΢Yue](https://github.com/LiangLuDev/WeYueReader)||\n|6||[SimplifyReader](https://github.com/chentao0707/SimplifyReader)||\n|7||[MinimalistWeather](https://github.com/BaronZ88/MinimalistWeather)||\n|8|༭|[Turbo Editor](https://github.com/vmihalachi/turbo-editor)||\n|9|ʼ+TODO|[rgzly-android](https://github.com/orgzly/orgzly-android)||\n|10|Markdown༭|[MarkdownEditors](https://github.com/qinci/MarkdownEditors)||\n|11|߷΢|[wechat](https://github.com/motianhuo/wechat)||\n|12|΢СƵ+|[VCameraDemo](https://github.com/motianhuo/VCameraDemo)|΢СƵ+,FFmpegװ|\n|13||[DarkCalculator](https://github.com/HK-SHAO/DarkCalculator)|һָ֧ͽ̵ⷽȹܵļ|\n|14||[ncalc](https://github.com/tranleduy2000/ncalc)||\n|15||[LeafPic](https://github.com/HoraApps/LeafPic)||\n|16||[Eyepetizer-in-Kotlin](https://github.com/LRH1993/Eyepetizer-in-Kotlin)||\n|17|ʼ|[simplenote](https://github.com/Automattic/simplenote-android)||\n|19|ļ|[aFileChooser](https://github.com/iPaulPro/aFileChooser)||\n|20||[CloudReader](https://github.com/youlookwhat/CloudReader)|UIʹGankIoapi|\n|21|MaterialDesignʾ|[LollipopShowcase](https://github.com/mikepenz/LollipopShowcase)|MaterialDesignʾ|\n|22||[Weather](https://github.com/Mixiaoxiao/Weather)||\n|23|ֲ|[Shuttle](https://github.com/timusus/Shuttle)||\n|24|ļ|[AnExplorer](https://github.com/1hakr/AnExplorer)||\n|25||[Travel-Mate](https://github.com/project-travel-mate/Travel-Mate)||\n|26| |[SuperMvp](https://github.com/liuyanggithub/SuperMvp)||\n|27|MaterialDesignʾ|[MaterialDesignDemo](https://github.com/Eajy/MaterialDesignDemo)||\n|28|ֲ|[MusicX-music-player](https://github.com/RajneeshSingh007/MusicX-music-player)||\n|29|ȫֱ|[KingTV](https://github.com/jenly1314/KingTV)||\n|30||[KotlinMvp](https://github.com/git-xuhao/KotlinMvp)||\n|31||[Eyepetizer](https://github.com/kaikaixue/Eyepetizer)||\n|32|ʼ|[Notes](https://github.com/lguipeng/Notes)||\n|33|СױǩԴ|[Notes](https://github.com/MiCode/Notes)||\n|34|MIUIļԴ|[FileExplorer](https://github.com/MiCode/FileExplorer)||\n|35| \"\" С˵Ķ|[BookReader](https://github.com/JustWayward/BookReader)||\n|36|ֲ|[Phonograph](https://github.com/kabouzeid/Phonograph)||\n|37|ļ|[AmazeFileManager](https://github.com/TeamAmaze/AmazeFileManager)||\n|38|Ķ|[AndroidFire](https://github.com/jaydenxiao2016/AndroidFire)||\n|39|ֲ|[MusicDNA](https://github.com/harjot-oberai/MusicDNA)||\n|40|Ķ|[LookLook](https://github.com/xinghongfei/LookLook)||\n|41|Ķ|[LookLook](https://github.com/xinghongfei/LookLook)||\n|42|BiliBili|[bilibili-android-client](https://github.com/HotBitmapGG/bilibili-android-client)||\n|43||[Douya](https://github.com/DreaminginCodeZH/Douya)||\n|44|+ͼƬ|[Meizhi](https://github.com/drakeet/Meizhi)||\n|45||[rebase](https://github.com/drakeet/rebase-android)||\n|46|ֲ|[Music-Player](https://github.com/andremion/Music-Player)||\n|47|ʼ|[Omni-Notes](https://github.com/federicoiosue/Omni-Notes)||\n|48|͹|[NewPipe](https://github.com/TeamNewPipe/NewPipe)||\n|49|ֲ|[UniversalMusicPlayer](https://github.com/googlesamples/android-UniversalMusicPlayer)||\n|50|LeetCode|[LeeCo](https://github.com/Nightonke/LeeCo)||\n|51||[AnimeTaste](https://github.com/daimajia/AnimeTaste)||\n|52|ֲ|[Timber](https://github.com/naman14/Timber)||\n|53|ý|[kotlin-life](https://github.com/Cuieney/kotlin-life)||\n|54||[Minimal-Todo](https://github.com/avjinder/Minimal-Todo)||\n|55|ļ|[MLManager](https://github.com/javiersantos/MLManager)||\n|56|QQ|[QQ](https://github.com/HuTianQi/QQ)||\n"
  },
  {
    "path": "开发工具/ADB_常见的ADB指令总结.md",
    "content": "\n\n\n\n\n\n\n\n"
  },
  {
    "path": "开发工具/Gradle_常见的指令和配置总结.md",
    "content": "# 常见的 Gralde 配置和指令总结\n\n## 1、依赖相关\n\n### 1.1 transitive = true\n\n```groovy\n    dependencies {\n        implementation (\"com.github.bumptech.glide:glide:4.8.0@aar\") {\n            transitive = true\n        }\n    }\n```\n\n在后面加上 `@aar`，意指你只是下载该 `aar` 包，而并不下载该 `aar` 包所依赖的其他库，那如果想在使用 `@aar` 的前提下还能下载其依赖库，则需要添加 `transitive=true` 的条件。该属性的默认值是 false，表示你所添加的库的所依赖的其他库会被 Gradle 自动下载。\n\n### 1.2 强制设置某个模块的版本\n\n```groovy\n    configurations.all {\n        resolutionStrategy {\n            force'org.hamcrest:hamcrest-core:1.3'\n        }\n    }\n```\n\n### 1.3 强制排除某个依赖\n\n```groovy\n    configurations.all {\n        exclude module: 'okhttp-ws'\n    }\n```\n\n### 1.3 输出模块的依赖树\n\n按照下面的方式，这里的 `commons` 是模块名：\n\n```groovy\n    gradlew :commons:dependencies\n```\n\n或者点击 AS 右侧的 `Gradle` 选择 `:commons` -> `Tasks` -> `android` -> `:commons:androidDependencies`\n\n\n\n"
  },
  {
    "path": "开发工具/Keytool_常用的指令.md",
    "content": "# Keytool 常用的指令合集\n\n### 获取 APK 的签名信息\n\n方式 1：在命令行中输入下面的命令，即可获取 SHA1 签名信息：\n\n    keytool -printcert -file C:\\META-INF\\CERT.RSA\n    \n这里的 `C:\\META-INF\\CERT.RSA` 是从 APK 包中解压出来的 `RSA` 文件的路径。\n\n方式 2：或者使用下面的命令也可以达到相同的目的，并且不用对 APK 进行解压：\n\n    keytool -printcert -jarfile example-release-v2.apk\n\n### 显示签名文件的信息\n\n在命令行输入下面的指令来显示签名文件的信息，这里的 `palm.jks` 是签名文件：\n\n    keytool -list -v -keystore palm.jks\n\n指令执行的结果是：\n\n![显示签名文件信息](res/keytool_list.jpg)\n\n### 修改签名文件的密码\n\n在命令行输入下面的指令来修改签名文件的密码，这里的 `palm.jks` 是签名文件：\n\n    keytool -storepasswd -keystore palm.jks\n\n指令的执行结构如下，我们输入旧密码，然后两次输入并确认新密码之后，密码即修改成功：\n\n![修改签名文件的密码](res/keytool_password.jpg)\n\n### 修改签名文件的别名\n\n在命令行输入下面的指令来修改签名文件的别名，这里的 `palm.jks` 是签名文件，`key0` 是之前的别名，`alias0` 是要修改成的别名：\n\n    keytool -changealias -keystore palm.jks -alias key0 -destalias alias0\n\n指令执行的结果如下，输入签名文件的密码之后，我们就成功地修改了别名：\n\n![修改签名文件的别名](res/keytool_alias_name.jpg)\n\n然后，我们使用下面的指令再来查看一下修改之后的签名文件的信息，以确认别名修改成功。如下图所示，别名已经被我们成功地修改成了 `alias0`：\n\n![确认修改别名成功](res/keytool_list_after.jpg)\n\n### 修改签名文件的别名的密码\n\n在命令行输入下面的指令来修改签名文件的别名的密码，这里的 `palm.jks` 是签名文件，`alias0` 是签名文件的别名：\n\n    keytool -keypasswd -keystore palm.jks -alias alias0\n\n指令执行的结果如下所示，当我们输入完了签名文件的密码和原来的别名密码之后，输入两遍新的别名的密码即可：\n\n![修改签名文件的别名的密码](res/keytool_alias_psd.jpg)\n\n\n"
  },
  {
    "path": "异步编程/Android多线程编程：IntentService和HandlerThread.md",
    "content": "# Android 多线程编程：IntentService & HandlerThread\n\n因为 Android 是使用 Java 开发的，所以当我们谈及 Android 中的多线程，必然绕不过 Java 中的多线程编程。但在这篇文章中，我们不会过多地分析 Java 中的多线程编程的知识。我们会在以后分析 Java 并发编程的时候分析 Java 中的多线程、线程池和并发 API 的用法。\n\n我们先来总结一下 Android 多线程编程的演变过程：首先是 Java 的 Thread。因为本身在创建一个线程和销毁一个线程的时候会有一定的开销，当我们任务的执行时间相比于这个开销很小的时候，单独创建一个线程就显得不划算。所以，当程序中存在大量的、小的任务的时候，建议使用线程池来进行管理。但我们一般也很少主动去创建线程池，这是因为——也许是考虑到开发者自己去维护一个线程池比较复杂—— Android 中已经为我们设计了 AsyncTask。AsyncTask 内部封装了一个线程池，我们可以使用它来执行耗时比较短的任务。但 AsyncTask 也有一些缺点：1).如果你的程序中存在很多的不同的任务的时候，你可能要为每个任务定义一个 AsyncTask 的子类。2).从异步线程切换到主线程的方式不如 RxJava 简洁。所以，在实际开发的过程中，我通常使用 RxJava 来实现异步的编程。尤其是局部的优化、不值得专门定义一个 AsyncTask 类的时候，RxJava 用起来更加舒服。\n\n上面的多线程创建的只是普通的线程，对系统来说，优先级比较低。在 Android 中还提供了 IntentService 来执行优先级相对较高的任务。启动一个 IntentService 任务的时候会将任务添加到其内部的、异步的消息队列中执行。此外，IntentService 又继承自 Service，所以这让它具有异步和较高的优先级两个优势。\n\n在之前的文章中，我们已经分析过 AsyncTask、RxJava 以及用来实现线程切换的 Handler. 这里奉上这些文章的链接：\n\n- [《Android AsyncTask 源码分析》](https://juejin.im/post/5b65c71af265da0f9402ca4a)\n- [《RxJava2 系列 (1)：一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd)\n- [《RxJava2 系列 (2)：背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187)\n- [《RxJava2 系列 (3)：使用 Subject》](https://juejin.im/post/5b801dfa51882542cb409905)\n- [《Android 消息机制：Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)\n\n你可以通过以上的文章来了解这部分的内容。在本篇文章中，我们主要来梳理下另外两个多线程相关的 API，HandlerThread 和 IntentService。\n\n## 1、异步消息队列：HandlerThread\n\n如果你之前还没有了解过 Handler 的实现的话，那么最好通过我们上面的那篇文章 [《Android 消息机制：Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb) 了解一下。因为 HandlerThread 就是通过封装一个 Looper 来实现的。\n\n### 1.1 HandlerThread 的使用\n\nHandlerThread 继承自线程类 Thread，内部又维护了一个 Looper，Looper 内又维护了一个消息队列。所以，我们可以使用 HandlerThread 来创建一个异步的线程，然后不断向该线程发送任务。这些任务会被封装成消息放进 HandlerThread 的消息队列中被执行。所以，我们可以用 HandlerThread 来创建异步的消息队列。\n\n在使用 HandlerThread 的时候有两个需要注意的地方：\n\n1. 因为 HandlerThread 内部的 Looper 的初始化和开启循环的过程都在 `run()` 方法中执行，所以，在使用 HandlerThread 之前，你必须调用它的 start() 方法。\n2. 因为 HandlerThread 的 `run()` 方法使用 Looper 开启一个了无限循环，所以，当不再使用它的时候，应该调用它的 `quitSafely()` 或 `quit()` 方法来结束该循环。\n\n在使用 HandlerThread 的时候只需要创建一个它的实例，然后使用它的 Looper 来创建 Handler 实例，并通过该 Handler 发送消息来将任务添加到队列中。下面是一个使用示例：\n\n    myHandlerThread = new HandlerThread(\"MyHandlerThread\");\n    myHandlerThread.start();\n    handler = new Handler( myHandlerThread.getLooper() ){\n        @Override\n        public void handleMessage(Message msg) {\n            // ... do something\n        }\n    };\n    handler.sendEmptyMessage(1);\n\n这里我们创建了 HandlerThread 实例之后用它来创建 Handler 然后通过 Handler 把任务加入到消息队列中进行执行。\n\n显然，使用 HandlerThread 可以很轻松地实现一个消息队列。你只需要在创建了 Handler 之后向它发送消息，然后所有的任务将被加入到队列中执行。当然，它也有缺点。因为所有的任务将会被按顺序执行，所以一旦队列中有某个任务执行时间过长，那么就会导致后续的任务都会被延迟处理。\n\n### 1.2 HandlerThread 源码解析\n\n下面是该 API 的源码，实现也比较简单，我们直接通过注释来对主要部分进行说明：\n\n    public class HandlerThread extends Thread {\n        int mPriority;\n        int mTid = -1;\n        Looper mLooper;\n        private @Nullable Handler mHandler;\n\n        public HandlerThread(String name) {\n            super(name);\n            mPriority = Process.THREAD_PRIORITY_DEFAULT;\n        }\n        \n        protected void onLooperPrepared() { }\n\n        // 在这个方法开启了 Looper 循环，因为是一个无限循环，所以不适用的时候应该将其停止\n        @Override\n        public void run() {\n            mTid = Process.myTid();\n            Looper.prepare();\n            synchronized (this) {\n                mLooper = Looper.myLooper();\n                notifyAll();\n            }\n            Process.setThreadPriority(mPriority);\n            onLooperPrepared();\n            Looper.loop();\n            mTid = -1;\n        }\n        \n        // 获取该 HandlerThread 对应的 Looper\n        public Looper getLooper() {\n            if (!isAlive()) {\n                return null;\n            }\n            synchronized (this) {\n                while (isAlive() && mLooper == null) {\n                    try {\n                        wait();\n                    } catch (InterruptedException e) { }\n                }\n            }\n            return mLooper;\n        }\n\n        public boolean quit() {\n            Looper looper = getLooper();\n            if (looper != null) {\n                looper.quit();\n                return true;\n            }\n            return false;\n        }\n\n        public boolean quitSafely() {\n            Looper looper = getLooper();\n            if (looper != null) {\n                looper.quitSafely();\n                return true;\n            }\n            return false;\n        }\n\n        // ... 无关代码\n    }\n\n上面的代码比较简单明了，在 run() 方法中初始化 Looper 并执行。如果 Looper 还没有被创建，那么当调用 `getLooper()` 方法获取 Looper 的时候会让线程阻塞。当 Looper 创建完毕之后会唤醒所有阻塞的线程继续执行。另外，就是两个停止 Looper 的方法。它们基本上就是对 Looper 进行了一层封装。\n\n## 2、IntentService\n\n### 2.1 使用 IntentService\n\nIntentService 继承自 Serivce，因此它比普通的多线程任务优先级要高。这使得它相比于普通的异步任务不容易被系统 kill 掉。它内部也是通过一个 Looper 来实现的，所以也是一种消息队列。在研究它的源码之前，我们先来看一下它的使用。\n\nIntentService 的使用是比较简单的，只需要：1).继承它并实现其中的 `onHandleIntent()` 方法；2). 将 IntentService 注册到 manifest 中；3). 像开启一个普通的服务那样开启一个 IntentService 即可。下面是该类的一个使用示例：\n\n    public class FileRecognizeTask extends IntentService {\n\n        public static void start(Context context) {\n            Intent intent = new Intent(context, FileRecognizeTask.class);\n            context.startService(intent);\n        }\n        \n        public FileRecognizeTask() {\n            super(\"FileRecognizeTask\");\n        }\n\n        @Override\n        protected void onHandleIntent(@androidx.annotation.Nullable @Nullable Intent intent) {\n            // 你的需要异步执行的业务逻辑\n        }\n    }\n\nOK，介绍完了 IntentService 的使用，我们再来分析一下它的源码。\n\n### 2.2 IntentService 源码分析\n\n实现 IntentService 的时候使用到了我们上面分析过的 HandlerThread. 首先，在 `onCreate()` 回调方法中创建了一个 HandlerThread，然后使用它的 Looper 创建了一个 ServiceHandler：\n\n    HandlerThread thread = new HandlerThread(\"IntentService[\" + mName + \"]\");\n    thread.start();\n    mServiceLooper = thread.getLooper();\n    mServiceHandler = new ServiceHandler(mServiceLooper);\n\nServiceHandler 是 IntentSerice 的内部类，其定义如下：\n\n    private final class ServiceHandler extends Handler {\n\n        public ServiceHandler(Looper looper) {\n            super(looper);\n        }\n\n        @Override\n        public void handleMessage(Message msg) {\n            onHandleIntent((Intent)msg.obj);\n            stopSelf(msg.arg1);\n        }\n    }\n\nServiceHandler 用来执行被添加到队列中的消息。它会回调 IntentService 中的 `onHandleIntent()` 方法，也就是我们实现业务逻辑的方法。当消息执行完毕之后，会调用 Service 的 `stopSelf(int)` 方法来尝试停止服务。注意这里调用的是 `stopSelf(int)` 而不是 `stopSelf()`。它们之间的区别是，当还存在没有完成的任务的时候 `stopSelf(int)` 不会立即停止服务，而 `stopSelf()` 方法会立即停止服务。\n\nIntentSerivce 的 `onCreate()` 方法会在第一次启动的时候被调用，来创建服务。而 `onStartCommond()` 方法会在每次启动的时候被调用。下面是该方法的定义。\n\n    @Override\n    public void onStart(@Nullable Intent intent, int startId) {\n        Message msg = mServiceHandler.obtainMessage();\n        msg.arg1 = startId;\n        msg.obj = intent;\n        mServiceHandler.sendMessage(msg);\n    }\n\n    @Override\n    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {\n        onStart(intent, startId);\n        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;\n    }\n\n`onStartCommand()` 内部调用了 `onStart()` 来处理请求。在 `onStart()` 方法中会通过 mServiceHandler 创建一个消息，并将该消息发送给 mServiceHandler. 该消息会在被 mServiceHandler 放进消息队列中排队，并在合适的时机被执行。\n\n因此，我们可以总结一下 IntentService 的工作过程：首先，当我们第一次启动 IntentService 的时候会初始化一个 Looper 和 Handler；然后调用它的 onStartCommond() 方法，把请求包装成消息之后发送到消息队列中等待执行；当消息被 Handler 处理的时候会回调 IntentService 的 `onHandleIntent()` 方法来执行。此时，如果又有一个任务需要执行，那么 IntentService 的 onStartCommond() 方法会再次被执行并把请求封装之后放入队列中。当队列中的所有的消息都执行完毕，并且没有新加入的请求，那么此时服务就会自动停止，否则服务还会继续在后台执行。\n\n这里，同样也应该注意下，IntentService 中的任务是按照被添加的顺序来执行的。\n\n## 总结\n\n以上就是我们对 IntentService 和 HandlerThread 的分析。它们都是使用了 Handler 来实现，所以搞懂它们的前提是搞懂 Handler。关于 Handler，还是推荐一下笔者的另一篇文章 [《Android 消息机制：Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)。\n\n\n------\n**如果您喜欢我的文章，可以在以下平台关注我：**\n\n- 个人主页：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n\n\n"
  },
  {
    "path": "异步编程/AsyncTask的使用和源码分析.md",
    "content": "# AsyncTask 的使用和源码分析\n\n## 1、AsyncTask的使用\n\n使用 `AsyncTask` 可以更加简单地实现任务的异步执行，以及任务执行完毕之后与主线程的交互。它被设计用来执行耗时比较短的任务，通常是几秒种的那种，如果要执行耗时比较长的任务，那么就应该使用 **JUC** 包中的框架，比如 `ThreadPoolExecutor` 和 `FutureTask`等。\n\nAsyncTask 用来在后台线程中执行任务，当任务执行完毕之后将结果发送到主线程当中。它有三个重要的泛类型参数，分别是 `Params`、`Progress` 和 `Result`，分别用来指定参数、进度和结果的值的类型。\n以及四个重要的方法，分别是 `onPreExecute()`, `doInBackground()`, `onProgressUpdate()` 和 `onPostExecute()`。\n这四个方法中，除了 `doInBackground()`，其他三个都是运行在UI线程的，分别用来处理在任务开始之前、任务进度改变的时候以及任务执行完毕之后的逻辑，而 `doInBackground()` 运行在后台线程中，用来执行耗时的任务。\n\n一种典型的使用方法如下：\n\n```java\nprivate class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {\n    \n    @Override\n    protected Long doInBackground(URL... urls) {\n        int count = urls.length;\n        long totalSize = 0;\n        for (int i = 0; i < count; i++) {\n            totalSize += Downloader.downloadFile(urls[i]);\n            publishProgress((int) ((i / (float) count) * 100));\n            if (isCancelled()) break;\n        }\n        return totalSize;\n    }\n\n    @Override\n    protected void onProgressUpdate(Integer... progress) {\n        setProgressPercent(progress[0]);\n    }\n\n    @Override\n    protected void onPostExecute(Long result) {\n        showDialog(\"Downloaded \" + result + \" bytes\");\n    }\n}\n```\n\n上面说 `AsyncTask` 有4个重要的方法，这里我们覆写了3个。`doInBackground()` 运行在线程当中，耗时的任务可以放在这里进行；`onProgressUpdate()` 用来处理当任务的进度发生变化时候的逻辑；`onPostExecute()` 用来处理当任务执行完毕之后的逻辑。另外，这里我们还用到了 `publishProgress()` 和 `isCancelled()` 两个方法，分别用来发布任务进度和判断任务是否被取消。\n\n然后，我们可以用下面的方式来使用它：\n\n```java\n    new DownloadFilesTask().execute(url1, url2, url3);\n```\n\n使用AsyncTask的时候要注意以下几点内容：\n\n1. AsyncTask 的类必须在主线程中进行加载，当在4.1之后这个过程会自动进行；\n2. AsyncTask 的对象必须在主线程中创建；\n3. `execute()` 方法必须在UI线程中被调用；\n4. 不要直接调用 `onPreExecute()`, `doInBackground()`, `onProgressUpdate()` 和 `onPostExecute()`；\n5. 一个AsyncTask对象的 `execute()` 方法只能被调用一次；\n\nAndroid 1.6 之前，AsyncTask 是**串行**执行任务的；1.6 采用线程池处理**并行**任务；从 3.0 开始，又采用一个线程来**串行**执行任务。\n3.0 之后可以用 `executeOnExecutor()` 来并行地执行任务，如果我们希望在3.0之后能并行地执行上面的任务，那么我们应该这样去写：\n\n```java\n    new DownloadFilesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);\n```\n\n这里的 `AsyncTask.THREAD_POOL_EXECUTOR` 是 AsyncTask 内部定义的一个线程池，我们可以使用它来将 AsyncTask 设置成并行的。\n\n## 2、AsyncTask源码分析\n\n### 2.1 AsyncTask 的初始化过程\n\n当初始化一个 AsyncTask 的时候，所有的重载构造方法都会调用下面的这个构造方法。这里做了几件事情：\n\n1. 初始化一个 Handler 对象 mHandler，该 Handler 用来将消息发送到它所在的线程中，通常使用默认的值，即主线程的 Handler；\n2. 初始化一个 WorkerRunnable 对象 mWorker。它是一个 `WorkerRunnable` 类型的实例，而 `WorkerRunnable` 又继承自 `Callable`，因此它是一个可以被执行的对象。我们会把在该对象中回调 `doInBackground()` 来将我们的业务逻辑放在线程池中执行。\n3. 初始化一个 FutureTask 对象 mFuture。该对象包装了 `mWorker` 并且当 `mWorker` 执行完毕之后会调用它的 `postResultIfNotInvoked()` 方法来通知主线程（不论任务已经执行完毕还是被取消了，都会调用这个方法）。\n\n```java\n    public AsyncTask(@Nullable Looper callbackLooper) {\n        // 1. 初始化用来发送消息的 Handler\n        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()\n            ? getMainHandler()\n            : new Handler(callbackLooper);\n\n        // 2. 封装一个对象用来执行我们的任务\n        mWorker = new WorkerRunnable<Params, Result>() {\n            public Result call() throws Exception {\n                mTaskInvoked.set(true);\n                Result result = null;\n                try {\n                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);\n                    // 回调我们的业务逻辑\n                    result = doInBackground(mParams);\n                    Binder.flushPendingCommands();\n                } catch (Throwable tr) {\n                    mCancelled.set(true);\n                    throw tr;\n                } finally {\n                    // 发送结果给主线程\n                    postResult(result);\n                }\n                return result;\n            }\n        };\n\n        // 3. 初始化一个 FutureTask，并且当它执行完毕的时候，会调用 postResultIfNotInvoked 来将消息的执行结果发送到主线程中\n        mFuture = new FutureTask<Result>(mWorker) {\n            @Override\n            protected void done() {\n                try {\n                    // 如果任务没有被触发，也要发送一个结果\n                    postResultIfNotInvoked(get());\n                } catch (InterruptedException e) {\n                    android.util.Log.w(LOG_TAG, e);\n                } catch (ExecutionException e) {\n                    throw new RuntimeException(\"An error occurred while executing doInBackground()\", e.getCause());\n                } catch (CancellationException e) {\n                    postResultIfNotInvoked(null);\n                }\n            }\n        };\n    }\n```\n\n当这样设置完毕之后，我们就可以使用 `execute()` 方法来开始执行任务了。\n\n### 2.2 AsyncTask 中任务的串行执行过程\n\n我们从 `execute()` 方法开始分析 AsyncTask，\n\n```java\n    @MainThread\n    public final AsyncTask<Params, Progress, Result> execute(Params... params) {\n        return executeOnExecutor(sDefaultExecutor, params);\n    }\n\n    @MainThread\n    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {\n        if (mStatus != Status.PENDING) { // 1.判断线程当前的状态\n            switch (mStatus) {\n                case RUNNING: throw new IllegalStateException(...);\n                case FINISHED: throw new IllegalStateException(...);\n            }\n        }\n        mStatus = Status.RUNNING;\n        onPreExecute();             // 2.回调生命周期方法\n        mWorker.mParams = params;   // 3.赋值给可执行的对象 WorkerRunnable\n        exec.execute(mFuture);      // 4.在线程池中执行任务\n        return this;\n    }\n```\n\n当我们调用 AsyncTask 的 `execute()` 方法的时候会立即调用它的 `executeOnExecutor()` 方法。这里传入了两个参数，分别是一个 `Executor` 和任务的参数 `params`。从上面我们可以看出，当直接调用 execute() 方法的时候会使用默认的线程池 `sDefaultExecutor`，而当我们指定了线程池之后，会使用我们指定的线程池来执行任务。\n\n在 1 处，会对 AsyncTask 当前的状态进行判断，这就对应了前面说的，一个任务只能被执行一次。在 2 处会调用 `onPreExecute()` 方法，如果我们覆写了该方法，那么它就会在这个时候被调用。在 3 处的操作是在为 `mWorker` 赋值，即把调用 `execute` 方法时传入的参数赋值给了 `mWorker`。接下来，会将 `mFuture` 添加到线程池中执行。\n\n当我们不指定任何线程池的时候使用的 `sDefaultExecutor` 是一个串行的线程池，它的定义如下：\n\n```java\n    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();\n    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;\n\n    private static class SerialExecutor implements Executor {\n        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();\n        Runnable mActive;\n\n        public synchronized void execute(final Runnable r) {\n            mTasks.offer(new Runnable() {\n                public void run() {\n                    try {\n                        // 相当于对传入的Runnable进行了一层包装\n                        r.run();\n                    } finally {\n                        // 分配下一个任务\n                        scheduleNext();\n                    }\n                }\n            });\n            // 如果当前没有正在执行的任务，那么就尝试从队列中取出并执行\n            if (mActive == null) {\n                scheduleNext();\n            }\n        }\n\n        protected synchronized void scheduleNext() {\n            // 从队列中取任务并使用THREAD_POOL_EXECUTOR执行\n            if ((mActive = mTasks.poll()) != null) {\n                THREAD_POOL_EXECUTOR.execute(mActive);\n            }\n        }\n    }\n```\n\n从上面我们可以看出，我们添加到线程池中的任务实际上并没有直接交给线程池来执行，而是对其进行了处理之后才执行的，SerialExecutor 通过内部维护了双端队列，每当一个 AsyncTask 调用 `execute()` 方法的时候都会被放在该队列当中进行排队。如果当前没有正在执行的任务，那么就从队列中取一个任务交给 `THREAD_POOL_EXECUTOR` 执行；当一个任务执行完毕之后又会调用 `scheduleNext()` 取下一个任务执行。也就是说，实际上 `sDefaultExecutor` 在这里只是起了一个任务调度的作用，任务最终还是交给 `THREAD_POOL_EXECUTOR` 执行的。\n\n这里的`THREAD_POOL_EXECUTOR`也是一个线程池，它在静态代码块中被初始化：\n\n```java\n    static {\n        // 使用指定的参数创建一个线程池\n        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(\n                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,\n                sPoolWorkQueue, sThreadFactory);\n        threadPoolExecutor.allowCoreThreadTimeOut(true);\n        THREAD_POOL_EXECUTOR = threadPoolExecutor;\n    }\n```\n\n我们也可以直接将这个静态的线程池作为我们任务执行的线程池而不是放在上面的队列中被串行地执行。\n\n### 2.3 将任务执行的结果发送到其他线程\n\n上面的 `WorkerRunnable` 中已经用到了 `postResult` 方法，它用来将任务执行的结果发送给 `Handler`：\n\n```java\n    private Result postResult(Result result) {\n        @SuppressWarnings(\"unchecked\")\n        Message message = mHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result));\n        message.sendToTarget();\n        return result;\n    }\n```\n\n`mHandler` 会在创建 AsyncTask 的时候初始化。我们可以通过 AsyncTask 的构造方法传入 Handler 和 Looper 来指定该对象所在的线程。当我们没有指定的时候，会使用 AsyncTask 内部的 `InternalHandler` 创建 `Handler`：\n\n```java\n    private final Handler mHandler;\n\n    public AsyncTask(@Nullable Looper callbackLooper) {\n        // 根据传入的参数创建Handler对象\n        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper() \n            ? getMainHandler() : new Handler(callbackLooper);\n    }\n\n    private static Handler getMainHandler() {\n        synchronized (AsyncTask.class) {\n            if (sHandler == null) {\n                // 使用 InternalHandler 创建对象\n                sHandler = new InternalHandler(Looper.getMainLooper());\n            }\n            return sHandler;\n        }\n    }\n\n    // AsyncTask 内部定义 的Handler 类型\n    private static class InternalHandler extends Handler {\n        public InternalHandler(Looper looper) {\n            super(looper);\n        }\n\n        @Override public void handleMessage(Message msg) {\n            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;\n            // 根据传入的消息类型进行处理\n            switch (msg.what) {\n                case MESSAGE_POST_RESULT: result.mTask.finish(result.mData[0]); break;\n                case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break;\n            }\n        }\n    }\n```\n\n## 3、总结\n\n上面我们梳理了 AsyncTask 的大致过程，我们来梳理下：\n\n每当我们实例化一个 AsyncTask 的时候都会在内部封装成一个 Runnable 对象，该对象可以直接放在线程池中执行。这里存在两个线程池，一个是 SerialExecutor 一个是 THREAD_POOL_EXECUTOR，前者主要用来进行任务调度，即把交给线程的任务放在队列中进行排队执行，而时机上所有的任务都是在后者中执行完成的。这个两个线程池都是静态的字段，所以它们对应于整个类的。也就是说，当使用默认的线程池的时候，实例化的 AsyncTask 会一个个地，按照加入到队列中的顺序依次执行。\n\n当任务执行完毕之后，使用 Handler 来将消息发送到主线程即可，这部分的逻辑主要与 Handler 机制相关，可以通过这篇文章来了解：[《Android 消息机制：Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)。\n\n以上就是 AsyncTask 的主要内容。\n\n\n------\n**如果您喜欢我的文章，可以在以下平台关注我：**\n\n- 个人主页：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n\n\n"
  },
  {
    "path": "性能优化/Android性能优化-ANR.md",
    "content": "# Android 性能优化 - ANR 的原因和解决方案\n\n## 1、出现 ANR 的情况\n\n满足下面的一种情况系统就会弹出 ANR 提示\n\n1. 输入事件(按键和触摸事件) 5s 内没被处理；\n2. BroadcastReceiver 的事件 ( `onRecieve()` 方法) 在规定时间内没处理完 (前台广播为 10s，后台广播为 60s)；\n3. Service 前台 20s 后台 200s 未完成启动；\n4. ContentProvider 的 `publish()` 在 10s 内没进行完。\n\n通常情况下就是主线程被阻塞造成的。\n\n## 2、ANR 的实现原理\n\n以输入无响应的过程为例（基于 9.0 代码）：\n\n最终弹出 ANR 对话框的位置是与 AMS 同目录的类 `AppErrors` 的 `handleShowAnrUi()` 方法。这个类用来处理程序中出现的各种错误，不只 ANR，强行 Crash 也在这个类中处理。\n\n```java\n    // base/core/java/com/android/server/am/AppErrors.java\n    void handleShowAnrUi(Message msg) {\n        Dialog dialogToShow = null;\n        synchronized (mService) {\n            AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj;\n            // ...\n\n            Intent intent = new Intent(\"android.intent.action.ANR\");\n            if (!mService.mProcessesReady) {\n                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY\n                        | Intent.FLAG_RECEIVER_FOREGROUND);\n            }\n            mService.broadcastIntentLocked(null, null, intent,\n                    null, null, 0, null, null, null, AppOpsManager.OP_NONE,\n                    null, false, false, MY_PID, Process.SYSTEM_UID, 0 /* TODO: Verify */);\n\n            boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),\n                    Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;\n            if (mService.canShowErrorDialogs() || showBackground) {\n                dialogToShow = new AppNotRespondingDialog(mService, mContext, data);\n                proc.anrDialog = dialogToShow;\n            } else {\n                MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,\n                        AppNotRespondingDialog.CANT_SHOW);\n                // Just kill the app if there is no dialog to be shown.\n                mService.killAppAtUsersRequest(proc, null);\n            }\n        }\n        // If we've created a crash dialog, show it without the lock held\n        if (dialogToShow != null) {\n            dialogToShow.show();\n        }\n    }\n```\n\n不过从发生 ANR 的地方调用到这里要经过很多的类和方法。最初抛出 ANR 是在 `InputDispatcher.cpp` 中。我们可以通过其中定义的常量来寻找最初触发的位置：\n\n```C++\n// native/services/inputflinger/InputDispatcher.cpp\nconstexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; // 5 sec\n```\n\n从这个类触发的位置会经过层层传递达到 `InputManagerService` 中\n\n```java\n    // base/services/core/java/com/android/server/input/InputManagerService.java\n    private long notifyANR(InputApplicationHandle inputApplicationHandle,\n            InputWindowHandle inputWindowHandle, String reason) {\n        return mWindowManagerCallbacks.notifyANR(\n                inputApplicationHandle, inputWindowHandle, reason);\n    }\n```\n\n这里的 `mWindowManagerCallbacks` 就是 `InputMonitor` ：\n\n```java\n    // base/services/core/java/com/android/server/wm/InputMonitor.java\n    public long notifyANR(InputApplicationHandle inputApplicationHandle,\n            InputWindowHandle inputWindowHandle, String reason) {\n        // ... 略\n\n        if (appWindowToken != null && appWindowToken.appToken != null) {\n            final AppWindowContainerController controller = appWindowToken.getController();\n            final boolean abort = controller != null\n                    && controller.keyDispatchingTimedOut(reason,\n                            (windowState != null) ? windowState.mSession.mPid : -1);\n            if (!abort) {\n                return appWindowToken.mInputDispatchingTimeoutNanos;\n            }\n        } else if (windowState != null) {\n            try {\n                // 使用 AMS 的方法\n                long timeout = ActivityManager.getService().inputDispatchingTimedOut(\n                        windowState.mSession.mPid, aboveSystem, reason);\n                if (timeout >= 0) {\n                    return timeout * 1000000L; // nanoseconds\n                }\n            } catch (RemoteException ex) {\n            }\n        }\n        return 0; // abort dispatching\n    }\n```\n\n然后回在上述方法调用 AMS 的 `inputDispatchingTimedOut()` 方法继续处理，并最终在 `inputDispatchingTimedOut()` 方法中将事件传递给 `AppErrors`\n\n```java\n    // base/services/core/java/com/android/server/am/ActivityManagerService.java\n    public boolean inputDispatchingTimedOut(final ProcessRecord proc,\n            final ActivityRecord activity, final ActivityRecord parent,\n            final boolean aboveSystem, String reason) {\n        // ...\n\n        if (proc != null) {\n            synchronized (this) {\n                if (proc.debugging) {\n                    return false;\n                }\n\n                if (proc.instr != null) {\n                    Bundle info = new Bundle();\n                    info.putString(\"shortMsg\", \"keyDispatchingTimedOut\");\n                    info.putString(\"longMsg\", annotation);\n                    finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info);\n                    return true;\n                }\n            }\n            mHandler.post(new Runnable() {\n                @Override\n                public void run() {\n                    // 使用 AppErrors 继续处理\n                    mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation);\n                }\n            });\n        }\n\n        return true;\n    }\n```\n\n当事件传递到了 `AppErrors` 之后，它会借助 Handler 处理消息也就调用了最初的那个方法并弹出对话框。\n\n参考：[《Android ANR原理分析》](https://www.cnblogs.com/android-blogs/p/5718302.html)\n\n## 3、ANR 的解决办法\n\n上面分析了 ANR 的成因和原理，下面我们分析下如何解决 ANR. \n\n### 1. 使用 adb 导出 ANR 日志并进行分析\n\n发生 ANR的时候系统会记录 ANR 的信息并将其存储到 `/data/anr/traces.txt` 文件中（在比较新的系统中会被存储都 `/data/anr/anr_*` 文件中）。我们可以使用下面的方式来将其导出到电脑中以便对 ANR 产生的原因进行分析：\n\n    adb root\n    adb shell ls /data/anr\n    adb pull /data/anr/<filename>\n\n*在笔者分析 ANR 的时候使用上述指令尝试导出 ANR 日志的时候都出现了 Permission Denied。此时，你可以将手机 Root 之后导出，或者尝试修改文件的读写权限，或者在开发者模式中选择将日志导出到 sdcard 之后再从 sdcard 将日志发送到电脑端进行查看*\n\n### 2. 使用 DDMS 的 traceview 进行分析\n\n在 AS 中打开 DDMS，或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。\n\nTraceView 工具的使用可以参考这篇文章：[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558)\n\n这种定位 ANR 的思路是：**使用 TraceView 来通过耗时方法调用的信息定位耗时操作的位置**。\n\n资料：\n\n- [《ANR 官方文档》](https://developer.android.com/topic/performance/vitals/anr)\n- [《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558)\n\n### 3. 使用开源项目 ANR-WatchDog 来检測 ANR\n\n项目地址是 [Github-ANR-WatchDog](https://github.com/SalomonBrys/ANR-WatchDog)\n\n该项目的实现原理：创建一个检测线程，该线程不断往 UI 线程 post 一个任务，然后睡眠固定时间，等该线程又一次起来后检測之前 post 的任务是否运行了，假设任务未被运行，则生成 ANRError，并终止进程。\n\n### 4. 常见的 ANR 场景\n\n1. I/O 阻塞\n2. 网络阻塞\n3. 多线程死锁\n4. 由于响应式编程等导致的方法死循环\n5. 由于某个业务逻辑执行的时间太长\n\n### 5. 避免 ANR 的方法\n\n1. UI 线程尽量只做跟 UI 相关的工作；\n2. 耗时的工作 (比如数据库操作，I/O，网络操作等)，采用单独的工作线程处理；\n3. 用 Handler 来处理 UI 线程和工作线程的交互；\n4. 使用 RxJava 等来处理异步消息。\n\n总之，一个原则就是：**不在主线程做耗时操作**。\n\n"
  },
  {
    "path": "性能优化/Android性能优化-内存优化.md",
    "content": "# Android 性能优化 - 内存优化\n\n\n\n\n\n\n\n"
  },
  {
    "path": "性能优化/Android性能优化-启动优化.md",
    "content": "# Android 性能优化 - 启动优化\n\n## 1、基础\n\n### 1.1 启动的类型\n\n首先是启动的三种类型：\n\n1. **冷启动场景**：后台完全没有任何进程的情况下，启动最慢；\n2. **温启动场景**：按返回键退回主界面再从主界面打开的情形，较快；\n3. **热启动场景**：按 Home 键退回到主界面再从主界面打开的情形，最快。\n\n应用启动的过程实际上也就是 Activity 启动的流程，所以具体涉及的源码不是我们这里的重点，你可以查找 Activity 启动流程相关的文章来了解源码。\n\n其实优化应用的启动速度无非也就是在那几个生命周期方法中进行优化，不做太多耗时操作等：Application 的生命周期和 Activity 的生命周期。\n\n### 1.2 启动速度的测量\n\n当然，我们而已通过自己的感觉判断启动的快慢，但量化还是非常重要的，不然你都无法向 PM 交差不是。所以，我们有必要了解下 Android 中的启动速度是如何测量的。\n\n#### 方式 1：使用 ADB\n\n获取启动速度的第一种方式是使用 ADB，使用下面的指令的时候在启动应用的时候会使用 AMS 进行统计。但是缺点是统计时间不够准确：\n\n```shell\nadb shell am start -n ｛包名｝/｛包名｝.{活动名}\n```\n\n#### 方式 2：代码埋点\n\n在 Application 的 `attachBaseContext()` 方法中记录开始时间，第一个 Activity 的 `onWindowFocusChanged()` 中记录结束时间。缺点是统计不完全，因为在 `attachBaseContext()` 之前还有许多操作。\n\n#### 方式 3：TraceView\n\n在 AS 中打开 DDMS，或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。\n\nTraceView 工具的使用可以参考这篇文章：[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558)\n\n通过 TraceView 主要可以得到两种数据：单次执行耗时的方法以及执行次数多的方法。但 TraceView 性能耗损太大，不能比较正确反映真实情况。\n\n#### 方式 4：Systrace\n\nSystrace 能够追踪关键系统调用的耗时情况，如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。\n\n#### 方式 5：Systrace + 插桩\n\n类似于 AOP，通过切面为每个函数统计执行时间。这种方式的好处是能够准确统计各个方法的耗时。\n\n原理就是\n\n```java\n    public void method() {\n        TraceMethod.i();\n        // Real work\n        TraceMethod.o();\n    }\n```\n\n#### 方式 6：录屏\n\n录屏方式收集到的时间，更接近于用户的真实体感。可以在录屏之后按帧来进行统计分析。\n\n## 2、启动优化\n\n### 2.1 一般解决办法\n\n#### 2.1.1 延迟初始化\n\n一些逻辑，如果没必要在程序启动的时候就立即初始化，那么可以将其推迟到需要的时候再初始化。比如，我们可以使用单例的方式来获取类的实例，然后在获取实例的时候再进行初始化操作。\n\n**但是需要注意的是，懒加载要防止集中化，否则容易出现首页显示后用户无法操作的情形。可以按照耗时和是否必要将业务划分到四个维度：必要且耗时，必要不耗时，非必要但耗时，非必要不耗时。**然后对应不同的维度来决定是否有必要在程序启动的时候立即初始化。\n\n#### 2.1.2 防止主线程阻塞\n\n一般我们也不会把耗时操作放在主线程里面，毕竟现在有了 RxJava 之后，在程序中使用异步代价并不高。这种耗时操作包括，大量的计算、IO、数据库查询和网络访问等。\n\n另外，关于开启线程池的问题下面的话总结得比较好，除了一般意义上线程池和使用普通线程的区别，还要考虑应用启动这个时刻的特殊性：\n\n> 如何开启线程同样也有学问：Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService 等都各有利弊；例如通常情况下 ThreadPoolExecutor 比 Thread 更加高效、优势明显，但是特定场景下单个时间点的表现 Thread 会比 ThreadPoolExecutor 好：同样的创建对象，ThreadPoolExecutor 的开销明显比 Thread 大。\n>   \n> 来自：https://www.jianshu.com/p/f5514b1a826c  \n\n#### 2.1.3 布局优化\n\n比如，之前我在使用 Fragment 和 ViewPager 搭配的时候，发现虽然 Fragment 可以被复用，但是如果通过 Adapter 为 ViewPager 的每个项目指定了标题，那么这些标题控件不会被复用。当 ViewPager 的条目比较多的时候，甚至会造成 ANR. \n\n对于这种布局优化相关的东西，可以参考性能优化的 [Android性能优化-布局优化](Android性能优化-布局优化.md) 模块。\n\n#### 2.1.4 使用启动页面防止白屏\n\n这种方法只是治标不治本的方法，就是在应用启动的时候避免白屏，可以通过设置自定义主题来实现。\n\n这种实现方式可以参考我的开源项目 [MarkNote](https://github.com/Shouheng88/MarkNote) 的实现。\n\n### 2.2 其他借鉴办法\n\n#### 2.2.1 使用 BlockCanary 检测卡顿\n\nBlockCanary 是一个开源项目，类似于 LeakCanary （很多地方也借鉴了 LeakCanary 的东西），主要用来检测程序中的卡顿，项目地址是 [Github-BlockCanary](https://github.com/markzhai/AndroidPerformanceMonitor). 它的原理是对 Looper 中的 `loop()` 方法打处的日志进行处理，通过一个自定义的日志输出 Printer 监听方法执行的开始和结束。可以通过该项目作者的文章来了解这个项目：\n\n[BlockCanary — 轻松找出Android App界面卡顿元凶](https://www.jianshu.com/p/cd7fc77405ac)\n\n#### 2.2.2 GC 优化\n\nGC 优化的思想就是减少垃圾回收的时间间隔，所以在启动的过程中不要频繁创建对象，特别是大对象，避免进行大量的字符串操作，特别是序列化跟反序列化过程。一些频繁创建的对象，例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象，可以考虑移到 Native 实现。\n\n#### 2.2.3 类重排\n\n如果我们的代码在打包的时候被放进了不同的 dex 里面，当启动的时候，如果需要用到的类分散在各个 dex 里面，那么系统要花额外的时间到各个 dex 里加载类。因此，我们可以通过类重排调整类在 Dex 中的排列顺序，把启动时用到的类放进主 dex 里。\n\n目前可以使用 [ReDex](https://github.com/facebook/redex) 的 [Interdex](https://github.com/facebook/redex/blob/master/docs/Interdex.md) 调整类在 Dex 中的排列顺序。\n\n可以参考下面这篇文章来了解类重拍在手 Q 中的应用以及他们遇到的各种问题：\n\n[Redex 初探与 Interdex：Andorid 冷启动优化](https://mp.weixin.qq.com/s/Bf41Kez_OLZTyty4EondHA?)\n\n#### 2.2.4 资源文件重排\n\n对应于类重排，还有资源的重排。可以参考下阿里的资源重排优化方案：\n\n[支付宝 App 构建优化解析：通过安装包重排布优化 Android 端启动性能](https://mp.weixin.qq.com/s/79tAFx6zi3JRG-ewoapIVQ)\n\n这种方案的原理时先通过测试找出程序启动过程中需要加载的资源，然后再打包的时候通过修改 7z 压缩工具将上述热点资源放在一起。这样，在系统进行资源加载的时候，这些资源将要用到的资源会一起被加载进程内存当中并缓存，减少了 IO 的次数，同时不需要从磁盘读取文件，来提高应用启动的速度。\n\n#### 2.2.5 类的加载\n\n通过 Hook 来去掉应用启动过程中的 verify 来减少启动过程中的耗时。但是这种方式存在虚拟机兼容的问题，在 ART 虚拟机上面进行 Hook 需要兼容几个版本。\n\n\n参考资料：\n\n- [App startup time](https://developer.android.com/topic/performance/vitals/launch-time)\n- [Android性能优化（一）之启动加速35%](https://www.jianshu.com/p/f5514b1a826c)\n- [爱奇艺技术分享：爱奇艺Android客户端启动速度优化实践总结](https://www.jianshu.com/p/bd3930316c8d)"
  },
  {
    "path": "性能优化/Android性能优化-布局优化.md",
    "content": "# Android 性能优化 - 布局优化\n\n### 1. 合理选择 ViewGroup\n\n在选择使用 Android 中的布局方式的时候应该遵循：**尽量少使用性能比较低的容器控件,比如 RelativeLayout，但如果使用 RelativeLayout 可以降低布局的层次的时候可以考虑使用**。\n\nAndroid 中的控件是树状的，降低树的高度可以提升布局性能。RelativeLayout 的布局比 FrameLayout、LinearLayout 等简单，因而可以减少计算过程，提升程序性能。\n\n*注：参见第 9 条，关于 ConstaintLayout 的介绍。*\n\n### 2. 使用 `<include>` 标签复用布局\n\n**多个地方共用的布局可以使用 `<include>` 标签在各个布局中复用**\n\n```xml\n   <include android:id=\"@+id/bar_layout\" layout=\"@layout/layout_toolbar\"/>\n```\n\n### 3. 使用 `<merge>` 标签复用父容器\n\n**可以通过使用 `<merge>` 来降低布局的层次**。 `<merge>` 标签通常与 `<include>` 标签一起使用， `<merge>` 作为可以复用的布局的根控件。然后使用 `<include>` 标签引用该布局。\n\n```xml\n    <!--布局1-->\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n        <include android:id=\"@+id/bar_layout\" layout=\"@layout/layout_toolbar\"/>\n    </RelativeLayout>\n    <!--布局2-->\n    <merge>\n        <Button\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n    </merge>\n```\n\n上面的布局方式中布局 2 被布局 1 引用之后其 Button 的控件父容器将会是布局 1 中的 RelativeLayout. 如果我们不使用 `<merge>` 标签而是一个单独的父容器的话就会多一层布局。\n\n### 4. 使用 `<ViewStub>` 标签动态加载布局\n\n**`<ViewStub>` 标签可以用来在程序运行的时候决定加载哪个布局，而不是一次性全部加载**。\n\n```xml\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\">\n        <ViewStub\n            android:id=\"@+id/stub\"\n            android:inflatedId=\"@+id/inflatedStart\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout=\"@layout/start\" />\n    </LinearLayout>\n```\n\n然后我们可以使用下面两种方式将布局加载进来：\n\n```java\n((ViewStub) findViewById(R.id.stub)).setVisibility(View.VISIBILITY); // 方式 1\nView v = ((ViewStub) findViewById(R.id.stub)).inflate(); // 方式 2\n```\n\n### 5. 性能分析：使用 Android Lint 来分析布局\n\n在 AS 的 `Analyze->Inspect Code` 中配置检测的范围，然后开启检测，Android Lint 会对布局进行分析，然后对存在的问题，给出建议\n\n可以在 `Settings->Editor->Inspections` 中配置 Android Lint.\n\nAndroid Lint 不仅可以检测布局，还可以检测 Java 代码。\n\n### 6. 性能分析：避免过度绘制\n\n在手机的开发者选项中的**绘图**选项中选择**显示布局边界**来查看布局\n\n蓝色、淡绿、淡红，深红代表了4种不同程度的 Overdraw 的情况，我们的目标就是尽量减少红色 Overdraw，看到更多的蓝色区域。\n\n### 7. 性能分析：Hierarchy View\n\n可以通过 Hierarchy View 来获取当前的 View 的层次图\n\n在最新的 AS 中已经移除了，可以到 sdk 的 tools 目录下，打开 `monitor.bat`，然后工具来中的 DDMS 旁边的 `Open Perspective` 中打开 Hierarchy View\n\n资料：\n\n- [官方文档中关于 Hierarchy Viewer 的介绍](http://developer.android.com/tools/debugging/debugging-ui.html)\n\n### 8. 使用 ConstaintLayout　\n\nConstaintLayout 是 Google 在 2016 年的 I/O 大会上发布的一个新的布局方式。可以通过阅读下面的资料来学习它的使用：\n\n- [官方文档中关于 ConstraintLayout 的介绍](https://developer.android.google.cn/reference/android/support/constraint/ConstraintLayout)\n- [Android新特性介绍，ConstraintLayout完全解析 - 郭霖](https://blog.csdn.net/guolin_blog/article/details/53122387)\n\n这种布局方式的好处是，布局更加灵活，它在性能上面的贡献是，使用它可以降低布局的层次。\n\n### 9. 性能分析：使用 systrace 分析 UI 性能\n\n使用 Systrace 你需要：\n\n1. 下载最新的 Android SDK Tools；\n2. 安装 Python 环境；\n3. 连接 API 18 以上的设备。\n\nSystrace 的目录位于 `sdk/platform-tools/systrace/`\n\nSystrace 的命令的格式：\n\n```python\npython systrace.py [options] [categories]\n```\n\n你可以通过阅读官方文档来了解它的各个选项和参数的意义。\n\n资料：\n\n- [官方文档中关于 Systrace 的介绍](https://developer.android.com/studio/command-line/systrace)\n\n\n\n"
  },
  {
    "path": "性能优化/Android相机Camera1资料.md",
    "content": ""
  },
  {
    "path": "性能优化/Android相机Camera2资料.md",
    "content": "# Android 相机开发资料梳理\n\n使用相机的前提是获取到 CameraManager 对象，然后从 CameraManager 中获取 CameraDevice 对象。每个 CameraDevice 对象包含了一系列的属性信息。这些信息可以从 CameraCharacteristics 对象中获取。而 CameraCharacteristics 对象又通过 `getCameraCharacteristics(String)` 方法得到。\n\n要拍摄照片或者视频，需要先使用一系列的 Surface 调用 `createCaptureSession(SessionConfiguration)` 方法创建相机拍摄会话。每个 Surface 需要先被只适当的大小和格式，以与相机设别中的尺寸和格式匹配。目标 Surface 可以通过很多的类来获取，包括  SurfaceView, SurfaceTexture (通过 `Surface(SurfaceTexture)` 获取), MediaCodec, MediaRecorder, Allocation 和 ImageReader.\n\n一般，相机预览图片会被设置给 SurfaceView 或者 TextureView（通过它的 SurfaceTexture）. 拍摄 JPEG 图片或者 DngCreator 的 RAW 缓存可以通过 ImageReader 和 JPEG 或者 RAW_SENSOR 格式。对于 RenderScript, OpenGL ES 或者直接通过 native 代码操作的数据，最好分别通过 Allocation 和 YUV Type, SurfaceTexture, ImageReader 和 YUV_420_888 格式完成。\n\n然后应用需要创建 CaptureRequest 来设置相机设备拍摄图片需要的各种参数。该请求还要列出哪些输出的 Surface 应该被用作目标 Surface. CameraDevice 又一个工厂方法用来创建指定用途的请求构建器。\n\n请求创建完毕之后，可以交给会话作为一次拍摄或者多次连续拍摄使用。连续请求的优先级低于拍摄。\n\n请求处理完毕，CameraDeive 会创建一个 TotalCaptureResult 对象, 它包含了相机设备拍摄时的信息和配置。相机同时会将图片数据片发送给请求包含的 Surface. 这些都是异步执行的。\n\n## 1、关于 SurfaceView 和 TextureView\n\n一般当开启相机的预览的时候，我们会在这两个接口的回调方法中打开相机，并将预览相关的 Surface 和 SurfaceTexture 赋值给 Camera. 我们需要了解它的回调方法都是在什么时候被调用，以便于我们准确地对 CameraPreview 进行封装。\n\n### 1.1 SurfaceView\n\n#### 1.1.1 关于 SurfaceHolder.Callback 接口\n\n`SurfaceHolder.Callback` 用来获取 surface 的改变的信息，surface 只在 `surfaceCreated()` 和 `surfaceDestroyed()` 之间可用。\n\n1. `surfaceCreated(SurfaceHolder)`：surface 创建的时候调用，只有一个线程可以对 Surface 进行绘制，所以如果正常的绘制在其他线程的时候，就不应该在这里对 Surface 进行绘制\n2. `surfaceChanged(SurfaceHolder, int, int, int)`：当 surface 的尺寸和大小发生变化的时候调用，应该在这个时刻更新 surface 上的图像，该方法至少被回调一次。\n3. `surfaceDestroyed(SurfaceHolder)`：该方法结束之后就不能再对 surface 进行绘制了。\n\n### 1.2 TextureView\n\n#### 1.2.1 关于 TextureView.SurfaceTextureListener 接口\n\n`TextureView.SurfaceTextureListener` 当 SurfaceTexture 发生变化的时候会被回调。\n\n1. `onSurfaceTextureAvailable(SurfaceTexture, int, int)`：当 SurfaceTexture 可用的时候被回调。\n2. `onSurfaceTextureSizeChanged(SurfaceTexture, int, int)`：当 SurfaceTexture 的 buffers 大小改变的时候被回调。\n3. `boolean onSurfaceTextureDestroyed(SurfaceTexture)`：SurfaceTexture 将要被销毁的时候回调，返回 true 的时候，SurfaceTexture 将无法再进行绘制，返回 false 的时候，需要用户手动释放。一般情况下都是返回 true.\n4. `onSurfaceTextureUpdated(SurfaceTexture)`：当 SurfaceTexture 的 `updateTextImage()` 被调用的时候被回调。\n\n#### 1.2.2 SurfaceTexture 的方法\n\n1. `setDefaultBufferSize(int, int)`：设置图片缓存的默认的大小，图片的生产者可能会重写这个值，此时重写的值将会被调用。\n\n## 2、Camera 2 的一些类\n\n### 2.0 CameraManager\n\n对应于相机的系统服务，连接到 CameraDevice.\n\n|公共方法||\n|:-|:-|\n|CameraCharacteristics|`getCameraCharacteristics(String cameraId)` 获取相机设备的功能|\n|String[]|`getCameraIdList()` 获取相机设备可用的相机 id 列表，也包括其他应用可能正在使用的相机|\n|void|`openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)` 打开指定 id 的相机|\n|void|`openCamera(String cameraId, Executor executor, CameraDevice.StateCallback callback)` 打开指定 id 的相机|\n|void|`registerAvailabilityCallback(Executor executor, CameraManager.AvailabilityCallback callback)` 注册当相机设备可用时的回调|\n|void|`registerAvailabilityCallback(CameraManager.AvailabilityCallback callback, Handler handler)` 注册当相机设备可用时的回调|\n|void|`registerTorchCallback(CameraManager.TorchCallback callback, Handler handler)` 注册用来获取火炬模式状态的回调|\n|void|`registerTorchCallback(Executor executor, CameraManager.TorchCallback callback)` 注册用来获取火炬模式状态的回调|\n|void|`setTorchMode(String cameraId, boolean enabled)` 设置闪光灯单元的火炬模式，而不用打开指定的相机|\n|void|`unregisterAvailabilityCallback(CameraManager.AvailabilityCallback callback)` 移除之前添加的回调|\n|void|`unregisterTorchCallback(CameraManager.TorchCallback callback)` 移除之前添加的回调|\n\n### 2.1 CameraDevice\n\nCameraDevice 通过 `CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL` 定义了一系列的支持的等级，下面是支持的的等级的一些说明：\n\n1. `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY`：相机设备处于向后兼容模式，支持最低的 camera2 API;\n2. `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED`：此时 Camera2 的特性基本等同于 Camera；\n3. `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL`：设备使用的是可移动的相机，特性近似 `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED`；\n4. `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_FULL` 和 `CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_3`：相机的特性比 Camera API 略多。\n\n如果你的应用需要最高级别的操作，需要在 manifest 中声明 `android.hardware.camera.level.full` 特性。\n\n|公共方法|说明|\n|:-|:-|\n|abstract void|`close()` 尽快关闭该相机设备的连接|\n|CaptureRequest.Builder|`createCaptureRequest(int templateType, Set<String> physicalCameraIdSet)` 创建新的拍摄请求的 CaptureRequest.Builder 构建者，并通过指定用途的 templateType 初始化|\n|abstract CaptureRequest.Builder|`createCaptureRequest(int templateType)` 创建新的拍摄请求的 CaptureRequest.Builder 构建者，并通过指定用途的 templateType 初始化。传入的是一个枚举，但不是所有的设备都支持所有的枚举类型。枚举的值包括：1.`TEMPLATE_PREVIEW`: 创建适用于相机预览的请求；2.`TEMPLATE_STILL_CAPTURE`: 创建适用于静态图像的请求；3.`TEMPLATE_RECORD`: 创建适用于视频拍摄的请求；4.`TEMPLATE_VIDEO_SNAPSHOT`: 创建适用于拍摄视频时的静态图像的请求；5.`TEMPLATE_ZERO_SHUTTER_LAG`: 创建适用于零快门静态拍摄的请求；6.`TEMPLATE_MANUAL`:|\n|void|`createCaptureSession(SessionConfiguration config)` 使用包含所有配置新的 SessionConfiguration 创建新的 CameraCaptureSession|\n|abstract void|`createCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)` 创建新的的相机拍摄会话。可以在回调的方法中得到该 CameraCaptureSession。|\n|abstract void|`createCaptureSessionByOutputConfigurations(List<OutputConfiguration> outputConfigurations, CameraCaptureSession.StateCallback callback, Handler handler)` 创建新的的相机拍摄会话|\n|abstract void|`createConstrainedHighSpeedCaptureSession(List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)` 创建告诉拍摄会话|\n|abstract CaptureRequest.Builder|`createReprocessCaptureRequest(TotalCaptureResult inputResult)` 通过 TotalCaptureResult 创建新的 CaptureRequest 的 CaptureRequest.Builder|\n|abstract void|`createReprocessableCaptureSession(InputConfiguration inputConfig, List<Surface> outputs, CameraCaptureSession.StateCallback callback, Handler handler)` 通过输入 Surface 配置和输出 Surface 创建相机拍摄会话|\t\n|abstract void|`createReprocessableCaptureSessionByConfigurations(InputConfiguration inputConfig, List<OutputConfiguration> outputs, CameraCaptureSession.StateCallback callback, Handler handler)` 通过输入 Surface 配置和输出 Surface 创建相机拍摄会话|\n|abstract String|`getId()` 获取相机设备的 Id|\n|boolean|`isSessionConfigurationSupported(SessionConfiguration sessionConfig)` 该相机设备是否支持指定的 SessionConfiguration|\n\n### 2.2 CameraCharacteristics \n\n存储了 `CameraDevice` 的属性信息，使用 `CameraManager` 的 `getCameraCharacteristics()` 方法传入了相机的 id 之后即可得到 CameraCharacteristics. 然后，我们可以使用 CameraCharacteristics 的方法获取相机的属性。这类的方法有：\n\n|返回类型|方法|\n|:-|:-|\n|`<T> T`|`get(Key<T> key)`|\n|`List<Key<?>>`|`getKeys()`|\n|`List<CaptureRequest.Key<?>>`|`getAvailableSessionKeys()`|\n|`List<CaptureRequest.Key<?>>`|`getAvailablePhysicalCameraRequestKeys()`|\n|`List<CaptureRequest.Key<?>>`|`getAvailableCaptureRequestKeys()`|\n|`List<CaptureResult.Key<?>>`|`getAvailableCaptureResultKeys()`|\n\n这里的属性的键 Key 以一组静态常量的形式分别定义在了 CameraCharacteristics, CaptureRequest 和 CaptureResult 中。其中的很多的常量，可以用来对相机的参数进行设置或者获取相机的参数。此外还有一个 TotalCaptureResult，它们都继承自 CameraMetadata，而 CaptureRequest 又是 TotalCaptureResult 的基类。\n\n可以参考附录来了解我们都有哪些属性可以使用。\n\n### 2.3 CameraCaptureSession\n\n#### 2.3.1 CameraCaptureSession\n\nCameraDevice 的拍摄请求用来使用相机拍摄图片或者在之前的会话中处理拍摄的图片。\n\nCameraCaptureSession 可以通过给 `CameraDevice#createCaptureSession()` 设置一系列的 Surface 实现。或者通过将 InputConfiguration 以及一系列的 Surface 赋值给 `CameraDevice#createReprocessableCaptureSession()` 方法来创建一个可以重复处理的拍摄会话。一旦会话被创建，会话将会一直处于激活的状态，直到该相机设备的新的会话被创建或者相机设备关闭。\n\n所有的拍摄会话都可以用来从相机拍摄图片，但是只有可以复用的拍摄会话能使用该相机之前的会话处理图片。\n\n创建会话是一个耗时的操作，可能需要花费几百毫秒的时间，因为需要配置相机设备的内部管道，并且需要申请内存缓存以发送图片到指定的目标。因此，该过程是通过异步来完成的，并且 `CameraDevice#createCaptureSession()` 和 `CameraDevice#createReprocessableCaptureSession()` 会把即将可用的 CameraCaptureSession 发送给监听的 `CameraCaptureSession.StateCallback#onConfigured()` 回调。如果配置过程发生了错误，那么 `CameraCaptureSession.StateCallback#onConfigureFailed()` 将会被触发，并且会话将会不可用（处于激活状态）。\n\n当新的会话被创建，之前的会话将会关闭，并且与之关联的 `StateCallback#onClosed()` 回调会被触发。一旦会话被关闭，那么所有的会话方法将会抛出 IllegalStateException 异常。\n\n关闭的会话会清理所有的重复的请求（跟 `stopRepeating()` 被调用的效果一样），但是在新创建的会话占用相机设备之前，会话会正常完成所有的正在处理的拍摄请求。\n\n|公共方法|说明|\n|:-|:-|\n|abstract void |`abortCaptures()` 丢弃所有正在处理和等待处理的请求|\n|abstract int|`capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)` 提交一个图片拍摄请求。该请求定义了拍摄图片的所有的参数。每个请求会产生一个 CaptureResult。这里的回调会在请求被处理的时候调用。handler 是回调被触发的线程。|\n|abstract int|`captureBurst(List<CaptureRequest> requests, CameraCaptureSession.CaptureCallback listener, Handler handler)` 提交一系列按顺序拍摄的请求|\n|int|`captureBurstRequests(List<CaptureRequest> requests, Executor executor, CameraCaptureSession.CaptureCallback listener)` 提交一系列按顺序拍摄的请求|\n|int|`captureSingleRequest(CaptureRequest request, Executor executor, CameraCaptureSession.CaptureCallback listener)` 提交一个图片拍摄请求|\n|abstract void|`close()` 异步地关闭该拍摄请求|\n|abstract void|`finalizeOutputConfigurations(List<OutputConfiguration> outputConfigs)` 回收输出配置|\n|abstract CameraDevice|`getDevice()` 获取创建该会话的相机设备|\n|abstract Surface|`getInputSurface()` 获取与可重新拍摄请求关联的输入的 Surface|\n|abstract boolean|`isReprocessable()` 该应用是否可以通过该拍摄会话提交请求|\n|abstract void|`prepare(Surface surface)` 为输出 Surface 预申请所有的缓存|\n|abstract int|`setRepeatingBurst(List<CaptureRequest> requests, CameraCaptureSession.CaptureCallback listener, Handler handler)` 通过该会话获取一个可以不断拍摄的请求|\n|int|`setRepeatingBurstRequests(List<CaptureRequest> requests, Executor executor, CameraCaptureSession.CaptureCallback listener)` 通过该会话获取一个可以不断拍摄的请求|\n|abstract int|`setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)` 通过该会话获取一个可以不断拍摄的请求|\n|int|`setSingleRepeatingRequest(CaptureRequest request, Executor executor, CameraCaptureSession.CaptureCallback listener)` 通过该会话获取一个可以不断拍摄的请求|\n|abstract void|`stopRepeating()` 取消所有正在进行的重复拍摄|\n|void|`updateOutputConfiguration(OutputConfiguration config)` 配置完成后更新 OutputConfiguration|\n\n#### 2.3.2 CameraCaptureSession.CaptureCallback\n\n用来获取提交给 CameraDevice 的 CaptureRequest 的处理过程的回调。\n\n当请求开始拍摄或者拍摄完成之后该回调将会被触发。当发生了错误的时候，\n\n|方法||\n|:-|:-|\n|void|`onCaptureBufferLost(CameraCaptureSession session, CaptureRequest request, Surface target, long frameNumber)` 当请求的一个 buffer 无法被发送到指定的 Suface 的时候该方法会被回调|\n|void|`onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result)` 当图片拍摄请求完成并且拍摄结果可用的时候该方法会被回调|\n|void|`onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure)` 当相机设备无法生成拍摄结果（发生了错误）的时候这个方法会被回调，而不是 `onCaptureCompleted(CameraCaptureSession, CaptureRequest, TotalCaptureResult)`|\n|void|`onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult)` 图片拍摄过程中被回调|\n|void|`onCaptureSequenceAborted(CameraCaptureSession session, int sequenceId)` 该方法的回调与其他方法独立，当一个拍摄序列被丢弃的时候触发|\n|void|`onCaptureSequenceCompleted(CameraCaptureSession session, int sequenceId, long frameNumber)` 该方法的回调与其他方法独立，当图片开始曝光或者相机设备开始处理输入的图片的时候被触发|\n|void|`onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber)` 相机设备开始拍摄的时候回调|\n\n### 2.3.3 CameraCaptureSession.StateCallback\n\n用来获取相机拍摄会话状态的回调。\n\n|公共方法||\n|:-|:-|\n|void|`onActive(CameraCaptureSession session)` 当会话开始处理拍摄请求的时候回调|\n|void|`onCaptureQueueEmpty(CameraCaptureSession session)` 相机设备的输入图片序列为空，并且准备接收下一个请求的时候被调用|\n|void|`onClosed(CameraCaptureSession session)` 会话关闭的时候被调用|\n|abstract void|`onConfigureFailed(CameraCaptureSession session)` 会话无法被配置或者请求的时候被调用|\n|abstract void|`onConfigured(CameraCaptureSession session)` 相机设备完成配置，会话可以开始处理拍摄请求时被回调|\n|void|`onReady(CameraCaptureSession session)` 每当会话没有拍摄请求需要处理的时候被调用|\n|void|`onSurfacePrepared(CameraCaptureSession session, Surface surface)` 用于输出 Surface 的预申请 buffer 完成的时候被调用|\n\n### 2.4 CaptureRequest\n\n#### 2.4.1 CaptureRequest\n\n包含了拍摄的配置的信息，比如硬件的信息、管道、算法和输出的 buffer 等，也包含了用来发送图片数据的目标 Suface.\n\n可以通过 `CameraDevice#createCaptureRequest()` 得到 CaptureRequests 构建者，然后使用构建者创建 CaptureRequests. \n\nCaptureRequests 可以用作 `CameraCaptureSession#capture()` 或者 `CameraCaptureSession#setRepeatingRequest()` 大参数来从相机中拍摄图片。\n\n每个请求可以指定不同的目标 Surfaces 子集，然后相机可以把拍摄到的数据发送到这些目标 Surface 上面。请求用到的所有的 Surface 必须包含再最好一次调用 `CameraDevice#createCaptureSession()` 时传入的 Surface 中。例如，预览的请求必须包含 SurfaceView 或 SurfaceTexture 的 Surface，但是高分辨率的静态拍摄必须包含从 ImageReader 中得到的 Surface，并且要使用高分辨率的 JPEG 配置。\n\n可再加工的拍摄请求允许之前配设的图片被发送给相机进一步处理。这种请求可以通过 `CameraDevice#createReprocessCaptureRequest()` 得到，然后配合可以再加工的拍摄会话一起使用，该会话可以通过 `CameraDevice#createReprocessableCaptureSession()` 得到。\n\n#### 2.4.2 CaptureRequest.Builder\n\n|公共方法||\n|:-|:-|\n|void|`addTarget(Surface outputTarget)` 添加一个 Surface 到该请求的 Surface 列表中。添加的 Surface 必须是添加到 `CameraDevice#createCaptureSession()` 中的 Surface|\n|CaptureRequest|`build()` 使用目标的 Surface 和设置构建一个请求|\n|<T> T|`get(Key<T> key)` 获取请求字段的值|\n|<T> T|`getPhysicalCameraKey(Key<T> key, String physicalCameraId)` 指定物理相机 id 的请求字段的值|\n|void|`removeTarget(Surface outputTarget)`|\n|<T> void|`set(Key<T> key, T value)` 设置请求的字段的值|\n|<T> CaptureRequest.Builder|`setPhysicalCameraKey(Key<T> key, T value, String physicalCameraId)` 设置请求的字段的值|\n|void|`setTag(Object tag)` 为请求添加一个 tag|\n\n这里的 Key 是 `CaptureRequest.Key` 类型的，它们是一些定义在 CaptureRequest 中的常量，可以参考附录了解。\n\n### 2.5 CaptureFailure\n\n单个图片拍摄请求失败的信息的封装类，可以使用它获取失败的信息。不论是全部失败还是部分失败，失败的信息都会通过这个类封装。你可以使用 `getReason()` 获取具体的失败的原因。\n\n收到 CaptureFailure 意味着与 frame 关联的 metadata 丢失了，也就是无法通过这个 CaptureResult 得到拍摄结果。\n\n|公共方法||\n|:-|:-|\n|long|`getFrameNumber()` 获取与当前失败的请求关联的 frame 的号码|\n|int|`getReason()` 用来得到失败的原因，不论是内部错误还是用户操作的问题|\n|CaptureRequest|`getRequest()` 获取与当前失败的关联的请求|\n|int|`getSequenceId()` 获取当前失败的请求的序列 id|\n|boolean|`wasImageCaptured()` 获取是否通过相机得到了图片|\n\n### 2.6 CaptureResult\n\n图片拍摄结果的子集，包含了一些最终的配置的子集，比如硬件信息、算法等，也包含了设备的 metadata 等信息。\n\n|公共方法||\n|:-|:-|\n|<T> T|`get(Key<T> key)` 获取拍摄结果的值|\n|long|`getFrameNumber()` 获取与该结果关联的 frame 号|\n|List<Key<?>>|`getKeys()` 返回可用 key|\n|CaptureRequest|`getRequest()` 获取与该结果对应的请求|\n|int|`getSequenceId()` 获取序列号|\n\n这里的 Key 指的是 CaptureResult.Key 类型的，它们被以一组常量的形式定义在 CaptureResult 中。参考附录了解更多。\n\n### 2.7 DngCreator\n\n该类提供了写 DNG 文件原始的像素数据的函数。该类设计用来配合 ImageFormat.RAW_SENSOR 缓存，或者 Bayer 类型的像素数据。DNG metadata tags 是通过 CaptureResult 对象得到或者直接设置的。\n\nDNG 文件格式是跨平台文件格式，用来存储像素数据。它允许像素数据定义在用户自定义的颜色空间。更多关于 DNG 文件格式的内容可以参考：https://wwwimages2.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf。\n\n### 2.8 TotalCaptureResult\n\n单词图片拍摄的完整结果，包含了一些最终的配置的子集，比如硬件信息、算法等，也包含了设备的 metadata 等信息。\n\nTotalCaptureResult 由 CameraDevice 处理 CaptureRequest 之后得到。拍摄请求的所有属性信息都可以从它当中获取到。\n\n对于逻辑多相机设备，如果 CaptureRequest 包含了物理相机的 Surface，那么对应的 TotalCaptureResult 对象将包含物理相机的 metadata。物理相机的 id 到结果的 metadata 之间的映射可以通过 getPhysicalCameraResults() 得到。如果请求的 Surface 是针对逻辑相机的，那么将不会包含物理相机的 metadata. \n\n|公共方法||\n|:-|:-|\n|List<CaptureResult>|`getPartialResults()` 获取组成完整结果的 CaptureResult|\n|Map<String, CaptureResult>|`getPhysicalCameraResults()` 物理相机到各自的 CaptureResult 之间的映射。多逻辑摄像机设备可以调用此函数，这些设备是具有 REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA 功能的逻辑多摄像机功能。调用 `CameraCharacteristics#getPhysicalCameraIds()` 返回支持逻辑摄像机的非空物理设备集|\n\n## 3、一些相关的类\n\n### 3.1 CamcorderProfile\n\n检索摄像机应用程序的预定义摄像机配置文件设置。这些设置是只读的。分成两种类型：针对音频的和视频的。每个配置文件指定以下参数集：\n\n1. 文件输出格式\n2. 视频编解码器格式\n3. 视频比特率，以每秒位数为单位\n4. 视频帧速率，以每秒帧数为单位\n5. 视频帧宽和高度，\n6. 音频编解码器格式\n7. 音频比特率，以每秒位数为单位，\n8. 音频采样率\n9. 录制的音频通道数。\n\n更多内容可以参考附录部分。\n\n### 3.2 ImageReader\n\n用来直接访问渲染到 Surface 上面的图片数据。几款 Android 媒体 API 将 Surface 作为渲染的目标，包括 MediaPlayer, MediaCodec, CameraDevice, ImageWriter and RenderScript Allocations。每种源所使用需要的尺寸和格式可能不同，需要参考相应文档。\n\n图片数据被存储在 Image 中，并且多个 Image 可以同时访问。能够同时访问的 Image 的最大值通过构造函数的 maxImages 参数指定。通过 Surface 发送给 ImageReader 的数据将被放进队列中，直到 `acquireLatestImage()` 或者 `acquireNextImage()` 方法被调用。由于内存限制，如果 ImageReader 获取和释放 Images 的速率以与生产速率不相当的化，那么图像源最终会在尝试停止或丢弃图像。\n\n|公共方法||\n|:-|:-|\nImage|`acquireLatestImage()` Image 从 ImageReader 的队列中 获取最新信息，删除旧版本 Image。\nImage|`acquireNextImage()` 从 ImageReader 的队列中获取下一个Image。\nvoid|`close()` 释放与此 ImageReader 关联的所有资源。\nvoid|`discardFreeBuffers()` 丢弃此 ImageReader 拥有的所有空闲缓冲区。\nint|`getHeight()` 默认高度Image，以像素为单位。\nint|`getImageFormat()` 默认 ImageFormat 值为Image。\nint|`getMaxImages()` 可以随时从 ImageReader 获取的最大图像数（例如，with acquireNextImage()）。\nSurface|`getSurface()` 得到一个 Surface 可用于生产 Image 的东西 ImageReader。\nint|`getWidth()` 默认宽度Image，以像素为单位。\nstatic ImageReader|`newInstance(int width, int height, int format, int maxImages, long usage)` 为所需大小，格式和消费者使用标志的图像创建新的阅读器。这里的 maxImages 表示能够从该 ImageReader 中同时获取的图片数量的最大值。\nstatic ImageReader|`newInstance(int width, int height, int format, int maxImages)` 为所需大小和格式的图像创建新的 ImageReader。\nvoid|`setOnImageAvailableListener(ImageReader.OnImageAvailableListener listener, Handler handler)` 注册监听，监听会 ImageReader 中新的图片可用的时候被触发。可以在回调被触发的时候从 ImageReader 的 `acquireNextImage()` 方法中获取图片对象，并将其存储到磁盘上。\n\n## 附录：\n\n### 1. CameraCharacteristics.Key\n\n总结的规则：\n\n1. 中间包含 AE 的表示自动曝光；\n2. 中间包含 AF 的表示自动对焦；\n3. 中间包含 AWB 的表示自动白平衡。\n\n|字段|说明|\n|:-|:-|\npublic static final Key<int[]>|`COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES`|\npublic static final Key<int[]>|`CONTROL_AE_AVAILABLE_ANTIBANDING_MODES` 获取相机支持的自动曝光反冲带模式|\npublic static final Key<int[]>|`CONTROL_AE_AVAILABLE_MODES` 获取相机支持的自动曝光模式|\npublic static final Key<Range<Integer>>|`CONTROL_AE_COMPENSATION_RANGE` 自动曝光补偿的取值范围|\npublic static final Key<Boolean>|`CONTROL_AE_LOCK_AVAILABLE` 该相机是否支持 CaptureRequest#CONTROL_AE_LOCK|\npublic static final Key<int[]>|`CONTROL_AF_AVAILABLE_MODES` 返回相机所支持的自动对焦模式|\npublic static final Key<int[]>|`CONTROL_AVAILABLE_EFFECTS` 获取相机支持的色彩效果|\npublic static final Key<int[]>|`CONTROL_AVAILABLE_MODES` 获取相机支持的控制模式`|\npublic static final Key<int[]>|`CONTROL_AVAILABLE_SCENE_MODES` 获取相机支持的场景模式|\npublic static final Key<int[]>|`CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES` 获取相机支持的视讯稳定模式|\npublic static final Key<int[]>|`CONTROL_AWB_AVAILABLE_MODES` 获取相机支持的自动白平衡模式|\npublic static final Key<Boolean>|`CONTROL_AWB_LOCK_AVAILABLE` 相机是否支持白平衡锁|\npublic static final Key<Integer>|`CONTROL_MAX_REGIONS_AE`|\npublic static final Key<Integer>|`CONTROL_MAX_REGIONS_AF`|\npublic static final Key<Integer>|`CONTROL_MAX_REGIONS_AWB`|\npublic static final Key<Range<Integer>>|`CONTROL_POST_RAW_SENSITIVITY_BOOST_RANGE`|\npublic static final Key<Boolean>|`DEPTH_DEPTH_IS_EXCLUSIVE`|\npublic static final Key<int[]>|`DISTORTION_CORRECTION_AVAILABLE_MODES`|\npublic static final Key<int[]>|`EDGE_AVAILABLE_EDGE_MODES` 获取支持的边缘增强模式|\npublic static final Key<Boolean>|`FLASH_INFO_AVAILABLE` 相机是否有闪光单元|\npublic static final Key<int[]>|`HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES`|\npublic static final Key<Integer>|`INFO_SUPPORTED_HARDWARE_LEVEL`|\npublic static final Key<String>|`INFO_VERSION` 版本信息字符串`|\npublic static final Key<Size[]>|`JPEG_AVAILABLE_THUMBNAIL_SIZES` JPEG 支持的尺寸|\npublic static final Key<float[]>|`LENS_DISTORTION` 相机的矫正系数|\npublic static final Key<Integer>|`LENS_FACING` 用来获取相机的方向，比如前置相机和后置相机|\npublic static final Key<float[]>|`LENS_INFO_AVAILABLE_APERTURES`|\npublic static final Key<float[]>|`LENS_INFO_AVAILABLE_FILTER_DENSITIES`|\npublic static final Key<float[]>|`LENS_INFO_AVAILABLE_FOCAL_LENGTHS`|\npublic static final Key<int[]>|`LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION`|\npublic static final Key<Integer>|`LENS_INFO_FOCUS_DISTANCE_CALIBRATION`|\npublic static final Key<Float>|`LENS_INFO_HYPERFOCAL_DISTANCE`|\npublic static final Key<Float>|`LENS_INFO_MINIMUM_FOCUS_DISTANCE`|\npublic static final Key<float[]>|`LENS_INTRINSIC_CALIBRATION`|\npublic static final Key<Integer>|`LENS_POSE_REFERENCE`|\npublic static final Key<float[]>|`LENS_POSE_ROTATION` 相机相对于传感器坐标系的方向|\npublic static final Key<float[]>|`LENS_POSE_TRANSLATION`|\npublic static final Key<float[]>|`LENS_RADIAL_DISTORTION`|\npublic static final Key<Integer>|`LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE`|\npublic static final Key<int[]>|`NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES`|\npublic static final Key<Integer>|`REPROCESS_MAX_CAPTURE_STALL`|\npublic static final Key<int[]>|`REQUEST_AVAILABLE_CAPABILITIES`|\npublic static final Key<Integer>|`REQUEST_MAX_NUM_INPUT_STREAMS`|\npublic static final Key<Integer>|`REQUEST_MAX_NUM_OUTPUT_PROC`|\npublic static final Key<Integer>|`REQUEST_MAX_NUM_OUTPUT_PROC_STALLING`|\npublic static final Key<Integer>|`REQUEST_MAX_NUM_OUTPUT_RAW`|\npublic static final Key<Integer>|`REQUEST_PARTIAL_RESULT_COUNT`|\npublic static final Key<Byte>|`REQUEST_PIPELINE_MAX_DEPTH`|\npublic static final Key<Float>|`SCALER_AVAILABLE_MAX_DIGITAL_ZOOM`|\npublic static final Key<Integer>|`SCALER_CROPPING_TYPE`|\npublic static final Key<MandatoryStreamCombination[]>|`SCALER_MANDATORY_STREAM_COMBINATIONS`|\npublic static final Key<StreamConfigurationMap>|`SCALER_STREAM_CONFIGURATION_MAP`用来获取 StreamConfigurationMap，然后可以从 StreamConfigurationMap 中获取输出流配置信息。可以使用 StreamConfigurationMap 的方法 `getOutputSizes()` 获取指定类型的图片或者类支持的尺寸、格式等信息。|\npublic static final Key<int[]>|`SENSOR_AVAILABLE_TEST_PATTERN_MODES`|\npublic static final Key<BlackLevelPattern>|`SENSOR_BLACK_LEVEL_PATTERN`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_CALIBRATION_TRANSFORM1`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_CALIBRATION_TRANSFORM2`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_COLOR_TRANSFORM1`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_COLOR_TRANSFORM2`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_FORWARD_MATRIX1`|\npublic static final Key<ColorSpaceTransform>|`SENSOR_FORWARD_MATRIX2`|\npublic static final Key<Rect>|`SENSOR_INFO_ACTIVE_ARRAY_SIZE`|\npublic static final Key<Integer>|`SENSOR_INFO_COLOR_FILTER_ARRANGEMENT`|\npublic static final Key<Range<Long>>|`SENSOR_INFO_EXPOSURE_TIME_RANGE`|\npublic static final Key<Boolean>|`SENSOR_INFO_LENS_SHADING_APPLIED`|\npublic static final Key<Long>|`SENSOR_INFO_MAX_FRAME_DURATION`|\npublic static final Key<SizeF>|`SENSOR_INFO_PHYSICAL_SIZE`|\npublic static final Key<Size>|`SENSOR_INFO_PIXEL_ARRAY_SIZE`|\npublic static final Key<Rect>|`SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE`|\npublic static final Key<Range<Integer>>|`SENSOR_INFO_SENSITIVITY_RANGE`|\npublic static final Key<Integer>|`SENSOR_INFO_TIMESTAMP_SOURCE`|\npublic static final Key<Integer>|`SENSOR_INFO_WHITE_LEVEL`|\npublic static final Key<Integer>|`SENSOR_MAX_ANALOG_SENSITIVITY`|\npublic static final Key<Rect[]>|`SENSOR_OPTICAL_BLACK_REGIONS`|\npublic static final Key<Integer>|`SENSOR_ORIENTATION` 输出的图像要旋转的角度|\npublic static final Key<Integer>|`SENSOR_REFERENCE_ILLUMINANT1`|\npublic static final Key<Byte>|`SENSOR_REFERENCE_ILLUMINANT2`|\npublic static final Key<int[]>|`SHADING_AVAILABLE_MODES`|\npublic static final Key<int[]>|`STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES`|\npublic static final Key<boolean[]>|`STATISTICS_INFO_AVAILABLE_HOT_PIXEL_MAP_MODES`|\npublic static final Key<int[]>|`STATISTICS_INFO_AVAILABLE_LENS_SHADING_MAP_MODES`|\npublic static final Key<int[]>|`STATISTICS_INFO_AVAILABLE_OIS_DATA_MODES`|\npublic static final Key<Integer>|`STATISTICS_INFO_MAX_FACE_COUNT`|\npublic static final Key<Integer>|`SYNC_MAX_LATENCY`|\npublic static final Key<int[]>|`TONEMAP_AVAILABLE_TONE_MAP_MODES`|\npublic static final Key<Integer>|`TONEMAP_MAX_CURVE_POINTS`|\n\n### 2. CaptureResult.Key\n\n### 3. CaptureRequest.Key\n\n### 4. TotalCaptureResult.Key\n\n### 5. CamcorderProfile\n\n|常量||\n|:-|:-|\n|int|`QUALITY_1080P` 质量等级对应1080p（1920 x 1080）分辨率。\nint|`QUALITY_2160P` 质量等级对应2160p（3840 x 2160）分辨率。\nint|`QUALITY_480P` 质量等级对应480p（720 x 480）分辨率。\nint|`QUALITY_720P` 质量等级对应720p（1280 x 720）分辨率。\nint|`QUALITY_CIF` 质量等级对应于cif（352 x 288）分辨率。\nint|`QUALITY_HIGH` 质量等级对应于最高可用分辨率。\nint|`QUALITY_HIGH_SPEED_1080P` 速（>=100fps）质量等级，对应1080p（1920 x 1080或1920x1088）分辨率。\nint|`QUALITY_HIGH_SPEED_2160P` 高速（>=100fps）质量等级，对应2160p（3840 x 2160）分辨率。\nint|`QUALITY_HIGH_SPEED_480P` 高速（>=100fps）质量等级，对应480p（720 x 480）分辨率。\nint|`QUALITY_HIGH_SPEED_720P` 高速（>=100fps）质量等级，对应720p（1280 x 720）分辨率。\nint|`QUALITY_HIGH_SPEED_HIGH` 高速（>=100fps）质量等级，对应于最高可用分辨率。\nint|`QUALITY_HIGH_SPEED_LOW` 高速（>=100fps）质量等级，对应于最低可用分辨率。\nint|`QUALITY_LOW` 质量等级对应于最低可用分辨率。\nint|`QUALITY_QCIF` 质量等级对应qcif（176 x 144）分辨率。\nint|`QUALITY_QVGA` 质量等级对应QVGA（320x240）分辨率。\nint|`QUALITY_TIME_LAPSE_1080P` 时间流逝质量等级对应于1080p（1920 x 1088）分辨率。\nint|`QUALITY_TIME_LAPSE_2160P` 时间流逝质量等级对应2160p（3840 x 2160）分辨率。\nint|`QUALITY_TIME_LAPSE_480P` 时间流逝质量等级对应480p（720 x 480）分辨率。\nint|`QUALITY_TIME_LAPSE_720P` 时间流逝质量等级对应720p（1280 x 720）分辨率。\nint|`QUALITY_TIME_LAPSE_CIF` 时间推移质量等级对应于cif（352 x 288）分辨率。\nint|`QUALITY_TIME_LAPSE_HIGH` 与最高可用分辨率对应的时间推移质量等级。\nint|`QUALITY_TIME_LAPSE_LOW` 与最低可用分辨率对应的时间推移质量等级。\nint|`QUALITY_TIME_LAPSE_QCIF` 时间推移质量等级对应于qcif（176 x 144）分辨率。\nint|`QUALITY_TIME_LAPSE_QVGA` 时间推移质量等级对应于QVGA（320 x 240）分辨率。\n\n|字段||\n|:-|:-|\npublic int|`audioBitRate` 目标音频输出比特率，以每秒位数为单位\npublic int|`audioChannels` 用于音轨的音频通道数\npublic int|`audioCodec` 音频编码器用于音轨。\npublic int|`audioSampleRate` 用于音轨的音频采样率\npublic int|`duration` 会话终止前的默认录制持续时间（秒）。\npublic int|`fileFormat` 摄像机配置文件的文件输出格式\npublic int|`quality` 摄像机配置文件的质量等级\npublic int|`videoBitRate` 目标视频输出比特率，以每秒位数为单位。如果应用程序在MediaRecorder#setProfile不指定任何其他MediaRecorder编码参数的情况下`配置视频录制，则这是目标录制的视频输出比特率。\npublic int|`videoCodec` 视频编码器用于视频轨道\npublic int|`videoFrameHeight` 目标视频帧高度（以像素为单位）\npublic int|`videoFrameRate` 目标视频帧速率，以每秒帧数为单位。\npublic int|`videoFrameWidth` 目标视频帧宽度（以像素为单位）\n\n公共方法||\n|:-|:-|\nstatic CamcorderProfile|`get(int quality)` 以给定的质量级别返回设备上第一台后置摄像头的摄像机配置文件。\nstatic CamcorderProfile|`get(int cameraId, int quality)` 以给定的质量级别返回给定摄像机的摄像机配置文件。\nstatic boolean|`hasProfile(int cameraId, int quality)` 如果给定质量级别的给定摄像机存在摄像机配置文件，则返回true。\nstatic boolean|`hasProfile(int quality)` 如果给定质量级别的第一台后置摄像机存在摄像机配置文件，则返回true。\n\n\n"
  },
  {
    "path": "性能优化/Android进程保活.md",
    "content": "\n[ Android ̱Ҫ֪һ](https://mp.weixin.qq.com/s/FudvsrZEEVnAbtMokpjP0Q)\n\n\n\n"
  },
  {
    "path": "消息机制/EventBus的源码分析.md",
    "content": "# Android EventBus 的源码解析\n\n## 1、EventBus 的使用\n\n### 1.1 EventBus 简介\n\nEventBus 是一款用于 Android 的事件发布-订阅总线，由 GreenRobot 开发，Gihub 地址是：[EventBus](https://github.com/greenrobot/EventBus)。它简化了应用程序内各个组件之间进行通信的复杂度，尤其是碎片之间进行通信的问题，可以避免由于使用广播通信而带来的诸多不便。\n\n首先是 EventBus 的三个重要角色\n\n1. **Event**：事件，它可以是任意类型，EventBus 会根据事件类型进行全局的通知。\n2. **Subscriber**：事件订阅者，在 EventBus 3.0 之前我们必须定义以onEvent开头的那几个方法，分别是 `onEvent()`、`onEventMainThread()`、`onEventBackgroundThread()` 和 `onEventAsync()`，而在3.0之后事件处理的方法名可以随意取，不过需要加上注解`@subscribe`，并且指定线程模型，默认是`POSTING`。\n3. **Publisher**：事件的发布者，可以在任意线程里发布事件。一般情况下，使用 `EventBus.getDefault()` 就可以得到一个EventBus对象，然后再调用 `post(Object)` 方法发布事件即可。\n\n其次是 EventBus 的四种线程模型（EventBus3.0），分别是：\n\n1. **POSTING**：默认，表示事件处理函数的线程跟发布事件的线程在同一个线程。\n2. **MAIN**：表示事件处理函数的线程在主线程(UI)线程，因此在这里不能进行耗时操作。\n3. **BACKGROUND**：表示事件处理函数的线程在后台线程，因此不能进行UI操作。如果发布事件的线程是主线程(UI线程)，那么事件处理函数将会开启一个后台线程，如果果发布事件的线程是在后台线程，那么事件处理函数就使用该线程。\n4. **ASYNC**：表示无论事件发布的线程是哪一个，事件处理函数始终会新建一个子线程运行，同样不能进行UI操作。\n\n### 1.2 使用 EventBus\n\n在使用之前先要引入如下依赖：\n\n    implementation 'org.greenrobot:eventbus:3.1.1'\n\n然后，我们定义一个事件的封装对象。在程序内部就使用该对象作为通信的信息：\n\n    public class MessageWrap {\n\n        public final String message;\n\n        public static MessageWrap getInstance(String message) {\n            return new MessageWrap(message);\n        }\n\n        private MessageWrap(String message) {\n            this.message = message;\n        }\n    }\n\n然后，我们定义一个 Activity 要拿过来测试事件发布的效果：\n\n    @Route(path = BaseConstants.LIBRARY_EVENT_BUS_ACTIVITY1)\n    public class EventBusActivity1 extends CommonActivity<ActivityEventBus1Binding> {\n\n        @Override\n        protected void doCreateView(Bundle savedInstanceState) {\n            // 为按钮添加添加单击事件\n            getBinding().btnReg.setOnClickListener(v -> EventBus.getDefault().register(this));\n            getBinding().btnNav2.setOnClickListener( v ->\n                    ARouter.getInstance()\n                            .build(BaseConstants.LIBRARY_EVENT_BUS_ACTIVITY2)\n                            .navigation());\n        }\n\n        @Override\n        protected void onDestroy() {\n            super.onDestroy();\n            EventBus.getDefault().unregister(this);\n        }\n\n        @Subscribe(threadMode = ThreadMode.MAIN)\n        public void onGetMessage(MessageWrap message) {\n            getBinding().tvMessage.setText(message.message);\n        }\n    }\n\n这里我们当按下按钮的时候向 EventBus 注册监听，然后按下另一个按钮的时候跳转到拎一个 Activity，并在另一个 Activity 发布我们输入的事件。在上面的 Activity 中，我们会添加一个监听的方法，即 `onGetMessage()`，这里我们需要为其加入注解 `@Subscribe` 并指定线程模型为主线程 `MAIN`。最后，就是在 Activity 的 `onDestroy()` 方法中取消注册该 Activity。\n\n下面是另一个 Activity 的定义，在这个 Activity 中，我们当按下按钮的时候从 EditText 中取出内容并进行发布，然后我们退出到之前的 Activity，以测试是否正确监听到发布的内容:\n\n    @Route(path = BaseConstants.LIBRARY_EVENT_BUS_ACTIVITY2)\n    public class EventBusActivity2 extends CommonActivity<ActivityEventBus2Binding> {\n\n        @Override\n        protected void doCreateView(Bundle savedInstanceState) {\n            getBinding().btnPublish.setOnClickListener(v -> publishContent());\n        }\n\n        private void publishContent() {\n            String msg = getBinding().etMessage.getText().toString();\n            EventBus.getDefault().post(MessageWrap.getInstance(msg));\n            ToastUtils.makeToast(\"Published : \" + msg);\n        }\n    }\n\n根据测试的结果，我们的确成功地接收到了发送的信息。\n\n### 1.3 黏性事件\n\n所谓的黏性事件，就是指发送了该事件之后再订阅者依然能够接收到的事件。使用黏性事件的时候有两个地方需要做些修改。一个是订阅事件的地方，这里我们在先打开的 Activity 中注册监听黏性事件：\n\n    @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)\n    public void onGetStickyEvent(MessageWrap message) {\n        String txt = \"Sticky event: \" + message.message;\n        getBinding().tvStickyMessage.setText(txt);\n    }\n\n另一个是发布事件的地方，这里我们在新的开的 Activity 中发布黏性事件。即调用 EventBus 的 `postSticky()` 方法来发布事件：\n\n    private void publishStickyontent() {\n        String msg = getBinding().etMessage.getText().toString();\n        EventBus.getDefault().postSticky(MessageWrap.getInstance(msg));\n        ToastUtils.makeToast(\"Published : \" + msg);\n    }\n\n按照上面的模式，我们先在第一个 Activity 中打开第二个 Activity，然后在第二个 Activity 中发布黏性事件，并回到第一个 Activity 注册 EventBus。根据测试结果，当按下注册按钮的时候，会立即触发上面的订阅方法从而获取到了黏性事件。\n\n### 1.4 优先级\n \n在 `@Subscribe` 注解中总共有3个参数，上面我们用到了其中的两个，这里我们使用以下第三个参数，即 `priority`。它用来指定订阅方法的优先级，是一个整数类型的值，默认是 0，值越大表示优先级越大。在某个事件被发布出来的时候，优先级较高的订阅方法会首先接受到事件。\n\n为了对优先级进行测试，这里我们需要对上面的代码进行一些修改。这里，我们使用一个布尔类型的变量来判断是否应该取消事件的分发。我们在一个较高优先级的方法中通过该布尔值进行判断，如果未 `true` 就停止该事件的继续分发，从而通过低优先级的订阅方法无法获取到事件来证明优先级较高的订阅方法率先获取到了事件。\n\n这里有几个地方需要**注意**：\n\n1. 只有当两个订阅方法使用相同的`ThreadMode`参数的时候，它们的优先级才会与`priority`指定的值一致；\n2. 只有当某个订阅方法的`ThreadMode`参数为`POSTING`的时候，它才能停止该事件的继续分发。\n\n所以，根据以上的内容，我们需要对代码做如下的调整：\n\n    // 用来判断是否需要停止事件的继续分发\n    private boolean stopDelivery = false;\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        // ...\n\n        getBinding().btnStop.setOnClickListener(v -> stopDelivery = true);\n    }\n\n    @Subscribe(threadMode = ThreadMode.POSTING, priority = 0)\n    public void onGetMessage(MessageWrap message) {\n        getBinding().tvMessage.setText(message.message);\n    }\n\n    // 订阅方法，需要与上面的方法的threadMode一致，并且优先级略高\n    @Subscribe(threadMode = ThreadMode.POSTING, sticky = true, priority = 1)\n    public void onGetStickyEvent(MessageWrap message) {\n        String txt = \"Sticky event: \" + message.message;\n        getBinding().tvStickyMessage.setText(txt);\n        if (stopDelivery) {\n            // 终止事件的继续分发\n            EventBus.getDefault().cancelEventDelivery(message);\n        }\n    }\n\n即我们在之前的代码之上增加了一个按钮，用来将 `stopDelivery` 的值置为 `true`。该字段随后将会被用来判断是否要终止事件的继续分发，因为我们需要在代码中停止事件的继续分发，所以，我们需要将上面的两个订阅方法的 `threadMode` 的值都置为`ThreadMode.POSTING`。\n\n按照，上面的测试方式，首先我们在当前的 Activity 注册监听，然后跳转到另一个 Activity，发布事件并返回。第一次的时候，这里的两个订阅方法都会被触发。然后，我们按下停止分发的按钮，并再次执行上面的逻辑，此时只有优先级较高的方法获取到了事件并将该事件终止。\n\n上面的内容是 EventBus 的基本使用方法，相关的源码参考：[Github](https://github.com/Shouheng88/Android-references/tree/master/libraries/src/main/java/me/shouheng/libraries/eventbus)。\n\n## 2、源码分析\n\n在分析 EventBus 源码的时候，我们先从获取一个 EventBus 实例的方法入手，然后再分别看一下它的注册、取消注册、发布事件以及触发观察方法的代码是如何实现的。在下面的文章中我们将会回答以下几个问题：\n\n1. 在 EventBus 中，使用 `@Subscribe` 注解的时候指定的 `ThreadMode` 是如何实现在不同线程间传递数据的？\n2. 使用注解和反射的时候的效率问题，是否会像 Guava 的 EventBus 一样有缓存优化？\n3. 黏性事件是否是通过内部维护了之前发布的数据来实现的，是否使用了缓存？\n\n### 2.1 获取实例的过程\n\n在创建 EventBus 实例的时候，一种方式是按照我们上面的形式，通过 EventBus 的静态方法 `getDefault()` 来获取一个实例。`getDefault()` 本身会调用其内部的构造方法，通过传入一个默认 的`EventBusBuilder` 来创建 EventBus。此外，我们还可以直接通过 EventBus 的 `builder()` 方法获取一个 `EventBusBuilder` 的实例，然后通过该构建者模式来个性化地定制自己的 EventBus。即：\n\n    // 静态的单例实例\n    static volatile EventBus defaultInstance;\n\n    // 默认的构建者\n    private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();\n\n    // 实际上使用了DCL双检锁机制，这里简化了一下\n    public static EventBus getDefault() {\n        if (defaultInstance == null) defaultInstance = new EventBus();\n        return defaultInstance;\n    }\n\n    public EventBus() {\n        this(DEFAULT_BUILDER);\n    }\n\n    // 调用getDefault的时候，最终会调用该方法，使用DEFAULT_BUILDER创建一个实例\n    EventBus(EventBusBuilder builder) {\n        // ...\n    }\n\n    // 也可以使用下面的方法获取一个构建者，然后使用它来个性化定制EventBus\n    public static EventBusBuilder builder() {\n        return new EventBusBuilder();\n    }\n\n### 2.2 注册\n\n当调用 EventBus 实例的 `register()` 方法的时候，会执行下面的逻辑：\n\n    public void register(Object subscriber) {\n        // 首席会获取注册的对象的类型\n        Class<?> subscriberClass = subscriber.getClass();\n        // 然后获取注册的对象的订阅方法\n        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);\n        // 对当前实例加锁，并不断执行监听的逻辑\n        synchronized (this) {\n            for (SubscriberMethod subscriberMethod : subscriberMethods) {\n                // 对订阅方法进行注册\n                subscribe(subscriber, subscriberMethod);\n            }\n        }\n    }\n\n这里的 `SubscriberMethod` 封装了订阅方法（使用 `@Subscribe` 注解的方法）类型的信息，它的定义如下所示。从下面可以的代码中我们可以看出，实际上该类就是通过几个字段来存储 `@Subscribe` 注解中指定的类型信息，以及一个方法的类型变量。\n\n    public class SubscriberMethod {\n        final Method method;\n        final ThreadMode threadMode;\n        final Class<?> eventType;\n        final int priority;\n        final boolean sticky;\n\n        // ...\n    }\n\n\n`register()` 方法通过 `subscriberMethodFinder` 实例的 `findSubscriberMethods()` 方法来获取该观察者类型中的所有订阅方法，然后将所有的订阅方法分别进行订阅。下面我们先看下查找订阅者的方法。\n\n#### 查找订阅者的订阅方法\n\n下面是 `SubscriberMethodFinder` 中的 `findSubscriberMethods()` 方法：\n\n    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {\n        // 这里首先从缓存当中尝试去取该订阅者的订阅方法\n        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);\n        if (subscriberMethods != null) {\n            return subscriberMethods;\n        }\n\n        // 当缓存中没有找到该观察者的订阅方法的时候使用下面的两种方法获取方法信息\n        if (ignoreGeneratedIndex) {\n            subscriberMethods = findUsingReflection(subscriberClass);\n        } else {\n            subscriberMethods = findUsingInfo(subscriberClass);\n        }\n        if (subscriberMethods.isEmpty()) {\n            throw new EventBusException(...);\n        } else {\n            // 将获取到的订阅方法放置到缓存当中\n            METHOD_CACHE.put(subscriberClass, subscriberMethods);\n            return subscriberMethods;\n        }\n    }\n\n这里我们先从缓存当中尝试获取某个观察者中的所有订阅方法，如果没有可用缓存的话就从该类中查找订阅方法，并在返回结果之前将这些方法信息放置到缓存当中。这里的 `ignoreGeneratedIndex` 参数表示是否忽略注解器生成的 `MyEventBusIndex`，该值默认为 `false`。然后，我们会进入到下面的方法中获取订阅方法信息：\n\n    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {\n        // 这里通过FindState对象来存储找到的方法信息\n        FindState findState = prepareFindState();\n        findState.initForSubscriber(subscriberClass);\n        // 这里是一个循环操作，会从当前类开始遍历该类的所有父类\n        while (findState.clazz != null) {\n            // 获取订阅者信息\n            findState.subscriberInfo = getSubscriberInfo(findState); // 1\n            if (findState.subscriberInfo != null) {\n                // 如果使用了MyEventBusIndex，将会进入到这里并获取订阅方法信息\n                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();\n                for (SubscriberMethod subscriberMethod : array) {\n                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {\n                        findState.subscriberMethods.add(subscriberMethod);\n                    }\n                }\n            } else {\n                // 未使用MyEventBusIndex将会进入这里使用反射获取方法信息\n                findUsingReflectionInSingleClass(findState); // 2\n            }\n            // 将findState.clazz设置为当前的findState.clazz的父类\n            findState.moveToSuperclass();\n        }\n        return getMethodsAndRelease(findState);\n    }\n\n在上面的代码中，会从当前订阅者类开始直到它最顶层的父类进行遍历来获取订阅方法信息。这里在循环的内部会根据我们是否使用了 `MyEventBusIndex` 走两条路线，对于我们没有使用它的，会直接使用反射来获取订阅方法信息，即进入2处。\n\n下面是使用反射从订阅者中得到订阅方法的代码：\n\n    private void findUsingReflectionInSingleClass(FindState findState) {\n        Method[] methods;\n        try {\n            // 获取该类中声明的所有方法\n            methods = findState.clazz.getDeclaredMethods();\n        } catch (Throwable th) {\n            methods = findState.clazz.getMethods();\n            findState.skipSuperClasses = true;\n        }\n        // 对方法进行遍历判断\n        for (Method method : methods) {\n            int modifiers = method.getModifiers();\n            // 这里会对方法的修饰符进行校验\n            if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {\n                // 这里对方法的输入参数进行校验\n                Class<?>[] parameterTypes = method.getParameterTypes();\n                if (parameterTypes.length == 1) {\n                    // 获取方法的注解，用来从注解中获取注解的声明信息\n                    Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);\n                    if (subscribeAnnotation != null) {\n                        // 获取该方法的第一个参数\n                        Class<?> eventType = parameterTypes[0];\n                        if (findState.checkAdd(method, eventType)) {\n                            ThreadMode threadMode = subscribeAnnotation.threadMode();\n                            // 最终将封装之后的方法塞入到列表中\n                            findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,\n                                    subscribeAnnotation.priority(), subscribeAnnotation.sticky()));\n                        }\n                    }\n                } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {\n                    String methodName = method.getDeclaringClass().getName() + \".\" + method.getName();\n                    throw new EventBusException(...);\n                }\n            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {\n                String methodName = method.getDeclaringClass().getName() + \".\" + method.getName();\n                throw new EventBusException(...);\n            }\n        }\n    }\n\n这里会对当前类中声明的所有方法进行校验，并将符合要求的方法的信息封装成一个`SubscriberMethod`对象塞到列表中。\n\n#### 注册订阅方法\n\n直到了如何拿到所有的订阅方法之后，我们回到之前的代码，看下订阅过程中的逻辑：\n\n    private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {\n        Class<?> eventType = subscriberMethod.eventType;\n        // 将所有的观察者和订阅方法封装成一个Subscription对象\n        Subscription newSubscription = new Subscription(subscriber, subscriberMethod); // 1\n        // 尝试从缓存中根据事件类型来获取所有的Subscription对象\n        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType); // 2\n        if (subscriptions == null) {\n            // 指定的事件类型没有对应的观察对象的时候\n            subscriptions = new CopyOnWriteArrayList<>();\n            subscriptionsByEventType.put(eventType, subscriptions);\n        } else {\n            if (subscriptions.contains(newSubscription)) {\n                throw new EventBusException(...);\n            }\n        }\n\n        // 这里会根据新加入的方法的优先级决定插入到队列中的位置\n        int size = subscriptions.size(); // 2\n        for (int i = 0; i <= size; i++) {\n            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {\n                subscriptions.add(i, newSubscription);\n                break;\n            }\n        }\n\n        // 这里又会从“订阅者-事件类型”列表中尝试获取该订阅者对应的所有事件类型\n        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber); // 3\n        if (subscribedEvents == null) {\n            subscribedEvents = new ArrayList<>();\n            typesBySubscriber.put(subscriber, subscribedEvents);\n        }\n        subscribedEvents.add(eventType);\n\n        // 如果是黏性事件还要进行如下的处理\n        if (subscriberMethod.sticky) { // 4\n            if (eventInheritance) {\n                Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();\n                for (Map.Entry<Class<?>, Object> entry : entries) {\n                    Class<?> candidateEventType = entry.getKey();\n                    if (eventType.isAssignableFrom(candidateEventType)) {\n                        Object stickyEvent = entry.getValue();\n                        // 这里会向该观察者通知所有的黏性事件\n                        checkPostStickyEventToSubscription(newSubscription, stickyEvent);\n                    }\n                }\n            } else {\n                Object stickyEvent = stickyEvents.get(eventType);\n                checkPostStickyEventToSubscription(newSubscription, stickyEvent);\n            }\n        }\n    }\n\n这里涉及到了几个集合，它们是用来做缓存的，还有就是来维护观察者、事件类型和订阅方法之间的关系的。注册观察的方法比较长，我们可以一点一点来看。首先，会在代码1处将观察者和订阅方法封装成一个 `Subscription` 对象。然后，在2处用到了 `CopyOnWriteArrayList` 这个集合，它是一种适用于**多读写少**场景的数据结构，是一种线程安全的数组型的数据结构，主要用来存储一个事件类型所对应的全部的 `Subscription` 对象。EventBus在这里通过一个 `Map<Class<?>, CopyOnWriteArrayList<Subscription>> ` 类型的哈希表来维护这个映射关系。然后，我们的程序执行到2处，在这里会对 `Subscription` 对象的列表进行遍历，并根据订阅方法的优先级，为当前的 `Subscription` 对象寻找一个合适的位置。3的地方主要的逻辑是获取指定的观察者对应的全部的观察事件类型，这里也是通过一个哈希表来维护这种映射关系的。然后，在代码 4 处，程序会根据当前的订阅方法是否是黏性的，来决定是否将当前缓存中的信息发送给新订阅的方法。这里会通过 `checkPostStickyEventToSubscription()` 方法来发送信息，它内部的实现的逻辑和 `post()` 方法类似，我们不再进行说明。\n\n取消注册的逻辑比较比较简单，基本上就是注册操作反过来——将当前订阅方法的信息从缓存中踢出来，我们不再进行分分析。下面我们分析另一个比较重要的地方，即发送事件相关的逻辑。\n\n### 2.3 通知\n\n通知的逻辑相对来说会比较复杂一些，因为这里面涉及一些线程之间的操作。我们看下下面的代码吧：\n\n    public void post(Object event) {\n        // 这里从线程局部变量中取出当前线程的状态信息\n        PostingThreadState postingState = currentPostingThreadState.get();\n        // 这里是以上线程局部变量内部维护的一个事件队列\n        List<Object> eventQueue = postingState.eventQueue;\n        // 将当前要发送的事件加入到队列中\n        eventQueue.add(event);\n\n        if (!postingState.isPosting) {\n            postingState.isMainThread = isMainThread();\n            postingState.isPosting = true;\n            if (postingState.canceled) {\n                throw new EventBusException(\"Internal error. Abort state was not reset\");\n            }\n            try {\n                // 不断循环来发送事件\n                while (!eventQueue.isEmpty()) {\n                    postSingleEvent(eventQueue.remove(0), postingState); // 1\n                }\n            } finally {\n                // 恢复当前线程的信息\n                postingState.isPosting = false;\n                postingState.isMainThread = false;\n            }\n        }\n    }\n\n这里的 `currentPostingThreadState` 是一个 `ThreadLocal` 类型的变量，其中存储了对应于当前线程的 `PostingThreadState` 对象，该对象中存储了当前线程对应的事件列表和线程的状态信息等。从上面的代码中可以看出，`post()` 方法会在1处不断从当前线程对应的队列中取出事件并进行发布。下面我们看以下这里的 `postSingleEvent()` 方法。\n\n    private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {\n        Class<?> eventClass = event.getClass();\n        boolean subscriptionFound = false;\n        if (eventInheritance) {\n            // 这里向上查找该事件的所有父类\n            List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);\n            int countTypes = eventTypes.size();\n            for (int h = 0; h < countTypes; h++) {\n                Class<?> clazz = eventTypes.get(h);\n                // 对上面的事件进行处理\n                subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);\n            }\n        } else {\n            subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);\n        }\n        // 找不到该事件的异常处理\n        if (!subscriptionFound) {\n            if (logNoSubscriberMessages) {\n                logger.log(Level.FINE, \"No subscribers registered for event \" + eventClass);\n            }\n            if (sendNoSubscriberEvent \n                    && eventClass != NoSubscriberEvent.class \n                    && eventClass != SubscriberExceptionEvent.class) {\n                post(new NoSubscriberEvent(this, event));\n            }\n        }\n    }\n\n在上面的代码中，我们会根据 `eventInheritance` 的值决定是否要同时遍历当前事件的所有父类的事件信息并进行分发。如果设置为 `true` 就将执行这一操作，并最终使用 `postSingleEventForEventType` 对每个事件类型进行处理。\n\n    private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {\n        // 获取指定的事件对应的所有的观察对象\n        CopyOnWriteArrayList<Subscription> subscriptions;\n        synchronized (this) {\n            subscriptions = subscriptionsByEventType.get(eventClass);\n        }\n        if (subscriptions != null && !subscriptions.isEmpty()) {\n            // 遍历观察对象，并最终执行事件的分发操作\n            for (Subscription subscription : subscriptions) {\n                postingState.event = event;\n                postingState.subscription = subscription;\n                boolean aborted = false;\n                try {\n                    postToSubscription(subscription, event, postingState.isMainThread);\n                    aborted = postingState.canceled;\n                } finally {\n                    postingState.event = null;\n                    postingState.subscription = null;\n                    postingState.canceled = false;\n                }\n                if (aborted) {\n                    break;\n                }\n            }\n            return true;\n        }\n        return false;\n    }\n\n在上面的代码中，我们会通过传入的事件类型到缓存中取寻找它对应的全部的 `Subscription`，然后对得到的 `Subscription` 列表进行遍历，并依次调用 `postToSubscription()` 方法执行事件的发布操作。下面是 `postToSubscription()` 方法的代码，这里我们会根据订阅方法指定的 `threadMode `信息来执行不同的发布策略。\n\n    private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {\n        switch (subscription.subscriberMethod.threadMode) {\n            case POSTING:\n                invokeSubscriber(subscription, event);\n                break;\n            case MAIN:\n                if (isMainThread) {\n                    invokeSubscriber(subscription, event);\n                } else {\n                    mainThreadPoster.enqueue(subscription, event);\n                }\n                break;\n            case MAIN_ORDERED:\n                if (mainThreadPoster != null) {\n                    mainThreadPoster.enqueue(subscription, event);\n                } else {\n                    invokeSubscriber(subscription, event);\n                }\n                break;\n            case BACKGROUND:\n                if (isMainThread) {\n                    backgroundPoster.enqueue(subscription, event);\n                } else {\n                    invokeSubscriber(subscription, event);\n                }\n                break;\n            case ASYNC:\n                asyncPoster.enqueue(subscription, event);\n                break;\n            default:\n                throw new IllegalStateException(...);\n        }\n    }\n\n在上面的方法中，会根据当前的线程状态和订阅方法指定 的 `threadMode` 信息来决定合适触发方法。这里的 `invokeSubscriber()` 会在当前线程中立即调用反射来触发指定的观察者的订阅方法。否则会根据具体的情况将事件加入到不同的队列中进行处理。这里的`mainThreadPoster` 最终继承自 `Handler`，当调用它的 `enqueue()` 方法的时候，它会发送一个事件并在它自身的 `handleMessage()` 方法中从队列中取值并进行处理，从而达到在主线程中分发事件的目的。这里的 `backgroundPoster` 实现了 `Runnable` 接口，它会在调用 `enqueue()` 方法的时候，拿到 EventBus 的 `ExecutorService` 实例，并使用它来执行自己。在它的 `run()` 方法中会从队列中不断取值来进行执行。\n\n## 总结\n\n以上就是Android中的EventBus的源码分析，这里我们回答之前提出的几个问题来作结：\n\n1. 在EventBus中，使用 `@Subscribe` 注解的时候指定的 `ThreadMode` 是如何实现在不同线程间传递数据的？\n\n要求主线程中的事件通过 `Handler` 来实现在主线程中执行，非主线程的方法会使用 EventBus 内部的 `ExecutorService` 来执行。实际在触发方法的时候会根据当前线程的状态和订阅方法的 `ThreadMode` 指定的线程状态来决定何时触发方法。非主线程的逻辑会在 `post()` 的时候加入到一个队列中被随后执行。\n\n2. 使用注解和反射的时候的效率问题，是否会像 Guava 的 EventBus 一样有缓存优化？\n\n内部使用了缓存，确切来说就是维护了一些映射的关系。但是它的缓存没有像 Guava 一样使用软引用之类方式进行优化，即一直是强引用类型的。\n\n3. 黏性事件是否是通过内部维护了之前发布的数据来实现的，是否使用了缓存？\n\n黏性事件会通过 EventBus 内部维护的一个`事件类型-黏性事件`的哈希表存储，当注册一个观察者的时候，如果发现了它内部有黏性事件监听，会执行 `post()` 类似的逻辑将事件立即发送给该观察者。\n"
  },
  {
    "path": "消息机制/线程通信：Handler、MessageQueue和Looper.md",
    "content": "# Android 消息机制：Handler、MessageQueue 和 Looper\n\n在这篇文章中，我们将会讨论 Android 的消息机制。提到 Handler，有过一些 Android 开发经验的都应该很清楚它的作用，通常我们使用它来通知主线程更新 UI。但是 Handler 需要底层的 MessageQueue 和 Looper 来支持才能运作。这篇文章中，我们将会讨论它们三个之间的关系以及实现原理。\n\n在这篇文章中，因为涉及线程方面的东西，所以就避不开 ThreadLocal。笔者在之前的文章中有分析过该 API 的作用，你可以参考笔者的这篇文章来学习下它的作用和原理，本文中我们就不再专门讲解：[《Java 并发编程：ThreadLocal 的使用及其源码实现》](https://juejin.im/post/5b44cd7c6fb9a04f980cb065)。\n\n## 1、Handler 的作用\n\n通常，当我们在非主线程当中做了异步的操作之后使用 Handler 来在主线程当中更新 UI。之所以这么设计无非就是因为 Android 中的 View 不是线程安全的。之所以将 View 设计成非线程安全的，是因为：1).对 View 进行加锁之后会增加控件使用的复杂度；2).加锁之后会降低控件执行的效率。但 Handler 并非只能用来在主线程当中更新 UI，确切来说它有两个作用：\n\n1. 任务调度：即通过 `post()` 和 `send()` 等方法来指定某个任务在某个时间执行；\n2. 线程切换：你也许用过 RxJava，但如果在 Android 中使用的话还要配合 RxAndroid，而这里的 RxAndroid 内部就使用 Handler 来实现线程切换。\n\n下文中，我们就来分别看一下它的这两个功能的作用和原理。\n\n### 1.1 任务调度\n\n使用 Hanlder 可以让一个任务在某个时间点执行或者等待某段时间之后执行。Handler 为此提供了许多方法，从方法的命名上，我们可以将其分成 `post()` 和 `sned()` 两类方法。``post() 类的用来指定某个 Runnable 在某个时间点执行，`send()` 类的用来指定某个 Message 在某个时间点执行。\n\n这里的 `Message` 是 Android 中定义的一个类。它内部有多个字段，比如 `what`、`arg1`、`arg2`、`replyTo` 和 `sendingUid` 来帮助我们指定该消息的内容和对象。同时， `Message` 还实现了 `Parcelable` 接口，这表明它可以被用来跨进程传输。此外，它内部还定义了一个 `Message` 类型的 `next` 字段，这表明 `Message` 可以被用作链表的结点。实际上 MessageQueue 里面只存放了一个 `mMessage`，即链表的头结点。所以，`MessageQueue` 内部的消息队列，本质上是一个单链表，每个链表的结点就是 `Message`。\n\n当调用 post() 类型的方法来调度某个 Runnable 的时候，首先会将其包装成一个 Message，然后再使用 send() 类的方法进行任务分发。所以，不论是 post() 类的方法还是 send() 类的方法，最终都会使用 `Handler`  的 `sendMessageAtTime()` 方法来将其加入到队列中：\n\n```java\n    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {\n        MessageQueue queue = mQueue;\n        if (queue == null) {\n            // ... 无关代码\n            return false;\n        }\n        return enqueueMessage(queue, msg, uptimeMillis);\n    }\n```\n\n使用 Handler 进行任务调度是非常简单的。下面的代码就实现了让一个 Runnable 在 500ms 之后执行的逻辑：\n\n```java\n    new Handler().postDelayed(new Runnable() {\n        @Override\n        public void run() {\n            // do something\n        }\n    }, 500);\n```\n\n上面的任务执行方式在主线程中执行不会出现任何问题，如果你在非主线程中执行的话就可能会出现异常。原因我们后面会讲解。\n\n既然每个 Runnable 被 `post()` 发送之后还要被包装成 Message，那么 Message 的意义何在呢？\n\nRunnable 被包装的过程依赖于 Handler 内部的 `getPostMessage()` 方法。下面是该方法的定义：\n\n```java\n    private static Message getPostMessage(Runnable r) {\n        Message m = Message.obtain();\n        m.callback = r;\n        return m;\n    }\n```\n\n可见，我们的 Runnable 会被赋值给 Message 的 callback。这种类型的消息无法做更详细的处理。就是说，我们无法利用消息的 `what`、`arg1` 等字段（本身我们也没有设置这些字段）。如果我们希望使用 Message 的这些字段信息，就需要：\n\n1. 首先，要使用 `send()` 类型的方法来传递我们的 Message 给 Handler；\n2. 然后，我们的 Handler 要覆写 `handleMessage()` 方法，并在该方法中获取每个 Message 并根据其内部的信息依次处理。\n\n下面的一个例子用来演示 `send()` 类型的方法。首先，我们要定义 Handler 并覆写其 `handleMessage()` 方法来处理消息：\n\n```java\n    private final static int SAY_HELLO = 1;\n\n    private static Handler handler = new Handler() {\n        @Override\n        public void handleMessage(Message msg) {\n            switch (msg.what) {\n                case SAY_HELLO:\n                    LogUtils.d(\"Hello!\");\n                    break;\n            }\n        }\n    };\n```\n\n然后，我们向该 Handler 发送消息：\n\n```java\n    Message message = Message.obtain(handler);\n    message.what = SAY_HELLO;\n    message.sendToTarget();\n```\n\n这样，我们的 Handler 接收到了消息并根据其 `what` 得知要 `SAY_HELLO`，于是就打印出了日志信息。除了调用 Message 的 `sendToTarget()` 方法，我们还可以直接调用 handler 的 `sendMessage()` 方法（`sendToTarget()` 内部调用了 handler 的 `sendMessage()`）。\n\n### 1.2 线程切换\n\n下面我们用了一份示例代码，它会先在主线程当中实例化一个 Handler，然后在某个方法中，我们开启了一个线程，并执行了某个任务。2 秒之后任务结束，我们来更新 UI。\n\n```java\n    // 在主线程中获取 Handler\n    private static Handler handler = new Handler();\t\t\n\n    // 更新UI，会将消息发送到主线程当中\n    new Thread(() -> {\n        try {\n            Thread.sleep(2000);\n            handler.post(() -> getBinding().tv.setText(\"主线程更新UI\"));\n        } catch (InterruptedException e) {\n            e.printStackTrace();\n        }\n    }).start();\n```\n\n上面之所以能够在主线程当中更新 UI，主要是因为我们的 Handler 是在主线程当中进行获取的。随后，我们调用 `handler` 的 `post()` 方法之后，传入的 `Runnable` 会被包装成 `Message`，然后加入到主线程对应的消息队列中去，并由主线程对应的 Looper 获取到并执行。所以，就使得该 Runnable 的操作最终在主线程中完成。\n\n也许你会觉得先在主线程当中获取到 Handler 然后再使用比较麻烦。别担心，我们还有另一种方式来解决这个问题。我们可以直接使用 Looper 的 `getMainLooper()` 方法来获取主线程对应的 Looper，然后使用它来实例化一个 `Handler` 并使用该 Handler 来处理消息：\n\n```java\n    new Handler(Looper.getMainLooper())\n        .post(() -> getBinding().tv.setText(\"主线程更新UI\"));\n```\n\n本质上，当我们调用 `Handler` 的无参构造方法，或者说不指定 Looper 的构造方法的时候，会直接使用当前线程对应的 Looper 来实例化 Handler。每个线程对应的 Looper 存储在该线程的局部变量 ThreadLocal 里。当某个线程的局部变量里面没有 Looper 的时候就会抛出一个异常。所以，我们之前说直接使用 `new` 来实例化一个 `Handler` 的时候可能出错就是这个原因。\n\n主线程对应的 Looper 会在 ActivityThread 的静态方法 `main()` 中被创建，它会调用 Looper 的 `prepareMainLooper()` 静态方法来创建主线程对应的 Looper。然后会调用 Looper 的 `loop()` 静态方法来开启 Looper 循环以不断处理消息。这里的 ActivityThread 用来处理应用进程中的活动和广播的请求，会在应用启动的时候调用。ActivityThread 内部定义了一个内部类 `H`，它继承自 `Handler`，同样运行在主线程中，用来处理接收到的来自各个活动、广播和服务的请求。\n\n除了使用主线程对应的 Looper，我们也可以开启我们自定义线程的 Looper。下面的代码中，我们开启了一个线程，并在线程中先调用 Looper 的 `prepare()` 静态方法，此时 Looper 会为我们当前的线程创建 Looper，然后将其加入到当前线程的局部变量里面。随后，当我们调用 Looper 的 `loop()` 方法的时候就开启了 Looper 循环来不断处理消息：\n\n```java\n    new Thread(() -> {\n        LogUtils.d(\"+++++++++\" + Thread.currentThread());\n        Looper.prepare();\n        new Handler().post(() -> LogUtils.d(\"+++++++++\" + Thread.currentThread()));\n        Looper.loop();\n    }).start();\n```\n\n从以上的内容我们可以看出，Handler 之所以能够实现线程切换，主要的原因是其内部的消息队列是对应于每一个线程的。发送的任务会在该线程对应的消息队列中被执行。而成功获取到该线程对应的消息队列就依靠 ThreadLocal 来对每个线程对应的消息队列进行存储。\n\n## 2、源码解析\n\n以上，我们分析了 Handler 的主要的两种主要用途，并且在这个过程中，我们提及了许多 Handler、MessageQueue 和 Looper 的底层设计。在上面的文章中，我们只是使用了文字来进行描述。在下文中，我们来通过源码来验证我们上面提到的一些内容。\n\n### 2.1 实例化 Handler\n\nHandler 了提供了多个重载的构造方法，我们可以将其分成两种主要的类型。一种在构造方法中需要明确指定一个 Looper，另一种在构造方法中不需要指定任何 Looper，在构造方法内部会获取当前线程对应的 Looper 来初始化 Handler。\n\n第一种初始化的方式最终都会调用下面的方法来完成初始化。这个方法比较简单，是基本的赋值操作：\n\n```java\n    public Handler(Looper looper, Callback callback, boolean async) {\n        mLooper = looper;\n        mQueue = looper.mQueue;\n        mCallback = callback;\n        mAsynchronous = async;\n    }\n```\n\n第二种初始化的方式最终会调用下面的方法。这里使用 Looper 的静态方法 `myLooper()` 来获取当前线程对应的 Looper。如果当前线程不存在任何 Looper 就会抛出一个异常。\n\n```java\n    public Handler(Callback callback, boolean async) {\n        // 潜在内存泄漏的检查\n        if (FIND_POTENTIAL_LEAKS) {\n            final Class<? extends Handler> klass = getClass();\n            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&\n                    (klass.getModifiers() & Modifier.STATIC) == 0) {\n                Log.w(TAG, \"The following Handler class should be static or leaks might occur: \" +\n                    klass.getCanonicalName());\n            }\n        }\n\n        // 使用 Looper 的静态方法 myLooper() 来获取当前线程的 Looper\n        mLooper = Looper.myLooper();\n        if (mLooper == null) {\n            throw new RuntimeException();\n        }\n        mQueue = mLooper.mQueue;\n        mCallback = callback;\n        mAsynchronous = async;\n    }\n```\n\n而 Looper 的静态方法 `myLooper()` 会使用线程局部变量 `sThreadLocal` 来获取之前存储到该线程内部的 Looper：\n\n```java\n    public static @Nullable Looper myLooper() {\n        return sThreadLocal.get();\n    }\n```\n\n### 2.2 Looper 的初始化\n\n前面我们也说过 Looper 的创建过程。对于主线程的 Looper 会在 `ActivityThread` 的 `main()` 方法中被调用：\n\n```java\n    public static void main(String[] args) {\n        // ... 无关代码\n        Looper.prepareMainLooper();\n        // ... 无关代码\n        // 开启 Looper 循环\n        Looper.loop();\n        throw new RuntimeException(\"Main thread loop unexpectedly exited\");\n    }\n```\n\n这里调用了 Looper 的静态方法 `prepareMainLooper()` 来初始化主线程的 Looper：\n\n```java\n    public static void prepareMainLooper() {\n        prepare(false);\n        synchronized (Looper.class) {\n            if (sMainLooper != null) {\n                throw new IllegalStateException(\"The main Looper has already been prepared.\");\n            }\n            sMainLooper = myLooper();\n        }\n    }\n```\n\n其内部先调用了 `prepare(boolean)` 方法来初始化一个 Looper 并将其放在线程局部变量 `sThreadLocal` 中，然后判断 `sMainLooper` 是否之前存在过。这是一种基本的单例校验，显然，我们只允许主线程的 Looper 被实例化一次。\n\n同样，非主线程的 Looper 也只允许被实例化一次。当我们在非主线程实例化一个 Looper 的时候会调用它的 `prepare()` 静态方法。它同样调用了  `prepare(boolean)` 方法来初始化一个 Looper 并将其放在线程局部变量 `sThreadLocal` 中。所以，主线程和非主线程的 Looper 实例化的时候本质上是调用同样的方法，只是它们实现的时机不同，并且，都只能被实例化一次。\n\n```java\n    public static void prepare() {\n        prepare(true);\n    }\n\n    private static void prepare(boolean quitAllowed) {\n        if (sThreadLocal.get() != null) {\n            throw new RuntimeException(\"Only one Looper may be created per thread\");\n        }\n        sThreadLocal.set(new Looper(quitAllowed));\n    }\n```\n\n经过上述分析，我们可以得知，对于一个线程只能实例化一个 Looper，所以当我们在同一个线程中多次创建 Handler 实例，它们是共享一个 Looper 的。或者说是一个 Looper 对应多个 Handler 也是可以的。\n\n### 2.3 MessageQueue 的实例化\n\n相比于 Looper 和 Handler，MessageQueue 就显得相对复杂一些。因为内部用到了 JNI 编程。初始化、销毁和入队等事件都用到了 `native` 的方法。你可以在 [android_os_MessageQueue](https://android.googlesource.com/platform/frameworks/base/+/master/core/jni/android_os_MessageQueue.cpp) 查看其源码的定义。\n\n每当我们实例化一个 Looper 的时候会调用它的构造方法，并在其中实例化一个 MessageQueue：\n\n```java\n    private Looper(boolean quitAllowed) {\n        mQueue = new MessageQueue(quitAllowed);\n        mThread = Thread.currentThread();\n    }\n```\n\n在实例化 Handler 的小节中可以看出，每次实例化一个 Handler 的时候，会从当前线程对应的 Looper 中取出 MessageQueue。所以，这里我们又可以得出结论一个 Handler 对应一个 MessageQueue。\n\n当我们实例化一个 MessageQueue 的时候会使用它的构造方法。这里会调用 native 层的 `nativeInit()` 方法来完成 MessageQueue 的初始化：\n\n```java\n    MessageQueue(boolean quitAllowed) {\n        mQuitAllowed = quitAllowed;\n        mPtr = nativeInit();\n    }\n```\n\n在 native 层，`nativeInit()` 方法的定义如下：\n\n```java\n    static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {\n        NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();\n        if (!nativeMessageQueue) {\n            jniThrowRuntimeException(env, \"Unable to allocate native queue\");\n            return 0;\n        }\n        nativeMessageQueue->incStrong(env);\n        return reinterpret_cast<jlong>(nativeMessageQueue);\n    }\n```\n\n从上面我们可以看出，在该方法中实例化了一个 `NativeMessageQueue` 之后返回了 `mPtr` 作为是 Java 层 MessageQueue `与NativeMessesageQueue` 的桥梁。这个 long 类型的成员保存了 native 实例，这是 jni 开发中常用到的方式。因此 MessageQueue 同样使用 mPtr 来表示 native 层的消息队列。`NativeMessageQueue` 在 native 层的部分定义和其构造方法的定义如下。\n\n```java\n    class NativeMessageQueue : public MessageQueue, public LooperCallback {\n    // ... 无关代码\n    NativeMessageQueue::NativeMessageQueue() :\n            mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {\n        mLooper = Looper::getForThread();\n        if (mLooper == NULL) {\n            mLooper = new Looper(false);\n            Looper::setForThread(mLooper);\n        }\n    }\n```\n\n从上面我们可以看出，`NativeMessageQueue` 继承自 `MessageQueue`。并且在其内部实例化了一个 native 层的 Looper（其源码在 [Looper](https://android.googlesource.com/platform/system/core/+/master/libutils/Looper.cpp)）。\n\n在 Android 的 native 层存在着一个于 Java 层类似的 Looper，它的主要作用是用来与 Java 层的 Looper 相互配合完成 Android 中最主要的线程通信。当消息队列中有消息存入时，会唤醒 Natvice 层的 Looper。当消息队列中没有消息时或者消息尚未到处理时间时， Natvice 层的 Looper 会 block 住整个线程。所以，创建了 Java Looper 的线程只有在有消息待处理时才处于活跃状态，无消息时 block 在等待消息写入的状态。既然如此，当我们在主线程中开启了 Looper 循环的话，为什么不会 block 住整个线程而导致 ANR 呢？这是因为，我们的主线程的消息都会发送给主线程对应的 Looper 来处理，所以，本质上，我们主线程中的许多事件也都是以消息的形式发送给主线程的 Handler 来进行处理的。只有当某个消息被执行的时间过长以至于无法处理其他事件的时候才会出现 ANR。\n\n上面我们实例化了一个 Native 层的 Looper。在其中主要做到的逻辑如下：\n\n```java\n    void Looper::rebuildEpollLocked() {\n        // 如果之前存在的话就关闭之前的 epoll 实例\n        if (mEpollFd >= 0) {\n            mEpollFd.reset(); // 关闭旧的epoll实例\n        }\n        // 申请新的 epoll 实例，并且注册 “Wake管道”\n        mEpollFd.reset(epoll_create(EPOLL_SIZE_HINT));\n        LOG_ALWAYS_FATAL_IF(mEpollFd < 0, \"Could not create epoll instance: %s\", strerror(errno));\n        struct epoll_event eventItem;\n        // 把未使用的数据区域进行置0操作\n        memset(& eventItem, 0, sizeof(epoll_event));\n        eventItem.events = EPOLLIN;\n        eventItem.data.fd = mWakeEventFd.get();\n        // 将唤醒事件 (mWakeEventFd) 添加到 epoll 实例 (mEpollFd)\n        int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);\n        LOG_ALWAYS_FATAL_IF(result != 0, \"Could not add wake event fd to epoll instance: %s\", strerror(errno));\n        // 这里主要添加的是Input事件如键盘，传感器输入，这里基本上由系统负责，很少主动去添加\n        for (size_t i = 0; i < mRequests.size(); i++) {\n            const Request& request = mRequests.valueAt(i);\n            struct epoll_event eventItem;\n            request.initEventItem(&eventItem);\n            // 将 request 队列的事件，分别添加到 epoll 实例\n            int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);\n        }\n    }\n```\n\n这里涉及了 [epoll](https://baike.baidu.com/item/epoll/10738144?fr=aladdin) 相关的知识。`epoll` 是一个可扩展的 Linux I/O 事件通知机制，用来实现多路复用 (Multiplexing)。它将唤醒事件按对应的 fd 注册进 epoll，然后 epoll 帮你监听哪些唤醒事件上有消息到达。此时的唤醒事件应该采用非阻塞模式。这样，整个过程只在调用 `epoll` 的时候才会阻塞，收发客户消息是不会阻塞的，整个进程或者线程就被充分利用起来，这就是事件驱动，所谓的响应模式。\n\n上面的代码中使用了 `epoll_ctl` 方法来将被监听的描述符添加到 epoll 句柄。关于 epoll 的指令，可以参考这篇博文 [《epoll机制:epoll_create、epoll_ctl、epoll_wait、close》](https://blog.csdn.net/yusiguyuan/article/details/15027821)。这部分代码的主要作用是创建一个 epoll 实例并用它来监听 event 触发。\n\n### 2.4 消息的执行过程\n\n#### 2.4.1 消息入队的过程\n\n在介绍 Handler 的使用的时候，我们也说过不论是 Runnable 还是 Message 最终都会被封装成 Meseage 并加入到队列中。那么，加入队列之后又是怎么被执行的呢？\n\n首先，我们先看下入队的过程。以下是 Handler 中定义的方法，每当我们将一个消息入队的时候，都会调用它来完成。\n\n```java\n    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {\n        msg.target = this;\n        if (mAsynchronous) {\n            msg.setAsynchronous(true);\n        }\n        return queue.enqueueMessage(msg, uptimeMillis);\n    }\n```\n\n从上面可以看出，入队的时候实际上是使用了 MessageQueue 的 `enqueueMessage()` 方法。所以，我们再来看下该方法的定义：\n\n```java\n    boolean enqueueMessage(Message msg, long when) {\n        // ... 无关代码，校验\n        synchronized (this) {\n            // ... 无关代码\n            Message p = mMessages;\n            boolean needWake;\n            if (p == null || when == 0 || when < p.when) {\n                msg.next = p;\n                mMessages = msg;\n                needWake = mBlocked;\n            } else {\n                needWake = mBlocked && p.target == null && msg.isAsynchronous();\n                Message prev;\n                for (;;) {\n                    prev = p;\n                    p = p.next;\n                    if (p == null || when < p.when) {\n                        break;\n                    }\n                    if (needWake && p.isAsynchronous()) {\n                        needWake = false;\n                    }\n                }\n                msg.next = p;\n                prev.next = msg;\n            }\n\n            if (needWake) {\n                nativeWake(mPtr);\n            }\n        }\n        return true;\n    }\n```\n\n从上面的方法可以看出，所谓的入队操作本质上就是一个将新的消息加入到队列中的逻辑。当然，这里加入的时候要根据消息的触发时间对消息进行排序。然后，会根据 `needWake` 来决定是否调用 native 层的方法进行唤醒。只有当当前的头结点消息之前存在栅栏 (barrier) 并且新插入的消息是最先要被触发的异步消息就进行唤醒。当一般情况下是无需进行唤醒的。\n\n这里的 `nativeWake()` 方法会最终调用 native 层的 Looper 的 awake() 方法：\n\n```java\n    void Looper::wake() {\n        uint64_t inc = 1;\n        ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));\n        if (nWrite != sizeof(uint64_t)) {\n            if (errno != EAGAIN) {\n                LOG_ALWAYS_FATAL(\"Could not write wake signal to fd %d: %s\", mWakeEventFd.get(), strerror(errno));\n            }\n        }\n    }\n```\n\n此方法向 mWakeEventFd 写入了一个字节的内容。到底是什么内容并不重要，重要的是 fd 存在内容了，换句话说就是 mWakeEventFd 可读了，也就是 Native 层的 Looper 的线程从 block 状态中醒了过来。之所以需要进行唤醒，是因为，每次我们处理了消息之后会根据下个消息执行的时间进行唤醒。如果新插入的消息是最新的消息，那么显然，我们需要把唤醒的时间重置。（Native 层的 Looper 会在我们调用 Java 层的 MessageQueue 的时候执行 `epoll_wait` 时进入 block 状态。）\n\n#### 2.4.2 消息执行的过程\n\n在上文中，我们分析了 MessageQueue 将消息入队的过程。那么这些消息要在什么时候被执行呢？在介绍 Handler 的使用的时候，我们也提到过当我们实例化了 Looper 之后都应该调用它的 `loop()` 静态方法来处理消息。下面我们来看下这个方法的定义。\n\n```java\n    public static void loop() {\n        final Looper me = myLooper();\n        // .. 无关代码\n        final MessageQueue queue = me.mQueue;\n        // .. 无关代码\n        for (;;) {\n            Message msg = queue.next(); // 可能会 bolck\n            if (msg == null) {\n                return;\n            }\n            // ... 无关代码\n            final long dispatchEnd;\n            try {\n                msg.target.dispatchMessage(msg);\n                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;\n            } finally {\n                if (traceTag != 0) {\n                    Trace.traceEnd(traceTag);\n                }\n            }\n            // ... 无关代码\n            msg.recycleUnchecked();\n        }\n    }\n```\n\n从上面我们可以看出，当该方法被调用的时候，它会先开启一个无限循环，并在该循环中使用 MessageQueue 的 `next()` 方法来取出下一个消息并进行分发。这里我们先不看 `next()` 方法的定义。我们先把这个方法中涉及的部分分析一下。\n\n当获取到了下一个消息之后，会调用它的` target` 也就是发送该消息的 Handler 的 `dispatchMessage()` 方法来进行处理。该方法的定义如下：\n\n```java\n    public void dispatchMessage(Message msg) {\n        if (msg.callback != null) {\n            handleCallback(msg);\n        } else {\n            if (mCallback != null) {\n                if (mCallback.handleMessage(msg)) {\n                    return;\n                }\n            }\n            handleMessage(msg);\n        }\n    }\n```\n\n从上面可以看出，如果该消息是通过包装 Runnable 得到的话，会直接调用它的 `handleCallback()` 方法进行处理。在该方法内部会直接调用 Runnable 的  `run()` 方法。因为比较见到那，我们就补贴出代码了。\n\n然后，会根据 `mCallback` 是否为空来决定是交给 `mCallback` 进行处理还是内部的 `handleMessage()` 方法。这里的 `mCallback` 是一个接口，可以在创建 Handler 的时候通过构造方法指定，也比较简单。而这里的 `handleMessage()` 方法，我们就再熟悉不过了，它就是我们创建 Handler 的时候重写的、用来处理消息的方法。这样，消息就被发送到了我们的 Handler 中进行处理了。\n\n以上就是消息被处理的过程，代码的逻辑还是比较清晰的。下面我们就来看下 MessageQueue 是如何获取 “下一个” 消息的。\n\n#### 2.4.3 MessageQueue 的消息管理\n\n上面我们已经分析完了 Handler 发送的消息执行的过程。这里我们在来分析一下其中的获取 “下一个” 消息的逻辑：\n\n```java\n    Message next() {\n        // 如果消息循环已经停止就直接返回。如果应用尝试重启已经停止的Looper就会可能发生这种情况。\n        final long ptr = mPtr;\n        if (ptr == 0) {\n            return null;\n        }\n        int pendingIdleHandlerCount = -1; // -1 only during first iteration\n        int nextPollTimeoutMillis = 0;\n        for (;;) {\n            if (nextPollTimeoutMillis != 0) {\n                Binder.flushPendingCommands();\n            }\n            // 调用 native 的方法，可能会这个函数发生 block\n            nativePollOnce(ptr, nextPollTimeoutMillis);\n            // ... 无关代码\n        }\n    }\n```\n\n从上面可以看出 Java 层的 MessageQueue 的 next() 方法是一个循环。除了获取消息队列之外，还要监听 Natvie 层 Looper 的事件触发。通过调用 native 层的 `nativePollOnce()` 方法来实现。该方法内部又会调用 `NativeMessageQueue` 的 `pollOnce()` 方法。而且注意下，在下面的方法中，`nativeMessageQueue` 是从 Java 层的 `mPtr` 中获取到的。所以我们说，在初始化 MessageQueue 的时候得到的 `mPtr` 起到了桥梁的作用：\n\n```java\n    static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,\n            jlong ptr, jint timeoutMillis) {\n        NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);\n        nativeMessageQueue->pollOnce(env, obj, timeoutMillis);\n    }\n```\n\n在 `NativeMessageQueue` 的 `pollOnce()` 方法中会调用 native 层的 `Looper` 的 `pollOnce()`，并最终调用 native 层 Looper 的 `pollInner()` 方法：\n\n```java\n    int Looper::pollInner(int timeoutMillis) {\n        // ... 根据下一个消息的事件调整超时时间\n        int result = POLL_WAKE;\n        mResponses.clear();\n        mResponseIndex = 0;\n        mPolling = true; // 将要空闲\n        struct epoll_event eventItems[EPOLL_MAX_EVENTS];\n        // 待已注册之事件被触发或计时终了\n        int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);\n        mPolling = false; // 不再空闲\n        mLock.lock(); // 请求锁\n        if (mEpollRebuildRequired) {\n            mEpollRebuildRequired = false;\n            rebuildEpollLocked(); // 根据需要重建 epoll\n            goto Done;\n        }\n        // 进行检查\n        if (eventCount < 0) {\n            if (errno == EINTR) {\n                goto Done;\n            }\n            result = POLL_ERROR; // 错误\n            goto Done;\n        }\n        if (eventCount == 0) {\n            result = POLL_TIMEOUT; // 超时\n            goto Done;\n        }\n        // 处理所有消息\n        for (int i = 0; i < eventCount; i++) {\n            int fd = eventItems[i].data.fd;\n            uint32_t epollEvents = eventItems[i].events;\n            if (fd == mWakeEventFd.get()) { // 唤醒 fd 有反应\n                if (epollEvents & EPOLLIN) {\n                    awoken(); // 已经唤醒了，则读取并清空管道数据\n                }\n            } else {\n                // 其他 input fd 处理，其实就是将活动 fd 放入到 responses 队列中，等待处理\n                ssize_t requestIndex = mRequests.indexOfKey(fd);\n                if (requestIndex >= 0) {\n                    int events = 0;\n                    if (epollEvents & EPOLLIN) events |= EVENT_INPUT;\n                    if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;\n                    if (epollEvents & EPOLLERR) events |= EVENT_ERROR;\n                    if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;\n                    // 将消息放进 mResponses 中\n                    pushResponse(events, mRequests.valueAt(requestIndex));\n                }\n            }\n        }\n    Done: ;\n        // 触发所有的消息回调，处理 Native 层的Message\n        mNextMessageUptime = LLONG_MAX;\n        while (mMessageEnvelopes.size() != 0) {\n            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);\n            const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);\n            if (messageEnvelope.uptime <= now) {\n                { // 获取 handler\n                    sp<MessageHandler> handler = messageEnvelope.handler;\n                    Message message = messageEnvelope.message;\n                    mMessageEnvelopes.removeAt(0);\n                    mSendingMessage = true;\n                    mLock.unlock();\n                    handler->handleMessage(message);\n                } // 释放 handler\n                mLock.lock();\n                mSendingMessage = false;\n                result = POLL_CALLBACK;\n            } else {\n                // 队列头部的消息决定了下个唤醒的时间\n                mNextMessageUptime = messageEnvelope.uptime;\n                break;\n            }\n        }\n        mLock.unlock(); // 释放锁\n        // 触发所有的响应回调\n        for (size_t i = 0; i < mResponses.size(); i++) {\n            Response& response = mResponses.editItemAt(i);\n            if (response.request.ident == POLL_CALLBACK) {\n                int fd = response.request.fd;\n                int events = response.events;\n                void* data = response.request.data;\n                int callbackResult = response.request.callback->handleEvent(fd, events, data);\n                if (callbackResult == 0) {\n                    removeFd(fd, response.request.seq); // 移除文件描述符\n                }\n                response.request.callback.clear();\n                result = POLL_CALLBACK;\n            }\n        }\n        return result;\n    }\n```\n\n从上面我们可以看出 Native 层的 `pollInner()` 方法首先会根据 Java 层传入的 `timeoutMillis` 调用 `epoll_wait` 方法来让线程进入等待状态。如果 timeoutMillis 不为 0，那么线程将进入等待状态。如果有事件触发发生，wake 或者其他复用 Looper 的 event，处理event，这样整个 Native 层的 Looper 将从 block 状态中解脱出来了。这样回到 Java 层就将继续执行 MessageQueue 中下一条语句。至于 Native 层的 Looper 何时从 block 状态中醒过来，就需要根据我们入队的消息来定。也就用到了 MessageQueue 的 `enqueueMessage()` 方法的最后几行代码：\n\n```java\n    if (needWake) {\n        nativeWake(mPtr);\n    }\n```\n\n即：只有当当前的头结点消息之前存在栅栏 (barrier) 并且新插入的消息是最先要被触发的异步消息就进行唤醒。\n\n上面主要是 Native 层的 Looper 线程 block 的相关的逻辑。即当我们获取消息队列的下一条消息的时候会根据下一个消息的时间来决定线程 block 的时长。当我们将一个消息加入到队列的时候会根据新的消息的时间重新调整线程 block 的时长，如果需要的话还需要唤起 block 的线程。当线程从 block 状态恢复出来的时候，Java 层的 Looper 就拿到了一个消息，对该消息进行处理即可。\n\n## 3、总结\n\n在上文中，我们从 Java 层到 Native 层分析了 Handler 的作用的原理。这里我们对这部分内容做一个总结。\n\n### 3.1 Handler、MessageQueue 和 Looper 之间的关系\n\n首先是 Handler、MessageQueue 和 Looper 之间的关系。我们用下面的这个图来表示：\n\n![MessageQueue Handler Looper](res/Handler_Looper_Message.png)\n\n也就是说，一个线程中可以定义多个 Handler 实例，但是每个 Handler 实际上引用的是同一个 Looper。当然，我们要在创建 Handler 之前先创建 Looper。而每个 Looper 又只对应一个 MessageQueue。该 MessageQueue 会在创建 Looper 的时候被创建。在 MessageQueue 中使用 Message 对象来拼接一个单向的链表结构，依次来构成一个消息队列。每个 Message 是链表的一个结点，封装了我们发送的信息。\n\n### 3.2 Handler 的消息发送过程\n\n然后，我们再来分析下 Handler 中的消息是如何被发送的。同样，我们使用一个图来进行分析：\n\n![Handler 的消息发送过程](res/Handler_send_message.png)\n\n根据上文的内容我们将 Handler 发送消息的方法分成 post 和 send 两种类型。post 的用来发送 Runnable 类型的数据，send 类型的用来发送 Message 类型的数据。但不论哪种类型最终都会调用 Handler 的 `sendMessageAtTime()` 方法来加入到 MessageQueue 的队列中。区别在于，post 类型的方法需要经过 Handler 的 `getPostMessage()` 包装成 Message 之后再发送。\n\n### 3.3 Looper 的执行过程\n\n当消息被添加到队列之后需要执行消息，这部分内容在 Looper 的 `loop()` 方法中。但是这部分内容稍显复杂，因为涉及 Native 层的一些东西。我们这里仍然使用图来进行描述：\n\n![Looper 的执行过程](res/Handler_handle_message.png)\n\n当我们调用 Looper 的 loop() 方法之后整个 Looper 循环就开始不断地处理消息了。在上图中就是我们用绿色标记的一个循环。当我们在循环中调用 MessageQueue 的 next() 方法来获取下一个消息的时候，会调用 nativePollOnce() 方法，该方法可能会造成线程阻塞和非阻塞，当线程为非阻塞的时候就会从 Native 层回到 Java 层，从 MessageQueuue 中取得一个消息之后给 Looper 进行处理。如果获取的时候造成线程阻塞，那么有两种情况会唤醒阻塞的线程，一个是当一个新的消息被加入到队列中，并且将会早于之前队列的所有消息被触发，那么此时将会重新设置超时时间。如果达到了超时时间同样可以从睡眠状态中返回，也就回到了 Java 层继续处理。所以，Native 层的 Looper 的作用就是通过阻塞消息队列获取消息的过程阻塞 Looper。\n\n### 3.4 最后\n\n因为本文中不仅分析了 Java 层的代码，同时分析了 framework 层的代码，所以最好能够结合两边的源码一起看，这样更有助于自己的理解。在上面的文章中，我们给出了一些类的在线的代码链接，在 Google Source 上面，需要 VPN 才能浏览。另外，因为笔者水平有限，难免存在有误和不足的地方，欢迎批评指正。\n\n\n\n------\n**我是 WngShhng. 如果您喜欢我的文章，可以在以下平台关注我：**\n\n- 个人主页：[https://shouheng88.github.io/](https://shouheng88.github.io/)\n- 掘金：[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a)\n- Github：[https://github.com/Shouheng88](https://github.com/Shouheng88)\n- CSDN：[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068)\n- 微博：[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113)\n\n\n"
  },
  {
    "path": "消息机制/跨进程通信：Binder机制.md",
    "content": "# Android 系统源码-2：Binder 通信机制\n\nBinder 是 Android 系统中非常重要的组成部分。Android 系统中的许多功能建立在 Binder 机制之上。在这篇文章中，我们会对 Android 中的 Binder 在系统架构中的作用进行分析；然后，我们会从底层的实现角度简要说明为什么 Android 要开发出一套独立的跨进程通信机制；最后，我们会给出一个 AIDL 的使用示例来说明如何使用 Binder 来进行通信。\n\n## 1、什么是 Binder？ 为什么说它对 Android 系统至关重要？\n\n“什么是 Binder？ 为什么说它对 Android 系统至关重要？” 在回答这个问题之前，我们先来说下其他的东西。\n\n不知道你有没有思考过这么一个问题：为什么当我们在 Android 中启动一个页面的时候需要调用 `startActivity()` 方法，然后还要传入一个 Intent？ 如果我们不使用这种传递值的方式，直接写成静态的变量有没有问题？这也是之前有人问过我的一个问题。\n\n对上面的两个问题，我们先回答第二个。使用静态的变量传递值在大部分情况下是可以的，当然要注意在使用完了值之后要及时释放资源，不然会占用太多内存，甚至 OOM. 但是，在特殊的情况下它是无法适用的，即跨进程的情况下。这是因为，静态的变量的作用范围只是其所在的进程，在其他进程访问的时候属于跨进程访问，当然访问不到了。对于第一个问题，Android 中的一个 Activity 的启动过程远比我们想象的复杂，其中就涉及跨进程的通信过程。当我们调用 `startActivity()` 方法之后，我们的所有的 “意图” 会经过层层过滤，直到一个称之为 AMS 的地方被处理。处理完之后，再跨进程调用你启动页面时的进程进行后续处理，即回调 `onCreate()` 等生命周期方法。\n\n*一个 Activity 的启动过程涉及 Android 中两种重要的通信机制，Binder 和 Handler，我们会在以后的文章中对此进行分析。*\n\n下面我们通过一个简单的图来说明一下 Activity 的启动过程：\n\n![一个Activity的启动过程](res/AMS.png)\n\n当我们调用 `startActivity()` 方法的时候，首先会从 ServiceManager 中获取到 ActivityManagerService （就是 AMS），然后将 ApplicationThread 作为参数传递给 AMS，然后执行 AMS 的方法来启动 Activity. （在我们的应用进程中执行另一个进程的方法。）\n\nAMS 是全局的，在系统启动的时候被启动。当我们使用它的时候从 ServiceManager 中获取这个全局的变量即可。当我们调用它的方法的时候，方法具体的执行逻辑将在系统的进程中执行。我们传入的  ApplicationThread 就像一个信使一样。当 AMS 处理完毕，决定回调 Activity 的生命周期方法的时候，就直接调用 ApplicationThread 的方法（这是在另一个进程中调用我们的应用进程）。这样就实现了我们的 Activity 的生命周期的回调。\n\n看了上面的过程，也许有的同学会觉得。Binder 对 Android 系统至关重要，但是我们并没有用到 Binder 啊。实际上，我们只是没有直接使用 Binder. 以下图为例，我们说下我们实际开发过程中是如何使用 Binder 的。\n\n![AIDL Manager](res/AIDL_Manager.png)\n\n在大多数情况下，我们都在与各个 Manager 进行交互，而实际上这些 Manager 内部是使用 Binder 来进行跨进程通信的。如上所示，当我们调用 Manager 的时候，Manager 会通过代理类来从 Binder 驱动中得到另一个进程的 Stub 对象，然后我们使用该 Stub 对象，远程调用另一个进程的方法。只是这个过程被封装了，我们没有感知到而已，而这个跨进程通信 (IPC) 的机制就是 Binder 机制。\n\n至于什么是 Stub 呢？Stub 是 AIDL 规范中的一部分。AIDL 为我们使用 Binder 提供了一套模板。在 Android 系统中大量使用了这种定义来完成跨进程通信。稍后我们介绍 AIDL 的时候，你将看到它是如何作用的。\n\n## 2、为什么是 Binder 而不是其他通信机制？\n\nAndroid 是基于 Linux 的，Linux 本身已经具有了许多的 IPC 机制，比如：管道（Pipe）、信号（Signal）和跟踪（Trace）、插口（Socket）、消息队列（Message）、共享内存（Share Memory）和信号量（Semaphore）。那么，为什么 Android 要特立独行地搞出一套 IPC 机制呢？这当然是有原因的：\n\n1. **效率上** ：Socket 作为一款通用接口，其传输效率低，开销大，主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式，即数据先从发送方缓存区拷贝到内核开辟的缓存区中，然后再从内核缓存区拷贝到接收方缓存区，至少有两次拷贝过程。共享内存虽然无需拷贝，但控制复杂，难以使用。**Binder 只需要一次数据拷贝，性能上仅次于共享内存**。\n\n2. **稳定性：Binder 基于 C|S 架构，客户端（Client）有什么需求就丢给服务端（Server）去完成，架构清晰、职责明确又相互独立，自然稳定性更好。** 共享内存虽然无需拷贝，但是控制负责，难以使用。从稳定性的角度讲，Binder 机制是优于内存共享的。\n\n3. **安全性：Binder 通过在内核层为客户端添加身份标志 `UID|PID`，来作为身份校验的标志，保障了通信的安全性。** 传统 IPC 访问接入点是开放的，无法建立私有通道。比如，命名管道的名称，SystemV 的键值，Socket 的 ip 地址或文件名都是开放的，只要知道这些接入点的程序都可以和对端建立连接，不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。\n\n除了上面的原因之外，Binder 还拥有许多其他的特性，比如：1).采用引用计数，当某个 Binder 不再被任何客户端引用的时候，会通知它的持有者可以将其释放，这适用于 Android 这种常常因为资源不足而回收资源的应用场景。2).它内部维护了一个线程池；3).可以像触发本地方法一样触发远程的方法。4).支持同步和异步 (oneway) 的触发模型；5).可以使用 AIDL 模板进行描述和开发。\n\n## 3、Binder 模型，Binder 中的 4 个主要角色\n\n在 Binder 模型中共有 4 个主要角色，它们分别是：Client、Server、Binder 驱动和 ServiceManager. Binder 的整体结构是基于 C|S 结构的，以我们启动 Activity 的过程为例，每个应用都会与 AMS 进行交互，当它们拿到了 AMS 的 Binder 之后就像是拿到了网络接口一样可以进行访问。如果我们将 Binder 和网络的访问过程进行类比，**那么 Server 就是服务器，Client 是客户终端，ServiceManager 是域名服务器（DNS），驱动是路由器。其中 Server、Client 和 ServiceManager 运行于用户空间，驱动运行于内核空间**。\n\n当我们的系统启动的时候，会在启动 SystemServer 进程的时候启动各个服务，也包括上面的 AMS. 它们会被放进一个哈希表中，并且哈希表的键是字符串。这样我们就可以通过服务的字符串名称来找到对应的服务。这些服务就是一个个的 Binder 实体，对于 AMS 而言，也就是 `IActivityManager.Stub` 实例。这些服务被启动的之后就像网络中的服务器一样一直等待用户的访问。\n\n对于这里的 ServiceManager，它也是一种服务，但是它比较特殊，它会在所有其他的服务之前被注册，并且只被注册一次。它的作用是用来根据字符串的名称从哈希表中查找服务，以及在系统启动的时候向哈希表中注册服务。\n\n![Binder 模型](res/binder_model.png)\n\n所以，我们可以使用上面的这张图来描述整个 Binder 模型：首先，在系统会将应用程序所需的各种服务通过 Binder 驱动注册到系统中（ServiceManager 先被注册，之后其他服务再通过 ServiceManager 进行注册），然后当某个客户端需要使用某个服务的时候，也需要与 Binder 驱动进行交互，Binder 会通过服务的名称到 ServiceManager 中查找指定的服务，并将其返回给客户端程序进行使用。\n\n## 4、Binder 的原理\n\n上面我们梳理了 Binder 的模型，以及为什么系统设计一套通信机制的原因。那么你是否也好奇神乎其神的 Binder 究竟是怎么实现的呢？这里我们来梳理下 Binder 内部实现的原理。\n\n首先，Binder 的实现过程是非常复杂的，在《Android 系统源码情景分析》一书中有 200 页的篇幅都在讲 Binder. 在这里我们不算详细地讲解它的具体的实现原理，我们只对其中部分内容做简单的分析，并且不希望涉及大量的代码。\n\n### 4.1 inder 相关的系统源码的结构\n\n然后，我们需要介绍下 Binder 相关的核心类在源码中的位置，\n\n```java\n-framework\n    |--base\n        |--core\n            |--java--android--os  \n                              |--IInterface.java\n                              |--IBinder.java\n                              |--Parcel.java\n                              |-- IServiceManager.java\n                              |--ServiceManager.java\n                              |--ServiceManagerNative.java\n                              |--Binder.java  \n            |--jni\n                |--android_os_Parcel.cpp\n                |--AndroidRuntime.cpp\n                |--android_util_Binder.cpp\n    |--native\n        |--libs--binder         \n                  |--IServiceManager.cpp\n                  |--BpBinder.cpp\n                  |--Binder.cpp             // Binder 的具体实现\n                  |--IPCThreadState.cpp\n                  |--ProcessState.cpp\n        |--include--binder                  // 主要是一些头文件\n                      |--IServiceManager.h\n                      |--IInterface.h\n        |--cmds--servicemanager\n                    |--service_manager.c    // 用来注册服务的 ServiceManager\n                    |--binder.c\n-kernel-drivers-staging-android\n                         |--binder.c        \n                         |--uapi-binder.h\n```\n\n### 4.2 Binder 实现过程中至关重要的几个函数\n\n当我们查看 binder.c 的源码的时候，或者查看与 Binder 相关的操作的时候，经常看到几个操作 ioctl, mmap 和 open. 那么这几个操作符是什么含义呢？\n\n首先，`open` 函数用来打开文件的操作符，在使用的时候需要引入头文件，`#include <sys/types.h>`、`#include <sys/stat.h>` 和 `#include <fcntl.h>`，其函数定义如下，\n\n```c\nint open(const char * pathname, int flags);\nint open(const char * pathname, int flags, mode_t mode);\n```\n\n这里的 `pathname` 表示文件路径；`flag` 表示打开方式；`mode` 表示打开的模式和权限等；若所有欲核查的权限都通过了检查则返回 0, 表示成功, 只要有一个权限被禁止则返回-1.\n\n然后是 `ioctl` 指令，使用的时候需要引入 `#include <sys/ioctl.h>` 头文件，ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数，用于向设备发控制和配置命令。其函数定义如下： \n\n```c\nint ioctl(int fd, ind cmd, …)； \n```\n\n其中 fd 是用户程序打开设备时使用 open 函数返回的文件标示符，cmd 是用户程序对设备的控制命令，至于后面的省略号，那是一些补充参数，一般最多一个，这个参数的有无和 cmd 的意义相关。 \n\n最后是 `mmap` 函数，它用来实现内存映射。使用的时候需要引入头文件 `#include <sys/mman.h>`. 与之对应的还有 `munmap` 函数。它们的函数定义如下，\n\n```c\nvoid* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);\nint munmap(void* start,size_t length);\n```\n\n这里的参数的含义是：\n\n1. start：映射区的开始地址，设置为0时表示由系统决定映射区的起始地址；\n2. length：映射区的长度。长度单位是以字节为单位，不足一内存页按一内存页处理；\n3. prot：期望的内存保护标志，不能与文件的打开模式冲突。是以下的某个值，可以通过 o r运算合理地组合在一起；\n4. flags：指定映射对象的类型，映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体；\n5. fd：有效的文件描述词。一般是由 `open()` 函数返回，其值也可以设置为-1，此时需要指定 flags 参数中的 MAP_ANON,表明进行的是匿名映射；\n6. off_toffset：被映射对象内容的起点。\n\n成功执行时，`mmap()` 返回被映射区的指针，`munmap()` 返回0。失败时，`mmap()` 返回 MAP_FAILED[其值为(void *)-1]，`munmap()` 返回 -1.\n\n### 4.3 ServiceManger 启动\n\nBinder 中的 ServiceManager 并非 Java 层的 ServiceManager，而是 Native 层的。启动 ServiceManager 由 init 进程通过解析 init.rc 文件而创建。启动的时候会找到上述源码目录中的 service_manager.c 文件中，并调用它的 main() 方法，\n\n```c\n// platform/framework/native/cmds/servicemanager.c\nint main(int argc, char** argv)\n{\n    struct binder_state *bs;\n    char *driver;\n\n    if (argc > 1) {\n        driver = argv[1];\n    } else {\n        driver = \"/dev/binder\";\n    }\n    // 1. 打开 binder 驱动\n    bs = binder_open(driver, 128*1024);\n    // ...\n    // 2. 将当前的 ServiceManger 设置成上下文\n    if (binder_become_context_manager(bs)) {\n        return -1;\n    }\n    // ...\n    // 3. 启动 binder 循环，进入不断监听状态\n    binder_loop(bs, svcmgr_handler);\n    return 0;\n}\n```\n\nServcieManager 启动的过程就是上面三个步骤，无需过多说明。下面我们给出这三个方法具体实现的。在下面的代码中你将看到我们之前介绍的三个函数的实际应用。相应有了前面的铺垫之后你理解起来不成问题 :)\n\n```c\n// platform/framework/native/cmds/servicemanager.c\nstruct binder_state *binder_open(const char* driver, size_t mapsize)\n{\n    struct binder_state *bs;\n    struct binder_version vers;\n    bs = malloc(sizeof(*bs));\n    if (!bs) {\n        errno = ENOMEM;\n        return NULL;\n    }\n    // 打开设备驱动\n    bs->fd = open(driver, O_RDWR | O_CLOEXEC);\n    if (bs->fd < 0) {\n        goto fail_open;\n    }\n    // 向驱动发送指令，获取binder版本信息\n    if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||\n        (vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {\n        goto fail_open;\n    }\n    bs->mapsize = mapsize;\n    // 通过系统调用，mmap 内存映射，mmap 必须是 page 的整数倍\n    bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);\n    if (bs->mapped == MAP_FAILED) {\n        goto fail_map;\n    }\n    return bs;\nfail_map:\n    close(bs->fd);\nfail_open:\n    free(bs);\n    return NULL;\n}\n```\n\n在上面的代码中，先使用 `open()` 函数打开设备驱动（就是一个打开文件的操作），然后使用 `ioctl()` 函数向上面的设备驱动发送指令以获取设备信息。最后，通过 `mmap()` 函数实现内存映射，并将上述的文件描述符传入。这里的 binder_state 是一个结构体，定义如下。其实就是用来描述 binder 的状态。从上面我们也能看到它的三个变量的赋值过程。\n\n```c\n// platform/framework/native/cmds/servicemanager.c\nstruct binder_state\n{\n    int fd;\n    void *mapped;\n    size_t mapsize;\n};\n```\n\n当然，在上面的代码中，我们又见到了久违的 goto 指令。它们主要用来处理发生一些异常的情况。\n\n打开了驱动之后，注册为上下文的方法更加简单，\n\n```c\n// platform/framework/native/cmds/servicemanager.c\nint binder_become_context_manager(struct binder_state *bs)\n{\n    return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);\n}\n```\n\n就是一个 `ioctl` 函数，使用指令 `BINDER_SET_CONTEXT_MGR` 将当前的 ServiceManager 注册为上下文。\n\n最后就是启动 Binder 循环了。它的逻辑也没有想象中得复杂，就是启动了 for 循环，\n\n```c\n// platform/framework/native/cmds/servicemanager.c\nvoid binder_loop(struct binder_state *bs, binder_handler func)\n{\n    int res;\n    struct binder_write_read bwr;\n    uint32_t readbuf[32];\n\n    bwr.write_size = 0;\n    bwr.write_consumed = 0;\n    bwr.write_buffer = 0;\n\n    readbuf[0] = BC_ENTER_LOOPER;\n    // 将 BC_ENTER_LOOPER 命令发送给 binder 驱动，内部调用 ioctl 函数\n    binder_write(bs, readbuf, sizeof(uint32_t));\n\n    for (;;) {\n        bwr.read_size = sizeof(readbuf);\n        bwr.read_consumed = 0;\n        bwr.read_buffer = (uintptr_t) readbuf;\n        // 使用 iotcl 函数读取\n        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);\n        if (res < 0) {\n            break;\n        }\n        // 解析\n        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);\n        if (res == 0) {\n            break;\n        }\n        if (res < 0) {\n            break;\n        }\n    }\n}\n```\n\n从上面看出，函数将会在 binder_write() 中将命令发送给 Binder 驱动，以启动循环。其实内部也是调用 ioctl 函数实现的。然后程序会启动一个循环来不断读取、解析。这是服务器很典型的操作了。\n\n当然，我们上面分析的是 ServiceManager 中向 Binder 写命令的过程，而驱动如何解析呢？当然是在驱动中实现了，详细的过程可以查看 Binder 驱动部分的源码。\n\n### 4.4 Binder 的跨进程通信过程\n\n下面我们以 AMS 作为例子来讲解下 Binder 跨进程通信的实现过程。首先，当我们调用 `startActivity()` 方法的时候，最终将会进入 ActivityManager 以获取 AMS，\n\n```java\n    // platform/framework/base/core/java/android/app/ActivityManager.java\n    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);\n    final IActivityManager am = IActivityManager.Stub.asInterface(b);\n    return am;\n```\n\n这里会使用 ServiceManger 来按名称查找 AMS，查找到 Binder 对象之后将其转换成 AMS 就可以使用了。之前，我们也说过用来查找 AMS 的 SeerviceManager 本身也是一种服务。所以，它这里的方法也是通过 Binder 来实现的。那么，我们就从这里的 `getService()` 方法入手。\n\n```java\n    // platform/framework/base/core/java/android/os/ServiceManager.java\n    public static IBinder getService(String name) {\n        try {\n            IBinder service = sCache.get(name);\n            if (service != null) {\n                return service;\n            } else {\n                return Binder.allowBlocking(rawGetService(name));\n            }\n        } catch (RemoteException e) { /* ... */ }\n        return null;\n    }\n```\n\n这里会先尝试从缓存当中取 Binder，取不到的话就从远程进行获取。这里使用 `rawGetService()` 方法来从远程获取 Binder，代码如下，\n\n```java\n    // platform/framework/base/core/java/android/os/ServiceManager.java\n    private static IBinder rawGetService(String name) throws RemoteException {\n        final IBinder binder = getIServiceManager().getService(name);\n        // ...\t\t\n        return binder;\n    }\n\n    // platform/framework/base/core/java/android/os/ServiceManager.java\n    private static IServiceManager getIServiceManager() {\n        if (sServiceManager != null) {\n            return sServiceManager;\n        }\n        sServiceManager = ServiceManagerNative\n                .asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));\n        return sServiceManager;\n    }\n```\n\n在 `rawGetService()` 方法中会使用 `ServiceManagerNative` 的 `getService()` 方法从远程获取 Binder. 这里的 ServiceManagerNative 本质上只是一个代理类，它实际的逻辑是由 `BinderInternal.getContextObject()` 返回的 Binder 实现的。\n\n也许你已经晕了，怎么那么多 Binder……我来说明下。当要查找 AMS 的时候实际上是一个跨进程的调用过程，也就是实际的查找的逻辑是在另一个进程实现，因此需要 Binder 来通信。而查找 AMS 的远程对象实际上就是我们上面所说的 ServiceManager (Native 层的而不是 Java 层的，Java 层的 ServiceManager 是一个代理类，是用来从远程获取服务的）。\n\n因此，按照上面的描述，`BinderInternal.getContextObject()` 返回的就应该是远程的 Binder 对象。于是方法进入 Native 层，\n\n```c++\n// platform/framework/base/core/jni/android_util_Binder.cpp\nstatic jobject android_os_BinderInternal_getContextObject(JNIEnv* env, jobject clazz)\n{\n    sp<IBinder> b = ProcessState::self()->getContextObject(NULL);\n    return javaObjectForIBinder(env, b);\n}\n```\n\n这里的 `ProcessState::self()` 是否熟悉呢？你是否还记得在上一篇文章中，我们介绍 Android 系统启动过程的时候介绍过它。我们曾经使用它来开启 Binder 的线程池。这里的 `self()` 方法其实是用来获取一个单例对象的。我们可以直接由 `getContextObject()` 进入 `getStrongProxyForHandle()` 方法。从下面的方法中我们可以看出，这里调用了 `BpBinder` 的 `create()` 方法创建了一个 BpBinder 实例并返回，也就是我们的 ServiceManager.\n\n```c++\n// plaftorm/framework/native/libs/binder/ProcessState.cpp\nsp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)\n{\n    sp<IBinder> result;\n    AutoMutex _l(mLock);\n    handle_entry* e = lookupHandleLocked(handle);\n    if (e != nullptr) {\n        IBinder* b = e->binder;\n        if (b == nullptr || !e->refs->attemptIncWeak(this)) {\n            // ...\n\t\t\t// 调用 BpBinder\n            b = BpBinder::create(handle);\n            e->binder = b;\n            if (b) e->refs = b->getWeakRefs();\n            result = b;\n        } else {\n            result.force_set(b);\n            e->refs->decWeak(this);\n        }\n    }\n```\n\n当我们拿到了 ServiceManager 的 Binder 之后就可以调用它的 `getService()` 方法来获取服务了，\n\n```java\n    // platform/framework/base/core/java/android/os/ServiceManagerNative.java\n    public IBinder getService(String name) throws RemoteException {\n        Parcel data = Parcel.obtain();\n        Parcel reply = Parcel.obtain();\n        data.writeInterfaceToken(IServiceManager.descriptor);\n        data.writeString(name);\n        mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);\n        IBinder binder = reply.readStrongBinder();\n        reply.recycle();\n        data.recycle();\n        return binder;\n    }\n```\n\n这里的 mRemote 就是之前返回的 BpBinder，这里调用它的 `transact()` 方法，并传入了一个方法标记 GET_SERVICE_TRANSACTION. \n\n```c++\n// platform/framework/native/libs/binder/BpBinder.cpp\nstatus_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)\n{\n    if (mAlive) {\n        status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);\n        if (status == DEAD_OBJECT) mAlive = 0;\n        return status;\n    }\n    return DEAD_OBJECT;\n}\n```\n\n显然这里会调用 IPCThreadState 的 `self()` 方法先获取一个单例的对象，然后调用它的 `transact()` 方法继续方法的执行。\n\n```c++\n// platform/framework/native/libs/binder/IPCThreadState.cpp\nstatus_t IPCThreadState::transact(int32_t handle, uint32_t code, \n    const Parcel& data, Parcel* reply, uint32_t flags)\n{\n    status_t err;\n    // ...\n    err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr);\n    // ...\n    if ((flags & TF_ONE_WAY) == 0) { // OneWay 类型的调用，同步的\n        // ...\n\t\tif (reply) {\n            // 等待相应\n            err = waitForResponse(reply);\n        } else {\n            Parcel fakeReply;\n            err = waitForResponse(&fakeReply);\n        }\n        IF_LOG_TRANSACTIONS() {\n            TextOutput::Bundle _b(alog);\n            if (reply) alog << indent << *reply << dedent << endl;\n            else alog << \"(none requested)\" << endl;\n        }\n    } else { // 异步的\n        err = waitForResponse(nullptr, nullptr);\n    }\n    return err;\n}\n```\n\n上面会调用 `writeTransactionData()` 方法用来将数据写入到 Parcel 中。然后将会进入 `waitForResponse()` 方法处理与 ServiceManager 交互的结果。而真实的交互发生的地方位于 `talkWithDriver()` 方法，\n\n```c++\n// platform/framework/native/libs/binder/IPCThreadState.cpp\nstatus_t IPCThreadState::talkWithDriver(bool doReceive)\n{\n    if (mProcess->mDriverFD <= 0) {\n        return -EBADF;\n    }\n\n    binder_write_read bwr;\n    const bool needRead = mIn.dataPosition() >= mIn.dataSize();\n    const size_t outAvail = (!doReceive || needRead) ? mOut.dataSize() : 0;\n\n    bwr.write_size = outAvail;\n    bwr.write_buffer = (uintptr_t)mOut.data();\n\n    if (doReceive && needRead) {\n        bwr.read_size = mIn.dataCapacity();\n        bwr.read_buffer = (uintptr_t)mIn.data();\n    } else {\n        bwr.read_size = 0;\n        bwr.read_buffer = 0;\n    }\n\n    if ((bwr.write_size == 0) && (bwr.read_size == 0)) return NO_ERROR;\n\n    bwr.write_consumed = 0;\n    bwr.read_consumed = 0;\n    status_t err;\n    do {\n        // 通过 ioctl 读写操作，与 Binder Driver 进行交互\n        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)\n            err = NO_ERROR;\n        else\n            err = -errno;\n        if (mProcess->mDriverFD <= 0) {\n            err = -EBADF;\n        }\n    } while (err == -EINTR);\n    // ...\n    return err;\n}\n```\n\nbinder_write_read 结构体用来与 Binder 设备交换数据的结构, 通过 ioctl 与 mDriverFD 通信，是真正与 Binder 驱动进行数据读写交互的过程。先向service manager进程发送查询服务的请求(BR_TRANSACTION)。然后，service manager 会在之前开启的循环中监听到，并使用 `svcmgr_handler()` 方法进行处理。\n\n```c++\n// platform/framework/native/cmds/servicemanager.c\nint svcmgr_handler(struct binder_state *bs,\n                   struct binder_transaction_data *txn,\n                   struct binder_io *msg,\n                   struct binder_io *reply)\n{\n    // ...\n    switch(txn->code) {\n        case SVC_MGR_GET_SERVICE:\n        case SVC_MGR_CHECK_SERVICE:\n            s = bio_get_string16(msg, &len);\n            if (s == NULL) {\n                return -1;\n            }\n            handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid);\n            if (!handle)\n                break;\n            bio_put_ref(reply, handle);\n            return 0;\n        case SVC_MGR_ADD_SERVICE: // ...\n        case SVC_MGR_LIST_SERVICES: // ...\n    }\n    return 0;\n}\n```\n\n显然，这里会从 binder_transaction_data 中取出 code，即 SVC_MGR_GET_SERVICE，然后使用 `do_find_service()` 方法查找服务。然后再 binder_send_reply() 应答发起者将结果返回即可。\n\n### 4.5 Binder 高效通信的原因\n\n上面我们梳理了 Binder 通信的过程，从上面我们似乎并没有看到能证明 Binder 高效的证据。那么 Binder 究竟靠什么实现高效的呢？\n\n实际上，Binder 之所以高效，从我们上面的代码还真看不出来。因为，我们上面的代码并没有涉及 Binder 驱动部分。正如我们之前描述的那样，ServiceManager、客户端和服务器实际是靠 Binder 驱动这个中间媒介进行交互的。而 Binder 高效的地方就发生在 Binder 驱动部分。\n\n![图片来自-写给 Android 应用工程师的Binder原理剖析-https://zhuanlan.zhihu.com/p/35519585](res/binder_diver.jpg)\n\n> 图片来自 [《写给 Android 应用工程师的Binder原理剖析》](https://zhuanlan.zhihu.com/p/35519585)\n\n就像图片描述的那样，当两个进程之间需要通信的时候，Binder 驱动会在两个进程之间建立两个映射关系：内核缓存区和内核中数据接收缓存区之间的映射关系，以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。这样，当把数据从 1 个用户空间拷贝到内核缓冲区的时候，就相当于拷贝到了另一个用户空间中。这样只需要做一次拷贝，省去了内核中暂存这个步骤，提升了一倍的性能。实现内存映射靠的就是上面的 `mmap()` 函数。\n\n## 5、Binder 的使用\n\n### 5.1 代理模式\n\n`Binder` 本质上只是一种底层通信方式，和具体服务没有关系。为了提供具体服务，`Server` 必须提供一套接口函数以便 `Client` 通过远程访问使用各种服务。这时通常采用**代理设计模式**：将接口函数定义在一个抽象类中，`Server` 和 `Client` 都会以该抽象类为基类实现所有接口函数。所不同的是 `Server` 端是真正的功能实现，而 `Client` 端是对这些函数远程调用请求的包装。为了简化这种设计模式，Android 中提供了 AIDL 供我们使用。下文中我们会介绍 AIDL 相关的内容以及它的一些基本的使用方式。\n\n### 5.2 AIDL\n\n`AIDL (Android Interface Definition Language，Android 接口定义语言)` 是一种文件格式，用来简化 Binder 的使用。当使用 Binder 的时候，只需要创建一个后缀名为 `.aidl` 的文件，然后像定义接口一样定义方法。定义完毕之后，使用工具 `aidl.exe` 即可生成 Binder 所需要的各种文件。当然，我们的 AS 已经为我们集成了 `aidl.exe`，所以，只需要在定义了 AIDL 文件之后，**编译**即可生成使用 `Binder` 时所需的文件。当然，不使用 AIDL，直接编写 Binder 所需的 java 文件也是可以的。\n\nAIDL 是一种接口定义语言，它与 Java 中定义接口的方式有所区别。下面我们通过一个例子来说明 AIDL 的使用方式。\n\n这里我们模拟一个笔记管理的类，通过在 Activity 中与一个远程的 Service 进行交互来实现 IPC 的效果。这里，我们先要定义数据实体 Note，它只包含两个字段，并且实现了 Parcelable。这里 Note 所在的目录是 `me.shouheng.advanced.aidl`，然后，我们需要在 `src/main` 建立一个同样的包路径，然后定义所需的 AIDL 文件：\n\n```java\n    // INoteManager.aidl\n    package me.shouheng.advanced.aidl;\n    import me.shouheng.advanced.aidl.Note;\n    interface INoteManager {\n        Note getNote(long id);\n        void addNote(long id, String name);\n    }\n\n    // Note.aidl\n    package me.shouheng.advanced.aidl;\n    parcelable Note;\n```\n\n注意，在 INoteManager 文件中，我们定义了远程服务所需的各种方法。这里只定义了两个方法，一个用来获取指定 `id` 的笔记，一个用来向远程服务中添加一条笔记记录。\n\n这样定义完了之后，我们可以对项目进行编译，这样就可以 build 目录下面得到为我们生成好的 INoteManager 类文件。以后，我们就可以使用这个文件中生成类和方法来进行远程通信。但在使用该接口之前，我们还是先来看一下其中都生成了些什么东西：\n\n```java\npackage me.shouheng.advanced.aidl;\n\npublic interface INoteManager extends android.os.IInterface {\n\n    // 交给远程来实现具体的业务逻辑\n    public static abstract class Stub extends android.os.Binder implements me.shouheng.advanced.aidl.INoteManager {\n\n        private static final java.lang.String DESCRIPTOR = \"me.shouheng.advanced.aidl.INoteManager\";\n\n        public Stub() {\n            this.attachInterface(this, DESCRIPTOR);\n        }\n\n        // 使用代理包装远程对象\n        public static me.shouheng.advanced.aidl.INoteManager asInterface(android.os.IBinder obj) {\n            if ((obj==null)) {\n                return null;\n            }\n            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);\n            if (((iin!=null)&&(iin instanceof me.shouheng.advanced.aidl.INoteManager))) {\n                return ((me.shouheng.advanced.aidl.INoteManager)iin);\n            }\n            // 返回代理对象\n            return new me.shouheng.advanced.aidl.INoteManager.Stub.Proxy(obj);\n        }\n\n        @Override\n        public android.os.IBinder asBinder() {\n            return this;\n        }\n\n        // 真实地发送数据交换的地方\n        @Override\n        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {\n            switch (code) {\n                case INTERFACE_TRANSACTION: {\n                    reply.writeString(DESCRIPTOR);\n                    return true;\n                }\n                case TRANSACTION_getNote: {\n                    data.enforceInterface(DESCRIPTOR);\n                    long _arg0;\n                    _arg0 = data.readLong();\n                    // 使用模板方法来实现业务\n                    me.shouheng.advanced.aidl.Note _result = this.getNote(_arg0);\n                    reply.writeNoException();\n                    if ((_result!=null)) {\n                        reply.writeInt(1);\n                        _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);\n                    } else {\n                        reply.writeInt(0);\n                    }\n                    return true;\n                }\n                case TRANSACTION_addNote: {\n                    data.enforceInterface(DESCRIPTOR);\n                    long _arg0;\n                    _arg0 = data.readLong();\n                    java.lang.String _arg1;\n                    _arg1 = data.readString();\n                    // 使用模板方法来实现业务\n                    this.addNote(_arg0, _arg1);\n                    reply.writeNoException();\n                    return true;\n                }\n            }\n            return super.onTransact(code, data, reply, flags);\n        }\n\n        // 代理对象，包装了远程对象，内部调用远程对象获取远程的服务信息\n        private static class Proxy implements me.shouheng.advanced.aidl.INoteManager {\n\n            private android.os.IBinder mRemote;\n\n            Proxy(android.os.IBinder remote) {\n                mRemote = remote;\n            }\n\n            @Override\n            public android.os.IBinder asBinder() {\n                return mRemote;\n            }\n\n            public java.lang.String getInterfaceDescriptor() {\n                return DESCRIPTOR;\n            }\n\n            @Override\n            public me.shouheng.advanced.aidl.Note getNote(long id) throws android.os.RemoteException {\n                android.os.Parcel _data = android.os.Parcel.obtain();\n                android.os.Parcel _reply = android.os.Parcel.obtain();\n                me.shouheng.advanced.aidl.Note _result;\n                try {\n                    _data.writeInterfaceToken(DESCRIPTOR);\n                    _data.writeLong(id);\n                    // 实际内部调用远程对象，在另一个进程实现业务逻辑\n                    mRemote.transact(Stub.TRANSACTION_getNote, _data, _reply, 0);\n                    _reply.readException();\n                    if ((0!=_reply.readInt())) {\n                        _result = me.shouheng.advanced.aidl.Note.CREATOR.createFromParcel(_reply);\n                    } else {\n                        _result = null;\n                    }\n                } finally {\n                    _reply.recycle();\n                    _data.recycle();\n                }\n                return _result;\n            }\n\n            @Override\n            public void addNote(long id, java.lang.String name) throws android.os.RemoteException {\n                android.os.Parcel _data = android.os.Parcel.obtain();\n                android.os.Parcel _reply = android.os.Parcel.obtain();\n                try {\n                    _data.writeInterfaceToken(DESCRIPTOR);\n                    _data.writeLong(id);\n                    _data.writeString(name);\n                    // 实际内部调用远程对象，在另一个进程实现业务逻辑\n                    mRemote.transact(Stub.TRANSACTION_addNote, _data, _reply, 0);\n                    _reply.readException();\n                } finally {\n                    _reply.recycle();\n                    _data.recycle();\n                }\n            }\n        }\n\n        // 方法 id，用来标记当前调用的是哪个方法\n        static final int TRANSACTION_getNote = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);\n        static final int TRANSACTION_addNote = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);\n    }\n\n    public me.shouheng.advanced.aidl.Note getNote(long id) throws android.os.RemoteException;\n\n    public void addNote(long id, java.lang.String name) throws android.os.RemoteException;\n}\n```\n\n如果只是看这上面的生成的代码，也许你仍然无法了解这些生成的类究竟有什么作用。下面就让我们通过使用上面生成的类来说明 AIDL 的具体工作流程。\n\n首先，我们要定义远程的服务，并在该服务中实现业务逻辑：\n\n```java\npublic class NoteService extends Service {\n\n    private CopyOnWriteArrayList<Note> notes = new CopyOnWriteArrayList<>();\n\n    // 当前服务运行于另一个进程，这里实现业务逻辑\n    private Binder binder = new INoteManager.Stub() {\n        @Override\n        public Note getNote(long id) {\n            return Observable.fromIterable(notes).filter(note -> note.id == id).singleOrError().blockingGet();\n        }\n\n        @Override\n        public void addNote(long id, String name) {\n            notes.add(new Note(id, name));\n        }\n    };\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        notes.add(new Note(100, \"Note 100\"));\n        notes.add(new Note(101, \"Note 101\"));\n    }\n\n    // 将 binder 返回，客户端可以使用连接来获取并调用\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return binder;\n    }\n}\n```\n\n这里在 `onCreate()` 方法中创建了两条记录，并且创建了 `INoteManager.Stub` 的实例，并在 `onBind()` 方法中将其返回。然后，我们在一个 `Activity` 中启动该远程服务，并尝试从该服务中获取指定 `id` 的笔记记录。从期望的结果来看，它的功能有些类似于 `ContentProvider`，即用来向调用者提供数据。\n\n下面是该 `Activity` 的实现。这里我们在 `onCreate()` 方法中启动上述服务。并将实例化的 `ServiceConnection` 作为参数启动该服务。在 `ServiceConnection` 的方法中，我们调用 `INoteManager.Stub` 的 `asInterface(IBinder)` 方法来讲 `service` 转换成 `INoteManager`，然后从其中获取指定 `id` 的笔记记录即可。\n\n```java\n    // 创建服务连接\n    private ServiceConnection connection = new ServiceConnection() {\n        @Override\n        public void onServiceConnected(ComponentName name, IBinder service) {\n            // 返回代理对象\n            INoteManager noteManager = INoteManager.Stub.asInterface(service);\n            try {\n                // 使用代理对象\n                Note note = noteManager.getNote(100);\n                LogUtils.d(note);\n            } catch (RemoteException e) {\n                e.printStackTrace();\n            }\n        }\n\n        @Override\n        public void onServiceDisconnected(ComponentName name) { }\n    };\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        Intent intent = new Intent(this, NoteService.class);\n        // 绑定服务\n        bindService(intent, connection, Context.BIND_AUTO_CREATE);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        // 解绑服务\n        unbindService(connection);\n    }\n}\n```\n\n根据 `INoteManager.Stub` 的 `asInterface()` 方法的定义，该方法中会将传入的 `service` 包装成一个 `INoteManager.Stub.Proxy` 返回，所以，我们在 `onServiceConnected()` 方法中实际调用的是该代理类的 `getNote()` 方法。而该代理类的 `getNote()` 方法中又调用了传入的 `mRemote.transact()` 方法。而这里的 `service` 正是我们在 `NoteService` 中创建的 `binder`。也就是说，当我们在 `onServiceConnected()` 中调用 `getNote()` 方法的时候，实际上调用了 `INoteManager.Stub` 的 `transact()` 方法。\n\n所以，从上面我们看出：\n\n1. 这里就像是在当前进程中调用了另一个进程的方法一样。这个调用的过程是通过 `Binder` 来实现的。\n2. 当调用 `INoteManager.Stub` 的 `transact()` 方法的时候，通过传入了一个整型的 `code` 来作为要触发的方法的标识，这就是我们上面提到的方法的编号。\n\n于是，我们可以通过下面的这张图来总结在上面使用 AIDL 的过程中各部分扮演的角色：\n\n![AIDL](res/AIDL.png)\n\n也就是客户端通过 `Proxy` 访问 `Binder` 驱动，然后 `Binder` 驱动调用 `Stub`，而 `Stub` 中调用我们的业务逻辑。这里的 `Proxy` 和 `Stub` 用来统一接口函数，`Proxy` 用来告诉我们远程服务中有哪些可用的方法，而具体的业务逻辑则由 `Stub` 来实现。`Binder` 的进程通信就发生在 `Proxy` 和 `Stub` 之间。\n\n## 总结\n\n以上就是 Binder 的工作原理，如有疑问，欢迎评论区交流。\n\n### 参考资料\n\n[Android Bander设计与实现 - 设计篇](https://blog.csdn.net/universus/article/details/6211589)\n\n**源代码**：[Android-references](https://github.com/Shouheng88/Android-references/tree/master/advanced)"
  },
  {
    "path": "混合开发/ReactNative.md",
    "content": "\n### Text\n\n1.塢ɫֶ뷽ʽ\n\n```\n<Text style={{fontSize:20, color:'red', {textAlign:'center'}}/>\n```\n\n\n2.\n\n```\n<View style={{height:100, width:100, backgroundColor:'orange', borderRadius:5}}/>\n```\n\n3.ԲǱ\n\n```\n<View style={{height:100, width:100, backgroundColor:'orange', borderRadius:5}}>\n  <Text style={{flex:1,backgroundColor:'transparent'}}>Text Content</Text>\n</View>\n```\n\n## Flex \n\n### flex-direction\n\n![flex-direction](res/flex-direction.jpg)\n\nstyleָ`flexDirection`Ծֵᡣ\nԪӦˮƽ(`row`)Уֱ(`column`)أĬֱֵ(`column`)\n\nӣ[flex-direction](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction)\n\n### align-conent\n\n![align-conent](res/align-conent.jpg)\n\nӣ[align-content](https://developer.mozilla.org/en-US/docs/Web/CSS/align-content)\n\n### align-items\n\n![align-items](res/align-items.jpg)\n\nstyleָ`alignItems`ԾԪŴᣨᴹֱᣬ᷽Ϊ`row`᷽Ϊ`column`зʽ\nԪӦÿʼ˻ĩβηֲأӦþȷֲӦЩѡУ`flex-start``center``flex-end`Լ`stretch`\n\nע⣺Ҫʹ`stretch`ѡЧĻԪڴ᷽ϲй̶ĳߴ硣\n\nӣ[align-items](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items)\n\n### justify-content\n\n![justify-content](res/justify-content.jpg)\n\nstyleָ`justifyContent`ԾԪзʽ\nԪӦÿʼ˻ĩβηֲأӦþȷֲӦЩѡУ`flex-start``center``flex-end``space-around`Լ`space-between`\n\nӣ[justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content)\n\n### flex-wrap\n\n![flex-wrap](res/flex-wrap.jpg)\n\nӣ[flex-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap)\n\n### \n\n```\n<view>\n    <view style=\"{{height:100,width:100,backgroundColor:'#eee',alignItems:'center'}}\">\n        <text>ˮƽ</text>\n    </view>\n    <view style=\"{{height:100,width:100,backgroundColor:'green',justifyContent:'center'}}\">\n        <text>ֱ</text>\n    </view>\n    <view style=\"{{height:100,width:100,backgroundColor:'red',alignItems:'center',justifyContent:'center'}}\">\n        <text>ˮƽֱ</text>\n    </view>\n</view>\n```\n\n## \n\n### ߿\n\n`borderWidth` `borderTopWidth` `borderRightWidth` `borderLeftWidth` `borderBottomWidth`\n\nCSSϵ`border-width`ȱһ\n\n### flex\n\nflexΪֵᰴձƿؼĴСЧAndroid`layoutWeight`\n\n### left right top \n\nleftֵָλ߶ٸ߼أߵĶȡpositionԣ\n\nıֺCSSϵleftƣעReact Nativeֻʹ߼ֵֵλʹðٷֱȡemκλ\n\nhttps://developer.mozilla.org/en-US/docs/Web/CSS/left\n\n### margin\n\nmargin marginBottom marginLeft marginRight marginTop marginVertical marginHorizontal \n\n### maxHeight maxWidth minHeight minWidth width \n\n### padding\n\npaddingBottom paddingHorizontal paddingLeft paddingRight paddingTop paddingVertical\n\n### position\n"
  },
  {
    "path": "笔试面试/Android高级软件工程师2017.md",
    "content": "链接们：\n\n[11](https://www.jianshu.com/p/a2bbd8565a64)    \n\n[22](https://blog.csdn.net/WHB20081815/article/details/74436204)\n\n[android面试题-与IPC机制相关面试题](https://blog.csdn.net/mwq384807683/article/details/70313632)\n\n[分享一份非常强势的Android面试题](http://www.jcodecraeer.com/plus/view.php?aid=12303)    \n\n[Android面试一天一题](https://blog.csdn.net/wo_ha/article/details/79729873)\n\n[2018年Android面试题含答案--适合中高级（上）](https://www.cnblogs.com/huangjialin/p/8657565.html)\n\n[Android-Interview](https://github.com/android-exchange/Android-Interview)\n\n## 算法\n\n- [ ] 排序，快速排序的实现\n- [ ] 树：B 树、B+ 树的介绍\n- [ ] 图：有向无环图的解释\n- [ ] 二叉树 深度遍历与广度遍历\n- [ ] 常用数据结构简介\n- [ ] 判断环（猜测应该是链表环）\n- [ ] 排序，堆排序实现\n- [ ] 链表反转\n- [ ] x 个苹果，一天只能吃一个、两个、或者三个，问多少天可以吃完\n- [ ] 堆排序过程，时间复杂度，空间复杂度\n- [ ] 快速排序的时间复杂度，空间复杂度\n- [ ] 翻转一个单项链表\n- [ ] 两个不重复的数组集合中，求共同的元素\n- [ ] 上一问扩展，海量数据，内存中放不下，怎么求出\n- [ ] 合并多个单有序链表（假设都是递增的）\n- [ ] 算法判断单链表成环与否？\n- [ ] 二叉树，给出根节点和目标节点，找出从根节点到目标节点的路径\n- [ ] 一个无序，不重复数组，输出 N 个元素，使得 N 个元素的和相加为 M，给出时间复杂度、空间复杂度。手写算法\n- [ ] 数据结构中堆的概念，堆排序\n\n常见的算法题！！！！\n\n参考：\n\n1. 整理自：[知乎 Misssss Cathy 的回答](https://zhuanlan.zhihu.com/p/30016683)\n\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_10_跨平台开发.md",
    "content": ""
  },
  {
    "path": "笔试面试/Android高级面试_11_JNINDK.md",
    "content": "\n\n\n\n- [ ] Jni 用过么？\n\n\n- [ ] 是否熟悉 Android jni 开发，jni 如何调用 java 层代码\n"
  },
  {
    "path": "笔试面试/Android高级面试_12_各种三方库分析.md",
    "content": "# Android 高级面试：三方库源码分析\n\n## 1、ARouter\n\n既然使用的时候需要 AnnotationProcessor，那么说明它是基于注解处理的，也就是编译期间根据注解动态生成代码，当然生成的代码要符合既定的规则，然后根据规则使用反射加载到内存中。ARouter 在编译期间生成路由到指定的类的对应的关系，当然包括类的类型信息：\n\n```java\npublic class ARouter$$Group$$app implements IRouteGroup {\n  @Override\n  public void loadInto(Map<String, RouteMeta> atlas) {\n    atlas.put(\"/app/intro\", RouteMeta.build(RouteType.ACTIVITY, AppIntroActivity.class, \"/app/intro\", \"app\", null, -1, -2147483648));\n  }\n}\n```\n\n然后当调用 `navigation()` 方法的时候，根据路由字符串从映射关系中取出对应的类。然后根据类的类型选择指定类型的启动方式。当然，本质上还是采用 Intent 来进行启动的。\n\n在模块化开发的时候这些自动生成的类被分散到各个模块里，这样各个模块之前的页面通过路由相互引用而不是类本身。因为如果是类本身进行引用的话，会因为类的引用找不到而导致编译失败。使用字符串映射就可以将各个模块编译成独立的 APK。而组件开发完毕之后，统一打包之后，就将所有的映射关系统一放进一个 APK 里，然后就可以在当前包中根据路由找到类并使用。\n\nARouter 的另一个功能是实现变量的自动注入，这个功能的实现原理和 ButterKnife 的实现相似。早期的版本是基于 Hook 来实现的，但是后来被废弃了，转而采用了使用注解的方式。它本质上依赖于我们在类内部调用 `inject()` 方法。当我们调用这个方法的时候，ARouter 会根据类名和指定的字符串拼接起来找到生成的类，这个生成的类会在指定的方法中为我们 Activity 或者 Fragment 的字段进行赋值。因为我们不会为了注入的变量提供 setter 方法，所以通常这类注入至少要保证该变量是包级别的访问权限。这也是为什么很多注入框架的要注入的变量都要求是包级别访问权限的原因。\n\n## 2、图片加载框架\n\n**问题：Glide 源码？**   \n**问题：Glide 使用什么缓存？**    \n**问题：Glide 内存缓存如何控制大小？**   \n**问题：图片框架实现原理？**   \n\n1. `Glide 的请求的生命周期的控制`：它使用没有 UI 的 Fragment 来管理 Glide 的生命周期。RxPermission 和 LiveData 的早期的版本都是使用这种方式来解决的。把一个回调设置到 Fragment 中，然后在它的生命周期方法中回调这个回调接口的方法。\n\n2. 当我们第一次使用 Glide 发起请求的时候会实例化一个`单例的 Glide`，在初始化 Glide 的过程中会调用我们的自定义 `GlideModule`. GlideModule 的方法提供了构建者供我们使用，拿到了这个构建者之后，我们就可以使用它的方法进行自定义配置。GlideModule 的方法还提供了用来向 Glide 中注册自定义类型的文件的加载方式的 Factory。它们会以映射的方式存储到 Glide 中。当加载自定义类型图片的时候，会根据映射关系，先拿到 Factory，然后从中获取加载该图片的 Loader，并使用它来加载数据。对于自定义的类型，如何对其进行缓存，缓存的 key 如何设置（这影响缓存的命中），都可以通过 GlideModule 来完成。\n\n3. 图片加载的核心流程位于 `DecodeJob 和 EngineJob`, 它们之间的关系是 DecodeJob 用来完成图片的加载、解密等工作；EngineJob 内部维护了一个线程池用来对 DecodeJob 进行调度。另外，对于请求信息都被包装到 Request 对象中，请求的过程也发生在 Request 对象中。用来显式图片的对象则需要继承 Target. 图片加载的逻辑位于 Request，在 SingleRequest 中使用了状态模式，因为图片加载可能中断、恢复和完成等，所以可以根据当前的状态做相应处理。图片是从缓存中进行加载还是从原始的数据源中进行加载也是在状态模式中进行控制的。此外，还包括从原始的数据源加载完毕之后，缓存到磁盘上面的过程，也是在该状态模式中进行处理的。\n\n4. 在默认配置中，当 Glide 进行图片加载的时候会先从缓存当中进行获取。Glide 采用了多种缓存，包括两种内存缓存和一种磁盘缓存。首先，图片会先从弱引用的内存缓存中获取数据。弱引用的缓存会因为内存不足而回收；当若引用中获取不到的时候，再从 LruCache 的内存缓存中获取数据。当内存缓存中获取不到的时候，会尝试从磁盘缓存中获取数据。它会使用图片的参数构建一个 Key 用来从磁盘上面读取图片。这里去从磁盘上面读取数据的时候也会使用与文件类型映射的 Loader 来从磁盘上面加载数据。当磁盘上面找不到缓存的时候，则回到之前的状态模式，从原始数据源中获取数据。当获取到数据之后，再使用磁盘缓存将其缓存到磁盘上面。\n5. 当 Glide 使用 Loader 加载数据之后会得到一个 InputStream，然后再经过层层回调使用 BitmapFactory 的 `decode()` 方法从该流中加载 Bitmap. 然后就是对其进行变换，并将其显式到控件上面的过程。\n\nGlide 的缓存控制，可以在自定义 GlideModule 的时候，通过构建者的方法对其进行配置。然后，可以在每次请求的时候，使用请求的构建者方法对本次请求的缓存策略进行配置。\n\n**问题：对 Bitmap 对象的了解？**      \n**问题：Bitmap 使用时候注意什么？**   \n\nBitmap 占用内存大小的计算，首先可以通过 `getByteCount()` 和 `getAllocationByteCount()` 获取到占用内存的大小。Bitmap 占用内存的大小可以通过 `像素数量*每个像素占用的字节` 得到。每个像素占用的字节可以通过枚举 Config 指定，常用的有下面四种：\n\n1. `ARGB_8888`：每个像素占`四个字节`，A、R、G、B 分量各占 8 位，是 Android 的默认设置；\n2. `RGB_565`：每个像素占`两个字节`，R 分量占 5 位，G 分量占 6 位，B 分量占 5 位；\n3. `ARGB_4444`：每个像素占`两个字节`，A、R、G、B 分量各占 4 位，成像效果比较差；\n5. `Alpha_8`: 只保存透明度，共 8 位，`1 字节`；\n\n所以问题在于如何得到像素的数量，`像素总数=宽度像素*高度像素`。这里的宽度和高度的像素又根据资源的位置分成两种情况，当资源位于网络或者磁盘上面的时候，通过 `decode()` 加载到内存中的时候，占用内存大小只与图片本身有关，与设备屏幕密度无关，此时 `占用内存=实际显示的宽 * 实际显示的高 * Bitmap.Config`。但如果资源是位于 drawable 文件夹下面，则会根据资源所处的位置和屏幕密度发生改变，此时计算公式如下：\n\n```java\n// 这里：\n// 1. width 和 height 是原素材大小； \n// 2. targetDensity 是设备像素密度； \n// 3. density 是素材所在 drawable 文件夹大小；\n\nint scaledWidth = (int) (width * targetDensity / density + 0.5f) \nint scaledHeight = (int) (height * targetDensity / density + 0.5f) \nint size = scaledWidth * scaledHeight * Bitmap.Config\n\n// 屏幕的像素密度可以通过下面的方法获取\nDisplayMetrics metric = new DisplayMetrics();\nint targetDensity = metric.densityDpi;\n// 而 density 的对应关系是：mdpi->160dp (默认), hdpi->240dp, xhdpi->320dp, xxhdpi->480dp, xxxhdpi->640dp\n```\n\n所以，同一种图片方的文件夹的像素密度越大，则占用内存越小。另外，对于 jpg 和 png，同一尺寸的图片被加载到内存之后占用的内存是相同的，但是在磁盘上面占用的内存是不同的。因此，为减小 apk 体积，建议使用 jpg.\n\n**问题：Bitmap recycler 相关**    \n\n从 3.0 开始，Bitmap 像素数据和 Bitmap 对象一起存放在 Dalvik 堆中，而在 3.0 之前，Bitmap 像素数据存放在 Native 内存中。所以，在 3.0 之前，Bitmap 像素数据在 Nativie 内存的释放是不确定的，容易内存溢出而 Crash，官方强烈建议调用 `recycle()`（当然是在确定不需要的时候）；而在 3.0 之后，则无此要求。（*参考：[Managing Bitmap Memory](https://developer.android.com/topic/performance/graphics/manage-memory)*）\n\n**问题：图片加载原理？**    \n**问题：图片压缩原理？**\n\nAndroid 平台上面图片压缩有两种方式：`质量压缩`和`尺寸压缩`，其中尺寸压缩又分为`邻近采样`和`双线性采样`两种方式。在把图片从磁盘上面加载到内存中的时候需要使用 BitmapFactory 的 `decode()` 方法加载指定类型的资源，得到一个 Bitmap. 在调用 `decode()` 方法的时候需要使用 `BitmapFactory.Option` 指定采样的比例。为了计算采样率，你需要在真正地 `deocde()` 资源之前得到图片的尺寸。可以通过设置 `decode()` 方法的 `Options` 的 `inJustDecodeBounds` 字段为 false 来实现。真正加载的时候再设置其为 true. 这样进行采样的时候就进行了第一次压缩，叫做邻近采样。然后，对得到的 Bitmap 实例调用 `compress()` 方法，并指定一个图片的质量，通常是在 0~100 之间。这样就可以对图片的质量进行压缩，质量的值越大，图片质量越高，图片也越大。最后是双线性压缩，我们可以调用 Bitmap 的 `createScaledBitmap()` 或者调用 `createBitmap()` 并传入一个 Matrix 来实现。双线性压缩的好处是可以得到一个图片的固定的大小。\n\n那么，在实际的使用过程中，我们可以根据我们的应用场景选择适合的压缩方式。比如，在我们的场景中，先使用尺寸压缩得到 Bitmap. 因为 Bitmap 太大的话，加载到内存中可能会发生 OOM. 然后，可以再使用双线性压缩把图片的尺寸压缩到固定的大小。最后就是使用质量压缩，通过降低图片质量来把降低图片的大小。\n\nAndroid 中加载图片使用的就是 BitmapFactory 的一系列 `decode()` 方法，包括 Glide 内部也是从各种流中获取到一个 Bitmap. 这些 `decode()` 方法会调用 Native 的方法。Native 方法中的实现引用了 Skia 来实现。Skia 是谷歌的一个 2D 图像库，用 C++ 实现。它提供了适用于多种应用平台的公共 API. 并作为 Chrome, Android 等的图片处理引擎。\n\nSkia 本身提供了基本的画图和编解码功能，它同时还挂载了其他第三方编解码库，例如：libpng.so、libjpeg.so、libgif.so. 指定类型的图片将交给指定类型的编码库来完成。\n\nSkia 的资料：[官方网址](https://skia.org/)、[Github 地址](https://github.com/google/skia) 以及[在 Android 源码中的位置](https://android.googlesource.com/platform/external/skia/)。\n\n**问题：大图加载**    \n**问题：图片加载库相关，Bitmap 如何处理大图，如一张 30M 的大图，如何预防 OOM**    \n\n使用 BitmapRegionDecoder 来部分加载图片。加载图片的时候调用下面的方法即可，\n\n```java\npublic Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)\n```\n\n在控件的 `onDraw()` 方法中进行绘制，然后使用 GestureDetector 来检查手势，当移动图片的时候调用 `invalid()` 方法进行重绘即可。\n\n**问题：几种图片加载框架的对比**\n\n1. Glide 比 Picasso `包体积`更大，方法数更多，使用的时候需要开启混淆。\n2. Glide 相比 Picasso 的一大优势是它可以和 Activity 以及 Fragment 的`生命周期`相互协作。\n3. Picasso 将图片下载后会`不经压缩直接将图片整个缓存到磁盘`中，当需要用到图片时，它会直接返回这张完整大小的图片，并在运行时根据 ImageView 的大小作适配。而 Glide 会综合要显式的图片的尺寸和变换等信息，构建一个缓存的 Key，然后将变换之后的图片缓存。此外，它还提供了可配置的缓存策略。\n4. 在加载同样配置的图片时，`Glide 内存占用`更少，这从前面的讨论中其实可以猜测到了，Picasso 是将完整大小的图片加载进内存，然后依赖 GPU 来根据 ImageView 的大小来适配并渲染图片，而 Glide 是针对每个 ImageView 适配图片大小后再存储到磁盘的，这样加载进内存的是压缩过的图片，内存占用自然就比 Picasso 要少。\n5. `图片加载的耗时`：Picasso 相比 Glide 要快很多。可能的原因是 Picasso 下载完图片后直接将整个图片加载进内存，而 Glide 还需要针对每个 ImageView 的大小来适配压缩下载到的图片，这个过程需要耗费一定的时间。\n6. Glide 独有的特性：对 `GIF 动画`的支持，`缩略图`的支持。\n7. `缓存格式`：默认使用 ARGB_8888 格式缓存图片, 缓存体积大；Glide 默认使用 RGB565.\n\nFresco - Facebook：\n\n1. 图片存储在安卓系统的`匿名共享内`存, 而不是`虚拟机的堆内存`中, 图片的中间缓冲数据也存放在本地堆内存, 所以, 应用程序有更多的内存使用，不会因为图片加载而导致 OOM, 同时也减少垃圾回收器频繁调用回收 Bitmap 导致的界面卡顿，性能更高。\n2. 图片可以以任意的中心点显示在 ImageView, 而不仅仅是图片的中心。\n3. `JPEG 图片改变大小也是在 native 进行的`, 不是在虚拟机的堆内存, 同样减少 OOM；\n4. 很好的支持 GIF 图片的显示\n\n缺点: 1). `框架较大`, 影响Apk体积；2). `使用较繁琐`。\n\nImageLoader：\n\n1. 支持下载进度监听\n2. 可以在 View 滚动中暂停图片加载 \n3. 默认实现多种内存缓存算法这几个图片缓存都可以配置缓存算法，不过 ImageLoader 默认实现了较多缓存算法，如 Size 最大先删除、使用最少先删除、最近最少使用、先进先删除、时间最长先删除等。\n4. 支持本地缓存文件名规则定义\n\n缺点: 缺点在于`不支持 GIF` 图片加载,  `缓存机制`没有和 http 的缓存很好的结合, 完全是自己的一套缓存机制。\n\n## 3、EventBus\n\n**问题：EventBus 作用，实现方式，代替 EventBus 的方式**\n**问题：EventBus 实现原理？**\n\nEventBus 事件发布-订阅总线，它简化了应用程序内各个组件之间进行通信的复杂度，尤其是碎片之间进行通信的问题，可以避免由于使用广播通信而带来的诸多不便。\n\n```java\n// 收消息\npublic class EventBusActivity1 extends CommonActivity<ActivityEventBus1Binding> {\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        EventBus.getDefault().unregister(this);\n    }\n\n    @Subscribe(threadMode = ThreadMode.MAIN)\n    public void onGetMessage(MessageWrap message) {\n        getBinding().tvMessage.setText(message.message);\n    }\n}\n\n// 发消息：\nEventBus.getDefault().post(MessageWrap.getInstance(msg));\n```\n\n当调用 `register()` 方法的时候会先获取到调用这个方法的类，然后根据类名，从缓存当中获取之前解析过的信息，否则会对类进行解析。这里解析的内容主要是类的方法，它会将使用了订阅注解的方法信息、注解内容等封装成一个类。这里的注册类到方法列表的映射缓存是通过 ConcurrentHashMap 类实现的。\n\n拿到了方法的列表之后，就通过循环对每个方法信息调用 `subscribe()` 执行订阅的逻辑。订阅的时候主要做了几件事情，第一它通过 CopyOnWriteArrayList 维护事件类型到观察者的订阅关系，根据事件的优先级与观察者的优先级，决定哪个观察者应该进入到这个事件的对应的观察者列表中。然后，它要构建观察者到观察者的事件类型的映射关系。最后，它根据订阅的事件是否存在于粘性事件的映射表中，来决定是否应该触发粘性事件。（刚订阅的时候，需要把粘性事件的结果通知给订阅的对象。）\n\n当 post() 消息的时候，它会先获取当前线程对应的状态信息，该状态信息中维护了一个事件队列，当 post() 信息的时候，不断循环从这个队列中取出消息并继续分发。它会先从上述的事件-观察者映射列表中取出所有观察者，然后继续分发事件。在分发事件的时候会根据事件的线程的状态，分成几种情况来进行处理。如果事件指定的线程与当前线程相同，那么直接触发方法即可（反射），否则如果要到主线程当中触发，就会在底层使用 Handler 将消息发送到主线程进行处理；如果是异步触发，则把反射的操作放在线程池当中执行。\n\n## 4、RxJava\n\n**问题：RxJava 的作用，优缺点**    \n**问题：RxJava 的作用，与平时使用的异步操作来比，优势**    \n**问题：RxJava 简介及其源码解读？**\n\n虽然 RxJava 的功能非常强大，但是其核心的实现却仅仅依赖两个设计模式，一个是观察者模式，另一个是装饰器模式。它采用了类似于 Java 的流的设计，每个装饰器负责自己一种任务，这复合单一责任原则；各个装饰器之间相互协作，来完成复杂的功能。从上面的源码分析过程中我们也可以看出，RxJava 的缺点也是非常明显的，大量的自定义类，在完成一个功能的时候各装饰器之间不断包装，导致调用的栈非常长。至于线程的切换，它依赖于自己的装饰器模式，因为一个装饰器可以决定其上游的 Observable 在哪些线程当中执行；两个装饰器处于不同的线程的时候，从一个线程中执行完毕自然进入到另一个线程中执行就完成了线程切换的过程。（参考：[RxJava 系列-4：RxJava 源码分析](https://juejin.im/post/5c701480e51d457f14363fd5)。）\n\n优势：实现线程切换更加容易，没有太多的回调    \n劣势：调用栈太长\n\n**问题：RxJava zip 操作**\n\n```java\nObservable<String> a = // ... A 请求\nObservable<Integer> b =  // ... B 请求\nObservable.zip(a, b, new BiFunction<String, Integer, Object>(){\n    @Override\n    public Object apply(@NonNull String s, @NonNull Integer integer) throws Exception {\n        // 拿到了 A 请求和 B 请求的第 n 次执行的结果\n        return new Object();\n    }\n}).subscribe();\n```\n\nA 和 B 会并行在各自的子线程当中, 并且会合并到 `apply()` 方法中。它能保证 B 操作在 A 操作之前执行。我们可以使用这种方式来实现线程的控制。即当一个任务完成之后才执行另一个任务，同时它们的任务的结果可以被合并。那么合并的规则是什么呢？如果 A 和 B 多次发送结果（也就是多次调用 `onNext()` 方法），此时，A 和 B 发送的结果会按照先后的顺序配对，并回调上述的 `BiFunction` 函数。\n\n**问题：RxJava FlatMap 操作**\n\n`flatMap()` 也是一种 `map()`，只是不同的是：假如传入的是一个列表，那么 `map()` 对列表的每一个元素进行变换，然后变换的元素又构成了一个集合。而 `flatMap()` 也可以对列表的每个元素进行变换，只是它变换之后的结果强制是 Observable 类型的，并且如果这些返回的 Observable 又都由列表构成，那么这些映射之后的列表将会构成一个新的列表，交给最终的 Observable. `flatMap()` 与 `contactMap()` 不同的地方是，前者会按照原始列表的顺序拼接返回的列表，而后者不会。\n\n```java\n        Observable.range(1, 5).flatMap(new Function<Integer, ObservableSource<Integer>>() {\n            @Override\n            public ObservableSource<Integer> apply(Integer integer) throws Exception {\n                return Observable.range(integer + 100, 2);\n            }\n        }).subscribe(new Consumer<Integer>() {\n            @Override\n            public void accept(Integer integer) throws Exception {\n                System.out.print(integer + \" \");\n            }\n        });\n```\n\n**问题：RxJava 中的 Schedulers.io() 和 Schedulers.computation() 的区别**\n\n分别对应于 io 密集型应用和计算密集型应用，针对两种不同类型的应用，比如两种不同的应用类型所需要的线程池的线程的数量不同。\n\n## 5、数据库\n\n**问题：数据库框架对比和源码分析？**    \n**问题：SQLite 数据库升级，数据迁移问题？**    \n**问题：SQLite 升级，增加字段的语句**    \n**问题：数据库数据迁移问题**\n\n数据库框架：Room 出现之前使用最多的是 OrmLite 和 GreenDAO. ORMLite 和 JavaWeb 框架中的 Hibernate 相似，都是使用注解的方式来标注数据库中的表、字段、关联关系的，这也是 ORMLite 的工作原理：ORMLite 是基于反射机制工作的，然而这也成为了 ORMLite 的一个非常致命的缺点，性能不好。因此，如果是对想能要求不高的项目，我们可以考虑使用 ORMLite，而如果项目对性能要求较高，我们可以考虑使用 GreenDAO. \n\n个人之前也搭建过简单的数据库框架，只是在 Android 的基础的 SQLite 类基础之上做了一层封装。我也是使用注解，但是对注解的解析只在第一次创建表的时候使用。对 ContentValue 和 Cursur 操作的时候使用的是它们的 setter 和 getter 进行赋值。顶层对基础的查询等操作做了封装，各个子类只需要实现模板的方法，无需写 SQL 就可以实现数据库的基本操作。但是 setter 和 getter 的过程比较繁琐，而且这种重复性的工作是应该交给程序来解决的。这个问题可以使用注解处理进行优化，在编译器期间按规则把这些逻辑写好。（没做的主要原因是出现了 Room，没必要重复造轮子了。）\n\nGreenDao 相较于 ORMLite 等其他数据库框架有以下优势：更精简；性能最大化；内存开销最小化；易于使用的 APIs；对 Android 进行高度优化。Android 中使用比较常见的注解处理器是 APT，但是 GreenDao 使用的是 JDT。代码生成框架使用的是 FreeMarker，对应的还有一种生成代码的方式叫 javapoet 的。GreenDAO 也是通过在编译期间生成代码来完成数据库的字段到对象的属性的映射。\n\n**问题：数据库升级**\n\n```java\n    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n        switch (oldVersion) {\n            case 1:\n            case 2:\n                db.execSQL(\"ALTER TABLE gt_note ADD COLUMN \" + NoteSchema.PREVIEW_IMAGE + \" TEXT\");\n                db.execSQL(\"ALTER TABLE gt_note ADD COLUMN \" + NoteSchema.NOTE_TYPE + \" INTEGER\");\n            case 4:\n                db.execSQL(\"ALTER TABLE gt_note ADD COLUMN \" + NoteSchema.PREVIEW_CONTENT + \" TEXT\");\n                break;\n            case 5:\n                // 判断指定的两个列是否存在，如果不存在的话就创建列\n                Cursor cursor = null ;\n                try{\n                    cursor = db.rawQuery( \"SELECT * FROM \" + tableName + \" LIMIT 0 \", null );\n                    boolean isExist = cursor != null && cursor.getColumnIndex(NoteSchema.PREVIEW_IMAGE) != -1 ;\n                    if (!isExist) {\n                        db.execSQL(\"ALTER TABLE gt_note ADD COLUMN \" + NoteSchema.PREVIEW_IMAGE + \" TEXT\");\n                        db.execSQL(\"ALTER TABLE gt_note ADD COLUMN \" + NoteSchema.NOTE_TYPE + \" INTEGER\");\n                    }\n                } finally{\n                    if(null != cursor && !cursor.isClosed()){\n                        closeCursor(cursor);\n                    }\n                }\n                break;\n        }\n    }\n```\n\n**问题：打开 db 文件**，\n\n## 其他\n\n**问题：用到的一些开源框架，介绍一个看过源码的，内部实现过程**    \n**问题：ButterKnife 实现原理？**\n\nButterKnife 基于注解处理，在运行时根据 Activity 的名称生成对应的注射类。可以在 Activity 名称后面增加 `$Injector` 的方式来实现。然后会根据这个类中使用的注解绑定的控件和事件，在编译期间赋值的代码。当我们调用 `bind()` 方法的时候，根据当前的类名寻找注射器。然后调用它的 `inject()` 方法，在这个生成的方法中去执行 `findViewById()` 和 `setOnClickListener()` 等工作。\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_12_算法.md",
    "content": "# Android 高级面试 12 —— 算法\n\n## 1.冒泡排序算法\n\n```java\n    protected static void exchange(Comparable[] arr, int fromPos, int toPos) {\n        Comparable temp = arr[fromPos];\n        arr[fromPos] = arr[toPos];\n        arr[toPos] = temp;\n    }\n\n    protected static boolean less(Comparable cmp1, Comparable cmp2) {\n        return cmp1.compareTo(cmp2) < 0;\n    }\n```\n\n ```java\n    public static void sort(Comparable[] arr) { // 增序\n        boolean exchanged = true; // 默认值为 true\n        for (int i=0; i<arr.length-1 && exchanged; i++) { // 将布尔值加入到 for 条件中\n            exchanged = false; // 开始循环的时候默认是 false\n            for (int j=arr.length-1; j>i; j--) {\n                if (less(arr[j], arr[j-1])) {\n                    exchange(arr, j, j-1);\n                    exchanged = true; // 当进行了交换的时候才设置为 true\n                }\n            }\n        }\n    }\n```\n\n## 2.快速排序\n\n ```java\n\tpublic class Quick extends Sort{\n\t\n\t    public static void sort(Comparable[] arr) {\n\t        sort(arr, 0, arr.length - 1);\n\t    }\n\t\n\t    private static void sort(Comparable[] arr, int lo, int hi) {\n\t        if (hi <= lo) {\n\t            return;\n\t        }\n\t        int j = partition(arr, lo, hi);\n\t        sort(arr, lo, j - 1);\n\t        sort(arr, j + 1, hi);\n\t    }\n\t\n\t    private static int partition(Comparable[] arr, int lo, int hi) {\n\t        int i = lo, j = hi + 1;\n\t        Comparable v = arr[lo];\n\t        while (true) {\n\t            while (less(arr[++i], v)) { // 找到一个大于v的元素\n\t                if (i == hi) {\n\t                    break;\n\t                }\n\t            }\n\t            while (less(v, arr[--j])) { // 找到一个小于v的元素\n\t                if (j == lo) {\n\t                    break;\n\t                }\n\t            }\n\t            if (i >= j) { // 两个指针相遇了，退出循环\n\t                break;\n\t            }\n\t            exchange(arr, i, j); // 交换i和j处的元素的位置，这样可以保证v左小于v，v右都大于v\n\t        }\n\t        exchange(arr, lo, j); // 还要注意最终要将v和a[j]交换\n\t        return j;\n\t    }\n\t}\n```\n\n## 3.链表翻转\n\n```java\n    private static Node reverse(Node head) {\n        if (head == null || head.next == null) return head;\n\n        Node current = head;\n        Node next = null; //定义当前结点的下一个结点\n        Node reverseHead = null;  //反转后新链表的表头\n        while (current != null) {\n            next = current.next;  //暂时保存住当前结点的下一个结点，因为下一次要用\n            current.next = reverseHead; //将current的下一个结点指向新链表的头结点\n            reverseHead = current;\n            current = next;   // 操作结束后，current节点后移\n        }\n\n        return reverseHead;\n    }\n```\n"
  },
  {
    "path": "笔试面试/Android高级面试_12_项目经验梳理.md",
    "content": "\n\n## 相机\n\n- [ ] Android 中开启摄像头的主要步骤\n\n\n## 压缩\n\n深度研究：\n\n1. SurefaceView, TextureView, Camera\n2. RecyclerView\n3. Adapter + Fragment\n\n热修补+插件化（组件化）\n\nPMW WMS AMW 相关的东西\n\n\n\n## 项目相关\n\n以上的深度研究 + 屏幕适配方式 + WorkManager 的研究\n\n\n\n\n\n\n实际相机拍照的时候是先把照片写到磁盘上面然后在从磁盘上面加载到内存的时候使用一个采样率来采样。从最终的效果来看，设置采样率起到了压缩的作用，但是它只是改变了加载的图片的比率。真正起到压缩作用的是把加载到内存之后的 Bitmap 再次写入到磁盘上面并替换原始的文件。所以，相机采用多大的预览图和输出的图片的尺寸跟最终得到的图片的尺寸和大小没有关系——拍摄的照片写入到磁盘上面之后对其进行压缩。\n\nQ：照片太大的话压缩和写入磁盘的效率可能降低，但是这会有多少的性能损失呢？相机预览和输出的时候，寻找一个不是太大的比例？\n\nQ：图片压缩的尺寸压缩的问题，难道除了设置成 2 的比例，就没有其他的办法了吗？\n\n```java\n/**\n邻近采样：采用一个 2 的倍数，\n1. CompressFormat format：压缩格式，它有 JPEG、PNG、WEBP 三种选择，JPEG 是有损压缩，PNG 是无损压缩，压缩后的图像大小不会变化（也就是没有压缩效果），WEBP 是 Google 推出的图像格式，它相比 JPEG 会节省 30% 左右的空间，处于兼容性和节省空间的综合考虑，我们一般会选择 JPEG。\n2. int quality：0~100 可选，数值越大，质量越高，图像越大。\n3. OutputStream stream：压缩后图像的输出流。\n*/\nBitmapFactory.Options options = new BitmapFactory.Options();\noptions.inSampleSize = 1; // 设置加载图片时的采样率\nBitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);\nbitmap.compress(Bitmap.CompressFormat.JPEG, 75, stream); // 压缩，然后使用 stream 写出数据\nbitmap.recycle();\n\n/**\n双线性采样：Bitmap.createBitmap() 的几个参数的意义，\n1. Bitmap source：源图像\n2. int x：目标图像第一个像素的 x 坐标\n3. int y：目标图像第一个像素的 y 坐标\n4. int width：目标图像的宽度（像素点个数）\n5. int height：目标图像的高度（像素点个数）\n6. Matrix m：变换矩阵\n7. boolean filter：是否开启过滤\n*/\nBitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);\nMatrix matrix = new Matrix();\nmatrix.setScale(0.5f, 0.5f);\nBitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);\nImageUtils.save(bitmap, savePath, Bitmap.CompressFormat.PNG);\n```\n\n但是第二种方式存在一个问题，因为它在进行压缩之前需要将图片全部加载到内存中，如果图片比较大，那么可能会把内存撑爆，导致 OOM. \n\n所以，我们可以考虑结合两种方式拉\n\nQ：如何根据相机支持的图像的质量选择一个合适的尺寸？即使相同的尺寸和质量的图片，不同分辨率的手机拍摄出的效果还是不同的？如何得出图片的质量的等级？不同分辨率的手机拍摄出的照片影响的是什么的效果？\n\nQ：图片的 Bitmap 的计算规则与最终上次的文件的计算规则一样吗？区别是什么？\n\n\n\n其他：视频压缩，视频录制\n\n\n# Android 性能优化-相机优化\n\n场景，人工智能识别图片，对图片质量要求较高，同时为了加快图片上传的速度，需要对图片的大小进行控制，也就是既要保证图片在的质量又要控制图片的体积。照片是由自定义相机拍摄完成的，在拍摄的时候相关参数的选择，相机各种功能的完善等。\n\n所以关注的地方在于，第一是相机，封装一个功能完善的相机库，可以处理常见的问题，同时对 Camera1 和 Camera2 进行兼容。第二是图片的压缩，保证图片的质量，并控制图片的大小。\n\n相机：\n\n相机要解决的几个问题，\n\n1. 使用 Camera1 还是 Camera2 的问题\n2. TextureView 还是 SurfaceView 的问题\n3. 相机的预览尺寸、输出图片的尺寸和拍摄视频的尺寸的计算\n4. 手势缩放\n5. 对焦（自动对焦、外部调整对焦）\n6. 相机实时预览时，提供对外接口，可以对图片实时进行处理\n\n压缩：\n\n1. 使用邻近采样将图片加载到内存中（内存防爆）\n2. 使用质量压缩和双线性采样控制图片的尺寸和质量\n\n遗留的问题，不同分辨率的相机拍摄出的相片的质量不一样，在图片尺寸相同的情况下，相机硬件会影响图片的什么呢？\n\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_1_Handler相关.md",
    "content": "# Android 高级面试-1：Handler 相关\n\n**问题：Handler 实现机制（很多细节需要关注：如线程如何建立和退出消息循环等等）**    \n**问题：关于 Handler，在任何地方 new Handler 都是什么线程下?**     \n**问题：Handler 发消息给子线程，looper 怎么启动?**   \n**问题：在子线程中创建 Handler 报错是为什么?**   \n**问题：如何在子线程创建 Looper?**    \n**问题：为什么通过 Handler 能实现线程的切换?**\n\n参考：\n\n1. `Handler 机制`：Handler 机制中有 4 个主要的对象：Handler、Message、MessageQueue 和 Looper. \n    1. Handler 负责消息的发送和处理；\n    2. Message 是消息对象，是`链表`（不是队列！）的一个结点。Message 的布局变量保存了 Handler，处理消息时就是从 Message 上获取 Handler，并调用它的 `dispatchMessage()`，并进一步回调 `handleMessage()` 方法执行我们的逻辑。\n    3. MessageQueue 是消息队列，用于存放消息对象的数据结构；\n    4. Looper 是消息队列的处理者，用于轮询消息队列，使用 MessageQueue 取出 Message。\n\n2. `线程的问题`：\n\n    1. 当我们在某个线程当中调用 `new Handler()` 的时候会使用当前线程的 Looper 创建 Handler. 当前线程的 Looper 存在于线程局部变量 ThreadLocal 中。\n    2. 在使用 Handler 之前我们需要先调用 `Looper.prepare()` 方法实例化当前线程的 Looper，并将其放置到当前线程的线程局部变量中。\n    3. 一个线程的 Looper 只会被创建一次，之后会先从 ThreadLocal 中获取再使用。\n    4. 调用 `Looper.prepare()` 时会调用 Looper 的构造方法，并在构造方法中初始化 MessageQueue. \n    5. 当我们调用 `Looper.loop()` 时开启消息循环。\n    6. 主线程的 Looper 在 ActivityThread 的静态 `main()` 方法中被创建。主线程的 Looper 跟其他线程有所区别，主线程的 Looper 不能停止。我们可以使用 `Looper.getMainLooper()` 方法来获取主线程的 Looper，并使用它来创建 Handler. \n    \n    所以，在非主线程中使用 Handler 的标准是：\n\n```java\nLooper.prepare(); // 内部会调用 Looper 的 new 方法实例化 Looper 并将其放进 TL\nnew Handler().post(() -> /* do something */);\nLooper.loop();\n```\n\n4. `在 Looper 的 loop() 中开启无限循环为什么不会导致主线程 ANR 呢？` 这是因为 Android 系统本身就是基于消息机制的，而 Looper 的 循环就是来处理这些消息的。造成卡顿和 ANR 是因为某个消息阻塞了 Looper 循环，导致界面消息得不到处理，而不是 Looper 循环本身。并且如果 Looper 中没有消息需要处理，循环将会结束，线程也就关闭了。\n\n5. `Handler 内存泄漏及解决办法`：如果 Handler 不是静态内部类，Handler 会持有 Activity 的匿名引用。当 Activity 要被回收时，因为 Handler 在做耗时操作没有被释放，Handler Activity 的引用不能被释放导致 Activity 没有被回收停留在内存中造成内存泄露。 解决方法是：\n\n    1. 将 Handler 设为静态内部类，如果需要的话，使用弱引用引用外部的 Activity；\n    2. 在 Activity 生命周期 `onDestroy()` 中调用 `Handler.removeCallbacks()` 方法。\n\n**问题：Handler.post() 的逻辑在哪个线程执行的，是由 Looper 所在线程还是 Handler 所在线程决定的？**    \n**问题：Handler 的 post()/send() 的原理？**    \n**问题：Handler 的 post() 和 postDelayed() 方法的异同？**\n\n`post()` 方法所在的线程由 Looper 所在线程决定的；最终逻辑是在 `Looper.loop()` 方法中，从 MQ 中拿出 Message，并且执行其逻辑。这是在 Looper 中执行的。因此由 Looper 所在线程决定。\n\n不论你调用 `send()` 类型的方法还是 `post()` 类型的方法，最终都会调用到 `sendMessageAtTime()` 方法。`post()` 和 `postDelay()` 的区别在于，前者使用当前时间，后者使用当前时间+delay 的时间来决定消息触发的时间。最终方法的参数都将被包装成一个 Message 对象加入到 Handler 对应的 Looper 的 MQ 中被执行。\n\n**问题：Looper 和 Handler 一定要处于一个线程吗？子线程中可以用 MainLooper 去创建 Handler吗？**\n\nLooper 和 Handler 不需要再一个线程中，默认的情况下会从 TL 中取当前线程对应的 Looper，但我们可以通过显式地指定一个 Looper 的方式来创建 Handler. 比如，当我们想要在子线程中发送消息到主线程中，那么我们可以\n\n```java\nHandler handler = new Handler(Looper.getMainLooper());\n```\n\n**问题：Handler.post() 方法发送的是同步消息吗？可以发送异步消息吗？**\n\n用户层面发送的都是同步消息，不能发送异步消息；异步消息只能由系统发送。\n\n**问题：MessageQueue.next() 会因为发现了延迟消息，而进行阻塞。那么为什么后面加入的非延迟消息没有被阻塞呢？**    \n**问题：MessageQueue.enqueueMessage() 方法的原理，如何进行线程同步的？**    \n**问题：MessageQueue.next() 方法内部的原理？**    \n**问题：next() 是如何处理一般消息的？**    \n**问题：next() 是如何处理同步屏障的？**     \n**问题：next() 是如何处理延迟消息的？**\n\n1. 创建 Looper 时会同时创建一个 MQ，而 MQ 同时又会调用 `nativeInit()` 方法在 Native 层实例化一个 MQ 和 Looper，并返回 Native 的 MQ 对象的指针. \n2. Java 层的 Looper 和 Native 层的 Looper 之间使用 epoll 进行通信。\n3. 当调用 Looper 的 `loop()` 方法的时候会启动一个 for 循环来对消息进行处理。它调用 MQ 的 `next()` 方法尝试获取消息，这个方法也是一个 for 循环，它调用 `nativePollOnce()` 向管道写入一个消息，并等待返回，如果没有消息这里就会阻塞。当拿到了返回结果之后，这里继续向下进行处理，从 Message 中读取消息并进行处理。\n4. 在线程安全方面，当从 `nativePollOnce()` 中返回之后，使用 `sychronized(this)` 对 MQ 进行加锁来保证线程安全。\n5. 当使用 Handler 向 MQ 中添加消息时，会根据消息触发时间决定它在链表中的位置，时间早的位于链表的头结点。然后，如果此时 MQ 处于阻塞状态，那么就会调用 `nativeWake()` 方法向管道中写入消息，这样 MQ 就从 `nativePollOnce()` 中返回了。\n6. `同步屏障`用来立即推迟所有将要执行的同步消息，知道释放同步屏障。使用 `postSyncBarrier()` 进行同步屏障，使用 `removeSyncBarrier()` 结束同步屏障。前者会返回一个 token，然后我们将其传入到 `removeSyncBarrier()` 中结束当前的同步屏障。进行内存屏障的时候会创建一个立即执行的消息，并将其添加到 MQ 中。当尝试获取消息的时候就可能会在 `nativePollOnce()` 阻塞。释放同步屏障的时候会从链表中找到这个结点，并可能调用 `nativeWake()` 方法。对于同步类型的消息，即使发生了同步屏障，它也会被正常执行。\n7. 同步屏障的使用案例：ViewRootImpl 中，`scheduleTraversals()` 方法在遍历 View 树之前会进行同步屏障。（猜测是用来暂停非 UI 绘制的消息，UI 绘制完毕之后再恢复执行。）\n\n**问题：Handler 的 dispatchMessage() 分发消息的处理流程？**    \n**问题：Handler 为什么要有 Callback 的构造方法？**\n\n使用 Handler 的时候我们会覆写 Handler 的 `handleMessage()` 方法。当我们调用该 Handler 的 `send()` 或者 `post()` 发送一个消息的时候，发送的信息会被包装成 Message，并且将该 Message 的 target 指向当前 Handler，这个消息会被放进 Looper 的 MQ 中。然后在 Looper 的循环中，取出这个 Message，并调用它的 target Handler，也就是我们定义的 Handler 的 `dispatchMessage()` 方法处理消息，此时会调用到 Handler 的  `handleMessage()` 方法处理消息，并回调 Callback. \n\n当 Handler 在消息队列中被执行的时候会直接调用 Handler 的 `dispatchMessage()` 方法回调 Callback.\n\n**问题：Looper 的两个退出方法？**     \n**问题：quit() 和 quitSafely() 有什么区别**    \n**问题：子线程中创建了 Looper，在使用完毕后，终止消息循环的方法？**    \n**问题：quit() 和 quitSafely() 的本质是什么？**    \n\n```java\n    public void quit() {\n        mQueue.quit(false);\n    }\n    public void quitSafely() {\n        mQueue.quit(true);\n    }\n    void quit(boolean safe) {\n        if (!mQuitAllowed)  throw new IllegalStateException(\"Main thread not allowed to quit.\");\n        synchronized (this) {\n            if (mQuitting) return;\n            mQuitting = true;\n            if (safe)  removeAllFutureMessagesLocked(); // 把所有延迟消息清除\n            else       removeAllMessagesLocked();  // 直接把消息队列里面的消息清空\n            nativeWake(mPtr); // 唤醒\n        }\n    }\n    public static void loop() {\n        // ...\n        for (;;) {\n            Message msg = queue.next();\n            if (msg == null) { // 得到了 null 就返回了\n                return;\n            }\n            // ...\n        }\n    }\n    // MQ:\n    Message next() {\n        // ...\n        for (;;) {\n            // ...\n            nativePollOnce(ptr, nextPollTimeoutMillis);\n            synchronized (this) {\n                // ... 这里就返回了 null\n                if (mQuitting) {\n                    dispose();\n                    return null;\n                }\n                // ...\n            }\n        }\n     }\n```\n\n`quit()` 和 `quitSafely()` 的本质就是让消息队列的 `next()` 返回 null，以此来退出 `Looper.loop()`。\n\n`quit()` 调用后直接终止 Looper，不在处理任何 Message，所有尝试把 Message 放进消息队列的操作都会失败，比如 `Handler.sendMessage()` 会返回 false，但是存在不安全性，因为有可能有 Message 还在消息队列中没来的及处理就终止 Looper 了。\n\n`quitSafely()` 调用后会在所有消息都处理后再终止 Looper，所有尝试把 Message 放进消息队列的操作也都会失败。\n\n**问题：Looper.loop() 在什么情况下会退出？**\n\n1. `next()` 方法返回的 msg == null；\n2. 线程意外终止。\n\n**问题：Looper.loop() 的源码流程?**\n\n1. 获取到 Looper 和消息队列；\n2. for 无限循环，阻塞于消息队列的 `next()` 方法；\n3. 取出消息后调用 `msg.target.dispatchMessage(msg)` 进行消息分发。\n\n**问题：Looper.loop() 方法执行时，如果内部的 myLooper() 获取不到Looper会出现什么结果?**\n\n```java\n    public static void loop() {\n        final Looper me = myLooper();\n        if (me == null) {\n            throw new RuntimeException(\"No Looper; Looper.prepare() wasn't called on this thread.\");\n        }\n        // ...\n    }\n```\n\n**问题：Android 如何保证一个线程最多只能有一个 Looper？如何保证只有一个 MessageQueue**\n\n通过保证只有一个 Looper 来保证只有以一个 MQ. 在一个线程中使用 Handler 之前需要使用 `Looper.prepare()` 创建 Looper，它会从 TL 中获取，如果发现 TL 中已经存在 Looper，就抛异常。\n\n**问题：Handler 消息机制中，一个 Looper 是如何区分多个 Handler 的？**\n\n根据消息的分发机制，Looper 不会区分 Handler，每个 Handler 会被添加到 Message 的 target 字段上面，Looper 通过调用 `Message.target.handleMessage()` 来让 Handler 处理消息。\n\n## 参考资料\n\n1. [《Android 中的 Handler 的 Native 层研究》](https://www.liangzl.com/get-article-detail-14435.html)\n"
  },
  {
    "path": "笔试面试/Android高级面试_2_IPC相关.md",
    "content": "# Android 高级面试-2：IPC 相关\n\n## 1、IPC\n\n**问题：Android 上的 IPC 跨进程通信时如何工作的**    \n**问题：简述 IPC？**    \n**问题：进程间通信的机制**    \n**问题：AIDL 机制**    \n**问题：Bundle 机制**    \n**问题：多进程场景遇见过么？**\n\nIPC 就是指进程之间的通信机制，在 Android 系统中启动 Activity/Service 等都涉及跨进程调用的过程。\n\nAndroid 中有多种方式可以实现 IPC，\n\n1. `Bundle`，用于在四大组件之间传递信息，优点是使用简单，缺点是只能使用它支持的数据类型。Bundle 继承自 BaseBundle，它通过内部维护的 `ArrayMap<String, Object>` 来存储数据。当我们使用 `put()` 和 `get()` 系列的方法的时候都会直接与其进行交互。`ArrayMap<String, Object>` 与 HashMap 类似，也是用作键值对的映射，但是它的实现方式与 SpareArray 类似，是基于两个数组来实现的映射。目的也是为了提升 Map 的效率。它在查找某个哈希值的时候使用的是二分查找。\n\n2. `共享文件`，即两个进程通过读/写同一个文件来进行交换数据。由于 Android 系统是基于 Linux 的，使得其并发读/写文件可以没有任何限制地进行，甚至两个线程同时对同一个文件进行写操作都是被充许的。如果并发读/写，我们读取出来的数据可能不是最新的。文件共享方式适合在对数据同步要求不高的情况的进程之间进行通信，并且要妥善处理并发读/写的问题。    \n另外，SharedPreferences 也是属于文件的一种，但是系统对于它的读/写有一定的缓存策略，即在内存中有一份 SP 文件的缓存，因此在多进程模式下，系统对它的读/写变得不可靠，面对高并发的读/写访问有很大几率会丢失数据。不建议在进程间通信中使用 SP.\n\n3. `Messenger` 是一种轻量级的 IPC 方案，它的底层实现是 AIDL，可以在不同进程中传递 Message. 它一次只处理一个请求，在服务端不需要考虑线程同步的问题，服务端不存在并发执行的情形。在远程的服务中，声明一个 Messenger，使用一个 Handler 用来处理收到的消息，然后再 `onBind()` 方法中返回 Messenger 的 binder. 当客户端与 Service 绑定的时候就可以使用返回的 Binder 创建 Messenger 并向该 Service 发送服务。\n\n    ```java\n        // 远程服务的代码\n        private Messenger messenger = new Messenger(new MessengerHandler(this));\n\n        @Nullable\n        @Override\n        public IBinder onBind(Intent intent) {\n            ToastUtils.makeToast(\"MessengerService bound!\");\n            return messenger.getBinder();\n        }\n\n        // 客户端 bind 服务的时候用到的 ServiceConnection\n        private ServiceConnection msgConn = new ServiceConnection() {\n            @Override\n            public void onServiceConnected(ComponentName name, IBinder service) {\n                // 这样就拿到了远程的 Messenger，向它发送消息即可\n                boundServiceMessenger = new Messenger(service);\n            }\n            // ... ...\n        }\n\n        // 客户端发送消息的代码\n        Message message = Message.obtain(null, /*what=*/ MessengerService.MSG_SAY_SOMETHING);\n        message.replyTo = receiveMessenger; // 客户端用来接收服务端消息的 Messenger\n        Bundle bundle = new Bundle(); // 构建消息\n        bundle.putString(MessengerService.MSG_EXTRA_COMMAND, \"11111\");\n        message.setData(bundle);\n        boundServiceMessenger.send(message); // 发送消息给服务端\n    ```\n\n4. `AIDL`：Messenger 是以`串行`的方式处理客户端发来的消息，如果大量消息同时发送到服务端，服务端只能一个一个处理，所以大量并发请求就不适合用 Messenger ，而且 Messenger 只适合传递消息，不能`跨进程`调用服务端的方法。AIDL 可以解决并发和跨进程调用方法的问题。    \nAIDL 即 Android 接口定义语言。使用的时候只需要创建一个后缀名为 `.aidl` 的文件，然后在编译期间，编译器会使用 `aidl.exe` 自动生成 Java 类文件。    \n 远程的服务只需要实现 Stub 类，客户端需要在 `bindService()` 的时候传入一个 ServiceConnection，并在连接的回调方法中将 Binder 转换成为本地的服务。然后就可以在本地调用远程服务中的方法了。\n\n    ```java\n        /* 注意！这里使用了自定义的 Parcelable 对象：Note 类，但是 AIDL 不认识这个类，所以我们要创建一个与 Note 类同名的 AIDL 文件：Note.aidl. 并且类必须与 aidl 文件的包结构一致。*/\n\n        // 远程服务的代码\n        private Binder binder = new INoteManager.Stub() {\n            @Override\n            public Note getNote(long id) {\n                // ... ...\n            }\n        };\n        // 绑定服务\n        public IBinder onBind(Intent intent) {\n            return binder;\n        }\n\n        // 客户端代码\n        private INoteManager noteManager;\n        private ServiceConnection connection = new ServiceConnection() {\n            @Override\n            public void onServiceConnected(ComponentName name, IBinder service) {\n                // 获取远程的服务，转型，然后就可以在本地使用了\n                noteManager = INoteManager.Stub.asInterface(service);\n            }\n\n            @Override\n            public void onServiceDisconnected(ComponentName name) { }\n        };\n\n        // 服务端访问权限控制：使用 Permission 验证，在 manifest 中声明\n        <permission android:name=\"com.jc.ipc.ACCESS_BOOK_SERVICE\"\n            android:protectionLevel=\"normal\"/>\n        <uses-permission android:name=\"com.jc.ipc.ACCESS_BOOK_SERVICE\"/>\n        // 服务端 onBinder 方法中\n        public IBinder onBind(Intent intent) {\n            //Permission 权限验证\n            int check = checkCallingOrSelfPermission(\"com.jc.ipc.ACCESS_BOOK_SERVICE\");\n            if (check == PackageManager.PERMISSION_DENIED) return null;\n            return mBinder;\n        }\n    ```\n\n    AIDL 支持的数据类型包括，1).基本数据类型；2).string 和 CharSequence；3).List 中只支持 ArrayList，并且其元素必须能够被 AIDL 支持；4).Map 中只支持 HashMap，并且其元素必须能够被 AIDL 支持；5).所有实现了 Parcelable 接口的对象；6).AIDL：所有 AIDL 接口本身也可以在AIDL文件中使用。    \n\n5. `ContentProvider`，主要用来对提供数据库方面的共享。缺点是主要提供数据源的 CURD 操作。\n\n6. `Socket`，Socket 主要用在网络方面的数据交换。在 Android 系统中，启动的 Zygote 进程的时候会启动一个 ServerSocket. 当我们需要创建应用进程的时候会通过 Socket 与之进行通信，这也是 Socket 的应用。\n\n7. `管道`，另外在使用 Looper 启动 MQ 的时候会在 Native 层启动一个 Looper. Native 层的与 Java 层的 Looper 进行通信的时候使用的是 epoll，也就是管道通信机制。\n\n![Android 中的进程间通信的机制](res/ipc.png)\n\n**问题：为何需要进行 IPC？多进程通信可能会出现什么问题？**\n\n在 Android 系统中一个应用默认只有一个进程，每个进程都有自己独立的资源和内存空间，其它进程不能任意访问当前进程的内存和资源，系统给每个进程分配的内存会有限制。如果一个进程占用内存超过了这个内存限制，就会报 OOM 的问题，很多涉及到大图片的频繁操作或者需要读取一大段数据在内存中使用时，很容易报 OOM 的问题，为了解决应用内存的问题，Android 引入了多进程的概念，它允许在同一个应用内，为了分担主进程的压力，将占用内存的某些页面单独开一个进程，比如 Flash、视频播放页面，频繁绘制的页面等。\n\n实现的方式很简单就是在 Manifest 中注册 Activity 等的时候，使用 `process` 属性指定一个进程即可。process 分私有进程和全局进程，`以 : 号开头的属于私有进程，其他应用组件不可以和他跑在同一个进程中；不以 : 号开头的属于全局进程，其他应用可以通过 ShareUID 的方式和他跑在同一个进程中`。此外，还有一种特殊方法，通过 JNI 在 native 层去 fork 一个新的进程。\n\n但是多进程模式出现以下问题：\n\n1. 静态成员和单例模式完全失效，因为没有存储在同一个空间上；\n2. 线程同步机制完全失效，因为线程处于不同的进程；\n3. SharedPreferences 的可靠性下降，因为系统对于它的读/写有一定的缓存策略，即在内存中有一份 SP 文件的缓存；\n4. Application 多次创建。\n\n解决这些问题可以依靠 Android 中的进程通信机制，即 IPC，接上面的问题。\n\n**问题：Binder 相关？**\n\n*为什么要设计 Binder，Binder 模型，高效的原因*\n\nBinder 是 Android 设计的一套进程间的通信机制。Linux 本身具有很多种跨进程通信方式，比如管道（Pipe）、信号（Signal）和跟踪（Trace）、插口（Socket）、消息队列（Message）、共享内存（Share Memory）和信号量（Semaphore）。之所以设计出 Binder 是因为，这几种通信机制在效率、稳定性和安全性上面无法满足 Android 系统的要求。\n\n1. `效率上` ：Socket 作为一款通用接口，其传输效率低，开销大，主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式，即数据先从发送方缓存区拷贝到内核开辟的缓存区中，然后再从内核缓存区拷贝到接收方缓存区，至少有两次拷贝过程。共享内存虽然无需拷贝，但控制复杂，难以使用。Binder 只需要一次数据拷贝，性能上仅次于共享内存。\n\n2. `稳定性`：Binder 基于 C|S 架构，客户端（Client）有什么需求就丢给服务端（Server）去完成，架构清晰、职责明确又相互独立，自然稳定性更好。共享内存虽然无需拷贝，但是控制负责，难以使用。从稳定性的角度讲，Binder 机制是优于内存共享的。\n\n3. `安全性`：Binder 通过在内核层为客户端添加身份标志 UID|PID，来作为身份校验的标志，保障了通信的安全性。 传统 IPC 访问接入点是开放的，无法建立私有通道。比如，命名管道的名称，SystemV 的键值，Socket 的 ip 地址或文件名都是开放的，只要知道这些接入点的程序都可以和对端建立连接，不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。\n\n在 Binder 模型中共有 4 个主要角色，它们分别是：`Client、Server、Binder 驱动和 ServiceManager`. Binder 的整体结构是基于 C|S 结构的，以我们启动 Activity 的过程为例，每个应用都会与 AMS 进行交互，当它们拿到了 AMS 的 Binder 之后就像是拿到了网络接口一样可以进行访问。如果我们将 Binder 和网络的访问过程进行类比，那么 Server 就是服务器，Client 是客户终端，ServiceManager 是域名服务器（DNS），驱动是路由器。\n\n1. Client、Server 和 Service Manager 实现在用户空间中，Binder 驱动程序实现在内核空间中；\n2. Binder 驱动程序和 ServiceManager 在 Android 平台中已经实现，开发者只需要在用户空间实现自己的 Client 和 Server；\n3. Binder 驱动程序提供设备文件 `/dev/binder` 与用户空间交互，Client、Server 和 ServiceManager 通过 `open 和 ioctl` 文件操作函数与 Binder 驱动程序进行通信；\n4. Client 和 Server 之间的进程间通信通过 Binder 驱动程序间接实现；\n5. ServiceManager 是一个守护进程，用来管理 Server，并向 Client 提供查询 Server 接口的能力。\n\n系统启动的 init 进程通过解析 `init.rc` 文件创建 ServiceManager. 此时会，先打开 Binder 驱动，注册 ServiceManager 成为上下文，最后启动 Binder 循环。当使用到某个服务的时候，比如 AMS 时，会先根据它的字符串名称到缓冲当中去取，拿不到的话就从远程获取。这里的 ServiceManager 也是一种服务。\n\n1. 客户端首先获取服务器端的代理对象。所谓的代理对象实际上就是在客户端建立一个服务端的“引用”，该代理对象具有服务端的功能，使其在客户端访问服务端的方法就像访问本地方法一样。\n2. 客户端通过调用服务器代理对象的方式向服务器端发送请求。\n3. 代理对象将用户请求通过 Binder 驱动发送到服务器进程。\n4. 服务器进程处理用户请求，并通过 Binder 驱动返回处理结果给客户端的服务器代理对象。\n\nBinder 高效的原因，当两个进程之间需要通信的时候，Binder 驱动会在两个进程之间建立两个映射关系：内核缓存区和内核中数据接收缓存区之间的映射关系，以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。这样，当把数据从 1 个用户空间拷贝到内核缓冲区的时候，就相当于拷贝到了另一个用户空间中。这样只需要做一次拷贝，省去了内核中暂存这个步骤，提升了一倍的性能。实现内存映射靠的就是上面的 `mmap()` 函数。\n\n## 2、序列化\n\n**问题：序列化的作用，以及 Android 两种序列化的区别**   \n**问题：序列化，Android 为什么引入 Parcelable**    \n**问题：有没有尝试简化 Parcelable 的使用**\n\nAndroid 中主要有两种序列化的方式。\n\n第一种是 `Serializable`. 它是 Java 提供的序列化方式，让类实现 Serializable 接口就可以序列化地使用了。这种序列化方式的缺点是，它`序列化的效率比较低，更加适用于网络和磁盘中信息的序列化，不太适用于 Android 这种内存有限的应用场景`。优点是使用方便，只需要实现一个接口就行了。\n\n这种序列化的类可以使用 ObjectOutputStream/ObjectInputStream 进行读写。这种序列化的对象可以提供一个名为 `serialVersionUID` 的字段，用来标志类的版本号，比如当类的解构发生变化的时候将无法进行反序列化。此外，\n\n1. 静态成员变量不属于对象，不会参与序列化过程 \n2. 用 transient 关键字标记的成员变量不会参与序列化过程。\n\n第二种方式是 Parcelable. 它是 Android 提供的新的序列化方式，主要用来进行内存中的序列化，无法进行网络和磁盘的序列化。它的`缺点是使用起来比较繁琐，需要实现两个方法，和一个静态的内部类。`\n\nSerializable 会使用反射，序列化和反序列化过程需要大量 I/O 操作，在序列化的时候会产生大量的临时变量，从而引起频繁的 GC。Parcelable 自已实现封送和解封（marshalled & unmarshalled）操作不需要用反射，数据也存放在 Native 内存中，效率要快很多。\n\n我自己尝试过一些简化 Parcelable 使用的方案，通常有两种解决方案：第一种方式是使用 IDE 的`插件`来辅助生成 Parcelable 相关的代码（[插件地址](https://github.com/mcharmas/android-parcelable-intellij-plugin)）；第二种方案是使用`反射`，根据字段的类型调用 `wirte()` 和 `read()` 方法（性能比较低）；第三种方案是基于`注解`处理，在编译期间生成代理类，然后在需要覆写的方法中调用生成的代理类的方法即可。\n\n## 3、进程与线程\n\n**问题：进程与线程之间有什么区别与联系？**     \n**问题：为什么要有线程，而不是仅仅用进程？**     \n\n一个进程就是一个执行单元，在 PC 和移动设备上指一个程序或应用。在 Android 中，一个应用默认只有一个进程，每个进程都有自己独立的资源和内存空间，其它进程不能任意访问当前进程的内存和资源，系统给每个进程分配的内存会有限制。实现的方式很简单就是在 Manifest 中注册 Activity 等的时候，使用 `process` 属性指定一个进程即可。process 分私有进程和全局进程，以 `:` 号开头的属于私有进程，其他应用组件不可以和他跑在同一个进程中；不以 `:` 号开头的属于全局进程，其他应用可以通过 ShareUID 的方式和他跑在同一个进程中\n\nAndroid 系统启动的时候会先启动 `Zygote 进程`，当我们需要创建应用程序进程的时候的会通过 Socket 与之通信，Zygote 通过 fork 自身来创建我们的应用程序的进程。\n\n*不应只是简单地讲述两者之间的区别，同时涉及系统进程的创建，应用进程的创建，以及如何在程序中使用多进程等。*\n\n线程是 CPU 调度的最小单元，一个进程可包含多个线程。Java 线程的实现是基于`一对一的线程模型`，即通过语言级别层面程序去间接调用系统的内核线程。内核线程由操作系统内核支持，由操作系统内核来完成线程切换，内核通过操作调度器进而对线程执行调度，并将线程的任务映射到各个处理器上。由于我们编写的多线程程序属于`语言层面`的，程序一般不会直接去调用`内核线程`，取而代之的是一种`轻量级的进程(Light Weight Process)`，也是通常意义上的线程。由于`每个轻量级进程都会映射到一个内核线程`，因此我们可以通过轻量级进程调用内核线程，进而由操作系统内核将任务映射到各个处理器。这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。\n\n![一对一的线程模型](https://img-blog.csdn.net/20170608094427710?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamF2YXplamlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)\n\n**问题：Android 中进程内存的分配，能不能自己分配定额内存**   \n**问题：进程和 Application 的生命周期**    \n**问题：进程调度**    \n**问题：Android 进程分类**    \n\nAndroid 应用的内存管理：由 AMS 集中管理所有进程的内存分配；系统回收进程的时候优先级如下所示。Android 基于进程中运行的组件及其状态规定了默认的五个回收优先级：\n\n1. `Empty process` (空进程)\n2. `Background process` (后台进程)\n3. `Service process` (服务进程)\n4. `Visible process` (可见进程)\n5. `Foreground process` (前台进程)\n\n系统需要进行内存回收时最先回收空进程，然后是后台进程，以此类推最后才会回收前台进程。\n\n## 附录\n\n1. *了解 Android 系统启动过程和虚拟机内存模型 JMM，请参考我的文章：[Android 系统源码-1：Android 系统启动流程源码分析](https://juejin.im/post/5c4471e56fb9a04a027aa8ac) 和 [JVM扫盲-3：虚拟机内存模型与高效并发](https://juejin.im/post/5b4f48e75188251b1b448aa0)*\n2. *了解 Binder 相关的知识，请参考我的文章：[《Android 系统源码-2：Binder 通信机制》](https://juejin.im/post/5c4861a0e51d45518d470805)*\n"
  },
  {
    "path": "笔试面试/Android高级面试_3_语言相关.md",
    "content": "# Android 高级面试-3：语言相关\n\n## 1、Java 相关\n\n### 1.1 缓存相关\n\n**问题：LruCache 的原理？**\n**问题：DiskLruCache 的原理？**\n\nLruCache 用来实现基于内存的缓存，LRU 就是`最近最少使用`的意思，LruCache 基于`LinkedHashMap`实现。LinkedHashMap 是在 HashMap 的基础之上进行了封装，除了具有哈希功能，还将数据插入到`双向链表`中维护。每次读取的数据会被移动到链表的尾部，当达到了缓存的最大的容量的时候就将链表的首部移出。使用 LruCache 的时候需要注意的是单位的问题，因为该 API 并不清楚要存储的数据是如何计算大小的，所以它提供了方法供我们实现大小的计算方式。\n\nDiskLruCache 与 LruCache 类似，也是用来实现缓存的，并且也是基于 LinkedHashMap 实现的。不同的是，它是基于磁盘缓存的，LruCache 是基于内存缓存的。所以，DiskLruCache 能够存储的空间更大，但是读写的速率也更慢。使用 DiskLruCache 的时候需要到 Github 上面去下载。OkHttp 和 Glide 的磁盘缓存都是基于 DiskLruCache 开发的。DiskLruCahce 内部维护了一个日志文件，记录了读写的记录的信息。其他的基本都是基础的磁盘 IO 操作。\n\n### 1.2 List 相关\n\n**问题：ArrayList 与 LinkedList 区别**\n\n1. ArrayList 是基于`动态数组`，底层使用`System.arrayCopy()`实现数组扩容；查找值的复杂度为`O(1)`，增删的时候可能扩容，复杂度也比 LinkedList 高；如果能够大概估出列表的长度，可以通过在 new 出实例的时候指定一个大小来`指定数组的初始大小`，以减少扩容的次数；适合应用到`查找多于增删`的情形，比如作为 Adapter 的数据的容器。\n2. LinkedList 是基于`双向链表`；增删的复杂度为`O(1)`，查找的复杂度为`O(n)`；适合应用到`增删比较多`的情形。\n3. 两种列表`都不是线程安全`的，`Vector`是线程安全的，但是它的线程安全的实现方式是通过对每个方法进行加锁，所以性能比较低。\n\n**问题：如何实现线程间安全地操作 List？**\n\n具体使用哪种方式，可以根据具体的业务逻辑进行选择。通常有以下几种方式：\n\n1. 操作 List 的时候使用`sychronized`进行控制。我们可以在我们自己的业务方法上面进行加锁来保证线程安全。\n2. 使用`Collections.synchronizedList()`进行包装。这个方法内部使用了`私有锁`来实现线程安全，就是通过对一个全局变量进行加锁。调用我们的 List 的方法之前需要先获取该私有锁。私有锁可以降低锁粒度。\n3. 使用并发包中的类，比如在读多写少的情况下，为了提升效率可以使用`CopyOnWriteArrayList`代替 ArrayList，使用`ConcurrentLinkedQueue`代替 LinkedList. 并发容器中的 `CopyOnWriteArrayList` 在读的时候不加锁，写的时候使用 Lock 加锁。`ConcurrentLinkedQueue` 则是基于 CAS，在增删数据之前会先进行比较。\n\n### 1.3 Map 相关\n\n**问题：SparseArray 的原理**\n\nSparseArray 主要用来替换 Java 中的 HashMap，因为 HashMap 将整数类型的键默认装箱成 Integer (效率比较低). 而 SparseArray 通过内部维护`两个数组来进行映射`，并且使用`二分查找`寻找指定的键，所以`它的键对应的数组无需是包装类型`。SparseArray 用于当 HashMap 的键是 Integer 的情况，它会在内部维护一个 int 类型的数组来存储键。同理，还有 LongSparseArray, BooleanSparseArray 等，都是用来通过减少装箱操作来节省内存空间的。但是，因为它内部使用二分查找寻找键，所以其效率不如 HashMap 高，所以当要存储的键值对的数量比较大的时候，考虑使用 HashMap. \n\n**问题：HashMap、ConcurrentHashMap 以及 HashTable?**     \n**问题：hashmap 如何 put 数据（从 hashmap 源码角度讲解）？（掌握 put 元素的逻辑）**\n\nHashMap (下称 HM) 是哈希表，ConcurrentHashMap (下称 CHM) 也是哈希表，它们之间的区别是 HM 不是`线程安全`的，CHM `线程安全`，并且对锁进行了优化。对应 HM 的还有 HashTable (下称 HT)，它通过对内部的每个方法加锁来实现线程安全，效率较低。\n\nHashMap 的实现原理：HashMap 使用`拉链法`来解决哈希冲突，即当两个元素的哈希值相等的时候，它们会被方进一个`桶`当中。当一个桶中的数据量比较多的时候，此时 HashMap 会采取两个措施，要么扩容，要么将桶中元素的数据结构从链表转换成红黑树。因此存在几个常量会决定 HashMap 的表现。在默认的情况下，当 HashMap 中的已经被占用的`桶的数量达到了 3/4` 的时候，会对 HashMap 进行`扩容`。当一个桶中的元素的数量达到了 `8` 个的时候，如果桶的数量达到了 64 个，那么会将该桶中的元素的`数据结构从链表转换成红黑树`。如果桶的数量还没有达到 64 个，那么此时会对 HashMap 进行扩容，而不是转换数据结构。\n\n从数据结构上，HashMap 中的桶中的元素的数据结构从链表转换成红黑树的时候，仍然可以保留其链表关系。因为 HashMap 中的 TreeNode 继承了 LinkedHashMap 中的 Entry，因此它存在两种数据结构。\n\nHashMap 在实现的时候对性能进行了很多的优化，获取对象的哈希码的时候，会先使用哈希值的高 16 位与低 16 进行异或运算来提升哈希值的随机性。然后截取后面几位而不是取余的方式计算元素在数组中的索引。（保证按照 2 的整数次幂进行扩容，这样截取后面的几位就可以得到桶的索引。）\n\n因为每个桶的元素的数据结构有两种可能，因此，当对 HashMap 进行增删该查的时候都会根据结点的类型分成两种情况来进行处理。当数据结构是链表的时候处理起来都非常容易，使用一个循环对链表进行遍历即可。当数据结构是红黑树的时候处理起来比较复杂。红黑树的查找可以沿用二叉树的查找的逻辑。\n\n**问题：集合 Set 实现 Hash 怎么防止碰撞？**\n**问题：HashSet 与 HashMap 怎么判断集合元素重复**\n**问题：HashMap 的实现？与 HashSet 的区别？**\n**问题：TreeMap 具体实现？**\n\nHashSet 内部通过 HashMap 实现:\n\n1. HashMap 解决哈希冲突使用的是`拉链法`，碰撞的元素会放进链表中，链表`长度超过 8`，并且桶的`数量大于 64` 的时候，会将桶的数据结构从链表转换成红黑树。\n2. HashMap 在求得每个结点在数组中的索引的时候，会使用对象的哈希码的高八位和低八位求异或，来增加哈希码的随机性。\n3. 当我们通过 `put()` 方法将一个键值对添加到哈希表当中的时候，会根据哈希值和键是否相等两个条件进行判断，只有当两者完全相等的时候才认为元素发生了重复。\n\nHashSet 不允许列表中存在重复的元素，HashSet 内部使用的是 HashMap 实现的。在我们向 HashSet 中添加一个元素的时候，会将该元素作为键，一个默认的对象作为值，构成一个键值对插入到内部的 HashMap 中。\n\nTreeMap 是基于红黑树实现的，它要求用户要么在创建 TreeMap 的时候传入比较器，要么键本身是 Comparable 的。当向树中插入一个键值对的时候，它会根据键和已有键的比较的情况为新插入的键值对寻找一个合适的位置。\n\n### 1.4 注解相关\n\n**问题：对 Java 注解的理解**\n\nJava 注解在 Android 中比较常见的使用方式有 3 种：\n\n1. 第一种方式是基于`反射`的。因为反射本身的性能问题，所以它通常用来做一些简单的工作，比如为类、类的字段和方法等添加额外的信息，然后通过反射来获取这些信息。    \n\n2. 第二种方式是基于 `AnnotationProcessor` 的，也就是在编译期间动态生成样板代码，然后通过反射触发生成的方法。比如 ButterKnife 就使用注解处理，在编译的时候 find 使用了注解的控件，并为其绑定值。然后，当调用 `bind()` 的时候直接反射调用生成的方法。Room 也是在编译期间为使用注解的方法生成数据库方法的。在开发这种第三方库的时候还可能使用到 `Javapoet` 来帮助我们生成 Java 文件。    \n\n3. 最后一种比较常用的方式是使用注解来取代枚举。因为枚举相比于常量有额外的内存开销，所以开发的时候通常使用常量来取代枚举。但是如果只使用常量我们无法对传入的常量的范围进行限制，因此我们可以使用注解来限制取值的范围。以整型为例，我们会在定义注解的时候使用注解 `@IntDef({/*各种枚举值*/})` 来指定整型的取值范围。然后使用注解修饰我们要方法的参数即可。这样 IDE 会给出一个提示信息，提示我们只能使用指定范围的值。\n\n### 1.5 Object 相关\n\n**问题：Object 类的 equal() 和 hashcode() 方法重写？**\n\n这两个方法都具有决定一个对象身份功能，所以两者的`行为必须一致`，覆写这两个方法需要遵循一定的原则。可以从`业务的角度`考虑使用对象的唯一特征，比如 ID 等，或者使用它的`全部字段来进行计算`得到一个整数的哈希值。一般，我不会直接覆写该方法，除非业务特征非常明显。因为一旦修改之后，它的作用范围将是全局的。我们还可以通过 `IDEA 的 generate` 直接生成该方法。\n\n**问题：Object 都有哪些方法？**\n\n1. `wait() & notify()`, 用来对线程进行控制，以让当前线程等待，直到其他线程调用了 `notify()/notifyAll()` 方法。`wait()` 发生等待的前提是当前线程获取了对象的锁（监视器）。调用该方法之后当前线程会释放获取到的锁，然后让出 CPU，进入等待状态。`notify/notifyAll()` 的执行只是唤醒沉睡的线程，而不会立即释放锁，锁的释放要看代码块的具体执行情况。\n2. `clone()` 与对象克隆相关的方法（深拷贝 & 浅拷贝的问题）\n3. `finilize()`\n4. `toString()`\n5. `equal() & hashCode()`\n\n### 1.6 字符串相关\n\n**问题：StringBuffer 与 StringBuilder 的区别？**\n\n前者是线程安全的，每个方法上面都使用 synchronized 关键字进行了加锁，后者是非线程安全的。一般情况下使用 StringBuilder 即可，因为非多线程环境进行加锁是一种没有必要的开销。\n\n**问题：对 Java 中 String 的了解**\n\n1. String `不是基本数据类型`。\n2. String 是`不可变`的，JVM 使用`字符串池来`存储所有的字符串对象。\n3. 使用 `new 创建字符串`，这种方式创建的字符串对象不存储于字符串池。我们可以调用`intern()` 方法将该字符串对象存储在字符串池，如果字符串池已经有了同样值的字符串，则返回引用。`使用双引号直接创建字符串`的时候，JVM 先去字符串池找有没有值相等字符串，如果有，则返回找到的字符串引用；否则创建一个新的字符串对象并存储在字符串池。\n\n**问题：String 为什么要设计成不可变的？**\n\n1. `线程安全`：由于 String 是不可变类，所以在多线程中使用是安全的，我们不需要做任何其他同步操作。\n2. String 是不可变的，它的值也不能被改变，所以用来存储数据密码很`安全`。\n3. `复用/节省堆空间`：实际在 Java 的开发当中 String 是使用最为频繁的类之一，通过 dump 的堆可以看出，它经常占用很大的堆内存。因为 java 字符串是不可变的，可以在 java 运行时节省大量 java `堆`空间。不同的字符串变量可以引用池中的相同的字符串。如果字符串是可变得话，任何一个变量的值改变，就会反射到其他变量，那字符串池也就没有任何意义了。\n\n**问题：常见编码方式有哪些？ Utf-8, Unicode, ASCII**    \n**问题：Utf-8 编码中的中文占几个字节？**\n\nUTF-8 编码把一个 Unicode 字符根据不同的数字大小编码成 1-6 个字节，常用的英文字母被编码成 1 个字节，汉字通常是 3 个字节，只有很生僻的字符才会被编码成 4-6 个字节。\n\n### 1.7 线程控制\n\n**问题：开启线程的三种方式，run() 和 start() 方法区别**      \n**问题：如何保证线程安全？（太泛，synchronized, voliate, Lock, 线程安全集合）**    \n**问题：如何保证多线程读写文件的安全？**\n\n线程启动的三种方式：\n\n```java\n    //  方式 1：Thread 覆写 run() 方法；\n    private class MyThread extends Thread {\n        @Override\n        public void run() {\n            // 业务逻辑\n        }\n    }\n\n    // 方式 2：Thread + Runnable\n    new Thread(new Runnable() {\n        public void run() {\n            // 业务逻辑\n        }\n    }).start();\n\n    // 方式 3：ExectorService + Callable\n    ExecutorService executor = Executors.newFixedThreadPool(5);\n    List<Future<Integer>> results = new ArrayList<Future<Integer>>();\n    for (int i=0; i<5; i++) {\n        results.add(executor.submit(new CallableTask(i, i)));\n    }\n```\n\n线程启动，`run()` 和 `start()` 方法区别：\n\n`start()` 会调用 native 的 `start()` 方法，然后 `run()` 方法会被回调，此时 `run()` 异步执行；如果直接调用 `run()`，它会使用默认的实现（除非覆写了），并且会在当前线程中执行，此时 Thread 如同一个普通的类。Thread 的 run() 方法的默认实现如下：\n\n```java\n    private Runnable target;\n    public void run() {\n        if (target != null)  target.run();\n    }\n```\n\n**问题：线程如何关闭，以及如何防止线程的内存泄漏？**     \n\n有两种方式可以用来关闭线程。一种是使用中断标志位进行判断。当需要停止线程的时候，调用线程的 `interupt()` 方法即可。这种情况下需要注意的地方是，当线程处于阻塞状态的时候调用了中断方法，此时会抛出一个异常，并将中断标志位复位。此时，我们是无法退出线程的。所以，我们需要同时考虑一般情况和线程处于阻塞时中断两种情况。另一个方案是使用一个 volatile 类型的布尔变量，使用该变量来判断是否应该结束线程。两种方案的示例代码如下：\n\n```java\n    // 方式 1：使用中断标志位\n    @Override\n    public void run() {\n        try {\n            while (!isInterrupted()) {\n                // do something\n            }\n        } catch (InterruptedException ie) {  \n            // 线程因为阻塞时被中断而结束了循环\n        }\n    }\n\n    private static class MyRunnable2 implements Runnable {\n        // 注意使用 volatile 修饰\n        private volatile boolean canceled = false;\n\n        @Override\n        public void run() {\n            while (!canceled) {\n                // do something\n            }\n        }\n\n        public void cancel() {\n            canceled = true;\n        }\n    }\n```\n\n防止线程内存泄漏：\n\n1. 在 Activity 等中使用线程的时候，将线程定义成`静态内部类`，非静态内部类会持有外部类的匿名引用；\n2. 当需要在线程中调用 Activity 的方法的时候，使用 `WeakReference` 引用 Activity；\n3. 或者当 Activity 需要结束的时候，在 `onDestroy()` 方法中终止线程。\n\n**问题：Java 线程池、线程池的几个核心参数的意义？**    \n**问题：多线程：怎么用、有什么问题要注意；Android 线程上限？然后提到线程池的上限** \n\nAndroid 中并没有明确规定可以创建的`线程的数量`，但是每个进程的资源是有限的，线程本身会占有一定的资源，所以受内存大小的限制，会有数量的上限。通常，我们在使用线程或者线程池的时候，不会创建太多的线程。线程池的大小经验值应该这样设置：（其中 N 为 CPU 的核数）\n\n1. 如果是 `CPU 密集型`应用，则线程池大小设置为 `N + 1`；(大部分时间在计算)\n2. 如果是 `IO 密集型`应用，则线程池大小设置为 `2N + 1`；(大部分时间在读写，Android)\n\n下面是 Android 中的 AysncTask 中创建线程池的代码（创建线程池的核心参数的说明已经家在了注释中），\n\n```java\n    // CPU 的数量\n    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();\n    // 核心线程的数量：只有提交任务的时候才会创建线程，当当前线程数量小于核心线程数量，新添加任务的时候，会创建新线程来执行任务\n    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));\n    // 线程池允许创建的最大线程数量：当任务队列满了，并且当前线程数量小于最大线程数量，则会创建新线程来执行任务\n    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;\n    // 非核心线程的闲置的超市时间：超过这个时间，线程将被回收，如果任务多且执行时间短，应设置一个较大的值\n    private static final int KEEP_ALIVE_SECONDS = 30;\n\n    // 线程工厂：自定义创建线程的策略，比如定义一个名字\n    private static final ThreadFactory sThreadFactory = new ThreadFactory() {\n        private final AtomicInteger mCount = new AtomicInteger(1);\n\n        public Thread newThread(Runnable r) {\n            return new Thread(r, \"AsyncTask #\" + mCount.getAndIncrement());\n        }\n    };\n\n    // 任务队列：如果当前线程的数量大于核心线程数量，就将任务添加到这个队列中\n    private static final BlockingQueue<Runnable> sPoolWorkQueue =\n            new LinkedBlockingQueue<Runnable>(128);\n\n    public static final Executor THREAD_POOL_EXECUTOR;\n\n    static {\n        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(\n                /*corePoolSize=*/ CORE_POOL_SIZE,\n                /*maximumPoolSize=*/ MAXIMUM_POOL_SIZE, \n                /*keepAliveTime=*/ KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,\n                /*workQueue=*/ sPoolWorkQueue, \n                /*threadFactory=*/ sThreadFactory\n                /*handler*/ defaultHandler); // 饱和策略：AysncTask 没有这个参数\n        threadPoolExecutor.allowCoreThreadTimeOut(true);\n        THREAD_POOL_EXECUTOR = threadPoolExecutor;\n    }\n```\n\n饱和策略：任务队列和线程池都满了的时候执行的逻辑，Java 提供了 4 种实现；    \n\n1. 当调用了线程池的 `prestartAllcoreThread()` 方法的时候，线程池会提前启动并创建所有核心线程来等待任务；\n2. 当调用了线程池的 `allowCoreThreadTimeOut()` 方法的时候，超时时间到了之后，闲置的核心线程也会被移除。\n\n**问题：wait() 与 sleep() 的区别**\n\n1. wait()、notify() 和 notifyAll() 方法是 `Object 的 final 方法`，无法被重写。\n2. wait() 使当前线程阻塞，直到`接到通知或被中断`为止。前提是`必须先获得锁`，一般配合 synchronized 关键字使用，在 synchronized 同步代码块里使用 wait()、notify() 和 notifyAll() 方法。如果调用 wait() 或者 notify() 方法时，线程并未获取到锁的话，则会抛出 `IllegalMonitorStateException` 异常。再次获取到锁，当前线程才能从 wait() 方法处成功返回。\n3. 由于 wait()、notify() 和 notifyAll() 在 synchronized 代码块执行，说明当前线程一定是获取了锁的。当线程执行 wait() 方法时候，`会释放当前的锁`，然后让出 CPU，进入等待状态。只有当 notify()/notifyAll() 被执行时候，才会唤醒一个或多个正处于等待状态的线程，然后继续往下执行，直到执行完 synchronized 代码块或是中途遇到 wait()，再次释放锁。也就是说，notify()/notifyAll() 的执行只是`唤醒沉睡的线程，而不会立即释放锁`，锁的释放要看代码块的具体执行情况。所以在编程中，尽量在使用了 notify()/notifyAll() 后立即退出临界区，以唤醒其他线程。\n4. wait() 需要被 `try catch` 包围，`中断`也可以使 wait 等待的线程唤醒。\n5. notify() 和 wait() 的`顺序`不能错，如果 A 线程先执行 notify() 方法，B 线程再执行 wait() 方法，那么 B 线程是无法被唤醒的。\n6. notify() 和 notifyAll() 的区别：notify() 方法`只唤醒一个等待（对象的）线程`并使该线程开始执行。所以如果有多个线程等待一个对象，这个方法只会唤醒其中一个线程，选择哪个线程取决于操作系统对多线程管理的实现。notifyAll() 会`唤醒所有等待 (对象的) 线程`，尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒，推荐使用 notifyAll() 方法。比如在生产者-消费者里面的使用，每次都需要唤醒所有的消费者或是生产者，以判断程序是否可以继续往下执行。\n\n对于 `sleep()` 和 `wait()` 方法之间的区别，总结如下，\n\n1. 所属类不同：sleep() 方法是 `Thread 的静态方法`，而 wait() 是 `Object 实例方法`。\n2. 调用位置不同：wait() 方法必须要在`同步方法或者同步块`中调用，也就是必须已经获得对象锁。而 sleep() 方法没有这个限制可以在任何地方种使用。\n3. 锁释放不同：wait() 方法会`释放占有的对象锁`，使得该线程进入等待池中，等待下一次获取资源。而 sleep() 方法只是会`让出 CPU 并不会释放掉对象锁`；\n4. 线程唤醒不同：sleep() 方法在休眠时间达到后如果`再次获得 CPU 时间片`就会继续执行，而 wait() 方法必须等待 `Object.notift()/Object.notifyAll()` 通知后，才会离开等待池，并且再次获得 CPU 时间片才会继续执行。\n\n**问题：线程的状态**\n\n![线程的状态](https://github.com/Shouheng88/Awesome-Java/blob/master/Java%E8%AF%AD%E8%A8%80%E5%92%8CJDK%E6%BA%90%E7%A0%81/res/Thread_states.jpg?raw=true)\n\n1. `新建 (NEW)`：新创建了一个线程对象。\n2. `可运行 (RUNNABLE)`：线程对象创建后，其他线程(比如 main 线程）调用了该对象的 `start()` 方法。该状态的线程位于可运行线程池中，等待被线程调度选中，获取 CPU 的使用权 。\n3. `运行 (RUNNING)`：RUNNABLE 状态的线程获得了 CPU 时间片（timeslice） ，执行程序代码。\n4. `阻塞 (BLOCKED)`：阻塞状态是指线程因为某种原因放弃了 CPU 使用权，也即让出了 CPU timeslice，暂时停止运行。直到线程进入 RUNNABLE 状态，才有机会再次获得 CPU timeslice 转到 RUNNING 状态。阻塞的情况分三种： \n    1. `等待阻塞`：RUNNING 的线程执行 `o.wait()` 方法，JVM 会把该线程放入等待队列 (waitting queue) 中。\n    2. `同步阻塞`：RUNNING 的线程在获取对象的同步锁时，若该同步锁被别的线程占用，则 JVM 会把该线程放入锁池 (lock pool) 中。\n\t3. `其他阻塞`：RUNNING 的线程执行 `Thread.sleep(long)` 或 `t.join()` 方法，或者发出了 I/O 请求时，JVM 会把该线程置为阻塞状态。当 `sleep()` 状态超时、`join()` 等待线程终止或者超时、或者 I/O 处理完毕时，线程重新转入 RUNNABLE 状态。\n5. `死亡` (DEAD)：线程 `run()`、`main()` 方法执行结束，或者因异常退出了 `run()` 方法，则该线程结束生命周期。死亡的线程不可再次复生。\n\n**问题：死锁，线程死锁的 4 个条件？**\n**问题：死锁的概念，怎么避免死锁？**\n\n当两个线程彼此占有对方需要的资源，同时彼此又无法释放自己占有的资源的时候就发生了死锁。发生死锁需要满足下面四个条件，\n\n1. `互斥`：某种资源一次只允许一个进程访问，即该资源一旦分配给某个进程，其他进程就不能再访问，直到该进程访问结束。（一个筷子只能被一个人拿）\n2. `占有且等待`：一个进程本身占有资源（一种或多种），同时还有资源未得到满足，正在等待其他进程释放该资源。（每个人拿了一个筷子还要等其他人放弃筷子）\n3. `不可抢占`：别人已经占有了某项资源，你不能因为自己也需要该资源，就去把别人的资源抢过来。（别人手里的筷子你不能去抢）\n4. `循环等待`：存在一个进程链，使得每个进程都占有下一个进程所需的至少一种资源。（每个人都在等相邻的下一个人放弃自己的筷子）\n\n产生死锁需要四个条件，那么，只要这四个条件中至少有一个条件得不到满足，就不可能发生死锁了。由于互斥条件是非共享资源所必须的，不仅不能改变，还应加以保证，所以，主要是破坏产生死锁的其他三个条件。\n\n1. `破坏占有且等待的问题`：允许进程只获得运行初期需要的资源，便开始运行，在运行过程中逐步释放掉分配到的已经使用完毕的资源，然后再去请求新的资源。\n2. `破坏不可抢占条件`：当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时，它必须释放已经保持的所有资源，待以后需要使用的时候再重新申请。释放已经保持的资源很有可能会导致进程之前的工作实效等，反复的申请和释放资源会导致进程的执行被无限的推迟，这不仅会延长进程的周转周期，还会影响系统的吞吐量。\n3. `破坏循环等待条件`：可以通过定义资源类型的线性顺序来预防，可将每个资源编号，当一个进程占有编号为i的资源时，那么它下一次申请资源只能申请编号大于 i 的资源。\n\n**问题：synchronized 的实现原理**    \n**问题：如何实现线程同步？synchronized, lock, 无锁同步, voliate, 并发集合，同步集合**\n\n`sychronized 代码块的同步原理`：Java 虚拟机中的同步基于进入和退出管程 (`Monitor`) 对象实现，无论是显式同步 (有明确的 `monitorenter` 和 `monitorexit` 指令，即同步代码块)，还是隐式同步都是如此。进入 monitorenter 时 monitor 中的计数器 count 加 1，释放当前持有的 monitor，count 自减 1. 反编译代码之后经常看到两个 monitorexit 指令对应一个 monitorenter，这是用来防止程序执行过程中出现异常的。虚拟机需要保证即使程序允许中途出了异常，锁也一样可以被释放（执行第二个  monitorexit）。\n\n`sychronized 方法的同步原理`：对同步方法，JVM 可以从`方法常量池`中的`方法表结构`(method_info Structure) 中的 `ACC_SYNCHRONIZED 访问标志`区分一个方法是否同步方法。当调用方法时，调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置，如果设置了，执行线程将先持有 monitor，然后再执行方法，最后再方法完成 (无论是正常完成还是非正常完成) 时释放 monitor. 在方法执行期间，其他任何线程都无法再获得同一个 monitor. 如果一个同步方法执行期间抛出了异常，并且在方法内部无法处理此异常，那这个同步方法所持有的 monitor 将在异常抛到同步方法之外时自动释放。\n\n`sychronized 原理的底层实现原理`：在 Java 对象的对象头中，有一块区域叫做 `MarkWord`，其中存储了`重量级锁 sychronized 的标志位`，其指针指向的是 `monitor 对象`。每个对象都存在着一个 monitor 与之关联。在 monitor 的数据结构中定义了两个队列，`_WaitSet 和 _EntryList`. 当多个线程同时访问一段同步代码时，首先会进入 _EntryList 集合，当线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 owner 变量设置为当前线程，同时 monitor 中的计数器 count 加 1，若线程调用 wait() 方法，将释放当前持有的 monitor，owner 变量恢复为 null，count 自减 1，同时该线程进入 _WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor (锁)并复位变量的值，以便其他线程进入获取 monitor (锁)。\n\n由此看来，monitor 对象存在于每个 Java 对象的对象头中(存储的指针的指向)，synchronized 锁便是通过这种方式获取锁的，也是为什么 Java 中任意`对象可以作为锁的`原因，同时也是 notify()/notifyAll()/wait() 等方法存在于顶级对象 Object 中的原因。\n\n当然，从 MarkWord 的结构中也可以看出 Java 对 sychronized 的优化：Java 6 之后，为了减少获得锁和释放锁所带来的性能消耗，引入了轻量级锁和偏向锁，锁效率也得到了优化。\n\n**问题：synchronized 与 Lock 的区别**\n\n1. `等待可中断`：对于 Lock，当持有锁的线程长期不释放锁的时候，正在等待的线程可以选择放弃等待；（两种方式获取锁的时候都会使计数+1，但是方式不同，所以重入锁可以终端）\n2. `公平锁`：当多个线程等待同一个锁时，公平锁会按照申请锁的时间顺序来依次获得锁；而非公平锁，当锁被释放时任何在等待的线程都可以获得锁（不论时间尝试获取的时间先后）。sychronized 只支持非公平锁，Lock 可以通过构造方法指定使用公平锁还是非公平锁。\n3. `锁可以绑定多个条件`：ReentrantLock 可以绑定多个 Condition 对象，而 sychronized 要与多个条件关联就不得不加一个锁，ReentrantLock 只要多次调用 newCondition 即可。\n\n**问题：ReentrantLock 的内部实现**\n**问题：CAS 介绍**\n**问题：Lock 的实现原理**\n\nReentrantLock 的实现是基于 AQS（同步器），同步器设计的思想是 CAS. 同步器中维护了一个链表，借助 CAS 的思想向链表中增删数据。其底层使用的是 `sun.misc.Unsafe` 类中的方法来完成 CAS 操作的。在 ReentrantLock 中实现两个 AQS 的子类，分别是 `NonfairSync` 和 `FairSync`. 也就是用来实现公平锁和非公平锁的关键。当我们使用构造方法获取 ReentrantLock 实例的时候，可以通过一个布尔类型的参数指定使用公平锁还是非公平锁。在实现上， `NonfairSync` 和 `FairSync` 的区别仅仅是，在当前线程获取到锁之前，是否会从上述队列中判断是否存在比自己更早申请锁的线程。对于公平锁，当存在这么一个线程的话，那么当前线程获取锁失败。当当前线程获取到锁的时候，也会使用一个 CAS 操作将锁获取次数 +1. 当线程再次获取锁的时候，会根据线程来进行判断，如果当前持有锁的线程是申请锁的线程，那么允许它再次获取锁，以此来实现锁的可重入。\n\n所谓 CAS 就是 `Compare-And-Swape`，类似于`乐观加锁`。但与我们熟知的乐观锁不同的是，它在判断的时候会涉及到 3 个值：`“新值”、“旧值” 和 “内存中的值”`，在实现的时候会使用一个无限循环，每次拿 “旧值” 与 “内存中的值” 进行比较，如果两个值一样就说明 “内存中的值” 没有被其他线程修改过；否则就被修改过，需要重新读取内存中的值为 “旧值”，再拿 “旧值” 与 “内存中的值” 进行判断。直到 “旧值” 与 “内存中的值” 一样，就把 “新值” 更新到内存当中。\n\n这里要注意上面的 CAS 操作是分 3 个步骤的，但是这 3 个步骤必须一次性完成，因为不然的话，当判断 “内存中的值” 与 “旧值” 相等之后，向内存写入 “新值” 之间被其他线程修改就可能会得到错误的结果。JDK 中的 `sun.misc.Unsafe` 中的 compareAndSwapInt 等一系列方法 Native 就是用来完成这种操作的。另外还要注意，上面的 CAS 操作存在一些问题：\n\n1. `ABA 的问题`，也就是说当内存中的值被一个线程修改了，又改了回去，此时当前线程看到的值与期望的一样，但实际上已经被其他线程修改过了。想要解决 ABA 的问题，则可以使用传统的互斥同步策略。\n2. CAS 还有一个问题就是可能会`自旋时间过长`。因为 CAS 是非阻塞同步的，虽然不会将线程挂起，但会自旋（无非就是一个死循环）进行下一次尝试，如果这里自旋时间过长对性能是很大的消耗。\n3. CAS `只能保证一个共享变量的原子性`，当存在多个变量的时候就无法保证。一种解决的方案是将多个共享变量打包成一个，也就是将它们整体定义成一个对象，并用 CAS 保证这个整体的原子性，比如 `AtomicReference`。\n\n**问题：volatile 原理和用法**\n\nvoliate 关键字的两个作用\n\n1. `保证变量的可见性`：当一个被 voliate 关键字修饰的变量被一个线程修改的时候，其他线程可以立刻得到修改之后的结果。当写一个 volatile 变量时，JMM 会把该线程对应的工作内存中的共享变量值刷新到主内存中，当读取一个 volatile 变量时，JMM 会把该线程对应的工作内存置为无效，那么该线程将只能从主内存中重新读取共享变量。\n2. `屏蔽指令重排序`：指令重排序是编译器和处理器为了高效对程序进行优化的手段，它只能保证程序执行的结果时正确的，但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题，但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入 voliate，就是为了防止指令重排序。\n\nvolatile 是通过`内存屏障(Memory Barrier）` 来实现其在 JMM 中的语义的。内存屏障，又称内存栅栏，是一个 CPU 指令，它的作用有两个，一是保证特定操作的执行顺序，二是保证某些变量的内存可见性。如果在指令间插入一条内存屏障则会告诉编译器和 CPU，不管什么指令都不能和这条 Memory Barrier 指令重排序。Memory Barrier 的另外一个作用是强制刷出各种 CPU 的缓存数据，因此任何 CPU 上的线程都能读取到这些数据的最新版本。\n\n**问题：手写生产者/消费者模式**（见文末）\n\n### 1.8 并发包\n\n**问题：ThreadLocal 的实现原理？**\n\nThreadLocal 通过将每个线程自己的局部变量存在自己的内部来实现线程安全。使用它的时候会定义它的静态变量，每个线程看似是从 TL 中获取数据，而实际上 TL 只起到了键值对的键的作用，实际的数据会以哈希表的形式存储在 Thread 实例的 Map 类型局部变量中。当调用 TL 的 `get()` 方法的时候会使用 `Thread.currentThread()` 获取当前 Thread 实例，然后从该实例的 Map 局部变量中，使用 TL 作为键来获取存储的值。Thread 内部的 Map 使用线性数组解决哈希冲突。\n\n**问题：并发集合了解哪些？**\n\n1. ConcurrentHashMap：线程安全的 HashMap，对桶进行加锁，降低锁粒度提升性能。\n2. ConcurrentSkipListMap：跳表，自行了解，给跪了……\n3. ConCurrentSkipListSet：借助 ConcurrentSkipListMap 实现\n4. CopyOnWriteArrayList：读多写少的 ArrayList，写的时候加锁\n5. CopyOnWriteArraySet：借助 CopyOnWriteArrayList 实现的……\n6. ConcurrentLinkedQueue：无界且线程安全的 Queue，其 `poll()` 和 `add()` 等方法借助 CAS 思想实现。锁比较轻量。\n\n### 1.9 输入输出\n\n**问题：NIO**\n**问题：多线程断点续传原理**\n\n断点续传和断点下载都是用的用的都是 RandomAccessFile，它可以从指定的位置开始读取数据。断点续传是由服务器给客户端一个已经上传的位置标记position，然后客户端再将文件指针移动到相应的 position，通过输入流将文件剩余部分读出来传输给服务器。\n\n如果要使用多线程来实现断点续传，那么可以给每个线程分配固定的字节的文件，分别去读，然后分别上传到服务器。\n\n## 2、Kotlin 相关\n\n**问题：对 Kotlin 协程的了解**\n\n协程实际上就是极大程度的复用线程，通过让线程满载运行，达到最大程度的利用 CPU，进而提升应用性能。相比于线程，协程不需要进行线程切换，和多线程比，线程数量越多，协程的性能优势就越明显。第二大优势就是不需要多线程的锁机制，因为只有一个线程，也不存在同时写变量冲突，在协程中控制共享资源不加锁，只需要判断状态就好了，所以执行效率比多线程高很多。\n\n协程和线程，都能用来实现异步调用，但是这两者之间是有本质区别的：\n\n1. 协程是`编译器`级别的，线程是系统级别的。协程的切换是`由程序来控制`的，线程的切换是由操作系统来控制的。\n2. 协程是`协作式`的，线程是`抢占式`的。协程是由程序来控制什么时候进行切换的，而线程是有操作系统来决定线程之间的切换的。\n3. 一个线程可以包含多个协程。Java 中，多线程可以充分利用多核 cpu，协程是在`一个线程`中执行。4. 协程适合 `IO 密集型` 的程序，多线程适合 `计算密集型` 的程序(适用于多核 CPU 的情况)。当你的程序大部分是文件读写操作或者网络请求操作的时候，这时你应该首选协程而不是多线程，首先这些操作大部分不是利用 CPU 进行计算而是等待数据的读写，其次因为协程执行效率较高，子程序切换不是线程切换，是由程序自身控制，因此，没有线程切换的开销，和多线程比，线程数量越多，协程的性能优势就越明显。\n5. 使用协程可以`顺序调用`异步代码，`避免回调地狱`。\n\n**问题：Kotlin 跟 Java 比，kotlin 具有哪些优势？**\n\nKotlin 是一门基于 JVM 的语言，它提供了非常多便利的语法特性。如果说 Kotlin 为什么那么优秀的话，那只能说是因为它站在了 Java 的肩膀上。学习了一段时间之后，你会发现它的许多语法的设计非常符合我们实际开发中的使用习惯。\n\n1. `默认无法被继承`：比如，对于一个类，通常我们不会去覆写它。尤其是 Java Web 方向，很多的类用来作为 Java Bean，它们没有特别多的继承关系。而 Kotlin 中的类默认就是不允许继承的，想允许自己的类被继承，你还必须显式地使用 open 关键字指定。\n\n2. `省略 setter 和 getter`：对于 Java Bean，作为一个业务对象，它会有许多的字段。按照 Java 中的处理方式，我们要为它们声明一系列的 setter 和 getter 方法。然后，获取属性的时候必须使用 setter 和 getter 方法。导致我们的代码中出现非常多的括号。而使用 Kotlin 则可以直接对属性进行赋值，显得优雅地多。IDEA 插件 Lombok 就是用来实现类似的功能。\n\n3. `条件默认 break`：再比如 Java 中使用 switch 的时候，我们通常会在每个 case 后面加上 break，而 kotlin 默认帮助我们 break，这样就节省了很多的代码量。\n\n3. `空指针的处理`：另外 Kotlin 非常优秀的地方在于对 NPE 的控制。在 Android 开发中，我们可以使用 @NoneNull 和 @Nullable 注解来标明某个字段是否可能为空。在 Java 中默认字段是空的，并且没有任何提示。你一个不留神可能就导致了 NPE，但 Kotlin 中就默认变量是非空的，你想让它为空必须单独声明。这样，对于可能为空的变量就给了我们提示的作用，我们知道它可能为空，就会去特意对其进行处理。对于可能为空的类，Kotlin 定义了如下的规则，使得我们处理起来 NPE 也变得非常简单：\n\n    1. 使用 `?` 在类型的后面则说明这个变量是可空的；\n    2. 安全调用运算符 `?.`，以 `a?.method()` 为例，当 a 不为 null 则整个表达式的结果是 `a.method()` 否则是 null；\n    3. Elvis 运算符 `?:`，以 `a ?: \"A\"` 为例，当 a 不为 null 则整个表达式的结果是 a，否则是 “A”；\n    4. 安全转换运算符 `as?`，以 `foo as? Typ`e 为例，当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例，否则返回 null；\n    5. 非空断言 `!!`，用在某个变量后面表示断言其非空，如 `a!!`；\n    6. let 表示对调用 let 的实例进行某种运算，如 `val b = \"AA\".let { it + \"A\" }` 返回 “AAA”；\n\n诸如此类，很多时候，我觉得 Java 设计的一些规则对人们产生了误导，实际开发中并不符合我们的使用习惯。而 Kotlin 则是根据多年来人们使用 Java 的经验，简化了许多的调用，更加符合我们使用习惯。所以说，Kotlin 之所以强大是因为站在 Java 的肩膀上。\n\n## 3、设计模式\n\n**问题：谈谈你对 Android 设计模式的理解**    \n**问题：项目中常用的设计模式有哪些？**\n\n1. `工厂+策略`：用来创建各种实例，比如，美国一个实现，中国一个实现的情形；\n2. `观察者`：一个页面对事件进行监听，注册，取消注册，通知；\n3. `单例`：太多，为了延迟初始化；\n4. `构建者`：类的参数太多，为了方便调用；\n5. `适配器`：RecyclerView 的适配器；\n6. `模板`：设计一个顶层的模板类，比如抽象的 Fragment 或者 Activity 等，但是注意组合优于继承，不要过度设计；\n7. `外观`：相机模块，Camera1 和 Camera2，封装其内部实现，统一使用 CameraManager 的形式对外提供方法。\n\n**问题：手写观察者模式？**\n\n观察者设计模式类似于我们经常使用的接口回调，下面的代码中在观察者的构造方法中订阅了主题，其实这个倒不怎么重要，什么时候订阅都可以。核心的地方就是主题中维护的这个队列，需要通知的时候调一下通知的方法即可。另外，如果在多线程环境中还要考虑如何进行线程安全控制，比如使用线程安全的集合等等。下面只是一个非常基础的示例程序，了解设计思想，用的时候可以灵活一些，不必循规蹈矩。（见文末附录）\n\n**问题：手写单例模式，懒汉和饱汉**\n\n```java\n    // 饱汉：就是在调用单例方法的时候，实例已经初始化过了\n    public class Singleton {\n        private static Singleton singleton = new Singleton();\n\n        private Singleton() {}\n\n        public static Singleton getInstance() {\n            return singleton;\n        }\n    }\n\n    // 懒汉：在调用方法的时候才进行初始化\n    public class Singleton {\n        private volatile static Singleton singleton;\n\n        private Singleton() {}\n\n        public static Singleton getInstance() {\n            if (singleton == null) {\n                sychronized(Singleton.class) {\n                    if (singleton == null) {\n                        singleton = new Singleton();\n                    }\n                }\n            }\n            return singleton;\n        }\n    } \n```\n\n另外，单例需要注意的问题是：\n\n1. 如果用户使用反射进行初始化怎么办？可以在创建第二个实例的时候抛出异常；\n2. 如果用户使用 Java 的序列化机制反复创建单例呢？将所有的实例域设置成 transient 的，然后覆写 `readResolve()` 方法并返回单例。\n\n另外，单实例太多的时候可以想办法使用一个 Map 将它们存储起来，然后通过一种规则从哈希表中取出，这样就没必要声明一大堆的单例变量了。\n\n**问题：适配器模式、装饰者模式、外观模式、代理模式的异同？（这个几个设计模式比较容易混）**\n\n四个设计模式相同的地方是，它们都需要你传入一个类，然后内部使用你传入的这个类来完成业务逻辑。\n\n我们以字母 A，B，C 来表示 3 种不同的类（某种东西）。\n\n外观模式要隐藏内部的差异，提供一个一致的对外的接口 X，那么让定义 3 个类 AX, BX, CX 并且都实现 X 接口，其中分别引用 A, B, C 按照各自的方式实现 X 接口的方法即可。以相机开发为例，Camera1 和 Camera2 各有自己的实现方式，定义一个统一的接口和两个实现类。\n\n假如现在有一个类 X，其中引用到了接口 A 的实现 AX. AX 的逻辑存在点问题，我们想把它完善一下。我们提供了 3 种方案，分别是 A1, A2 和 A3. 那么此时，我们让 A1, A2 和 A3 都实现 A 接口，然后其中引用 AX 完成业务，在实现的 A 接口的方法中分别使用各自的方案进行优化即可。这种方式，我们对 AX 进行了修饰，使其 A1, A2 和 A3 可以直接应用到 X 中。\n\n对于适配器模式，假如现在有一个类 X，其中引用到了接口 A. 现在我们不得不使用 B 来完成 A 的逻辑。因为 A 和 B 属于两个不同的类，所以此时我们需要一个适配器模式，让 A 的实现 AX 引用 B 的实现 BX 完成 A 接口的各个方法。\n\n外观模式的目的是隐藏各类间的差异性，提供一致的对外接口。装饰者模式对外的接口是一致的，但是内部引用的实例是同一个，其目的是对该实例进行拓展，使其具有多种功能。所以，前者是多对一，后者是一对多的关系。而适配器模式适用的是两个不同的类，它使用一种类来实现另一个类的功能，是一对一的。相比之下，代理模式也是用一类来完成某种功能，并且一对一，但它是在同类之间，目的是为了增强类的功能，而适配器是在不同的类之间。装饰者和代理都用来增强类的功能，但是装饰者装饰之后仍然是同类，可以无缝替换之前的类的功能。而代理类被修饰之后已经是代理类了，是另一个类，无法替换原始类的位置。\n\n**问题：设计模式相关（例如Android中哪里使用了观察者模式，单例模式相关）**\n\n## 参考文献：\n\n（[《Android 内存缓存框架 LruCache 的源码分析》](https://juejin.im/post/5bea581be51d451402494af2)）\n（[《Java 注解及其在 Android 中的应用》](https://juejin.im/post/5b824b8751882542f105447d)）\n[《死锁的四个必要条件和解决办法》](https://blog.csdn.net/guaiguaihenguai/article/details/80303835#commentBox)\n*(关于 sychronized 的底层实现原理可以参考笔者的文章：[并发编程专题 3：synchronized](https://blog.csdn.net/github_35186068/article/details/87732560))*\n参考 [《并发编程专题-5：生产者和消费者模式》](https://blog.csdn.net/github_35186068/article/details/87537570) 中的三种写法。\n([《ThreadLocal的使用及其源码实现》](https://juejin.im/post/5b44cd7c6fb9a04f980cb065))\n参考：[是继续Rxjava，还是应该试试Kotlin的协程 - Android架构的文章 - 知乎](https://zhuanlan.zhihu.com/p/53271210)\n*（了解更多关于观察者设计模式的内容，请参考文章：[设计模式解析：观察者模式](https://juejin.im/post/5b60659df265da0f793a85ba)）*\n*（了解更多关于单例设计模式的内容，请参考文章：[设计模式-4：单例模式](https://blog.csdn.net/github_35186068/article/details/78606032)）*\n\n## 附录：\n\n### 1、生产者和消费者模式的三种写法\n\n#### 第一种写法：基于 wait(), notify(), notifyAll()\n\n```java\n    // 生产者\n    private static class Consumer implements Runnable {\n        private List<Object> products;\n\n        // 传入的对象是产品，也就是说，生产者和消费者通过产品建立联系\n        public Consumer(List<Object> products) {\n            this.products = products;\n        }\n\n        public void run() {\n            while (true) { // 使用循环来不断消费\n                synchronized (products) { // 对产品加锁 products\n                    while (products.isEmpty()) { // 1. 没有产品了\n                        try { // 调用 wait() 的时候使用 tru...catch 防止线程终端\n                            products.wait(); // 已经没有产品可以消费了\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    products.remove(0); // 消费一个\n                    products.notifyAll(); // 通知其他线程\n                    System.out.println(\"Eat one. Left : \" + products.size());\n                }\n            }\n        }\n    }\n\n    // 消费者\n    private static class Producer implements Runnable {\n        private final int max; // 产品的上限\n        private List<Object> products;\n\n        // 参数是产品的上限和产品列表（理解成仓库和仓库的最大容量亦可）\n        public Producer(int max, List<Object> products) {\n            this.max = max;\n            this.products = products;\n        }\n\n        public void run() {\n            while (true) {\n                synchronized (products) {\n                    while (products.size() > max) { // 2. 大于上限就停止生产\n                        try {\n                            products.wait(); // 暂停生成\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    products.add(new Object()); // 生成一个\n                    products.notifyAll(); // 唤醒\n                    System.out.println(\"Made one. Total : \" + products.size());\n                }\n            }\n        }\n    }\n\n    // 模拟 & 验证\n    public static void main(String...args) {\n        final int MAX_PRODUCTS = 20;\n        List<Object> products = new LinkedList<>();\n        Executor executor = Executors.newCachedThreadPool(); // 使用线程池\n        executor.execute(new Producer(MAX_PRODUCTS, products));\n        executor.execute(new Consumer(products));\n        executor.execute(new Consumer(products));\n    }\n```\n\n#### 第二种写法：基于 ReentrantLock\n\n```java\n    // 定义锁、仓库已满的条件和仓库为空的条件\n    private static ReentrantLock lock = new ReentrantLock();\n    private static final Condition full = lock.newCondition();\n    private static final Condition empty = lock.newCondition();\n\n    // 消费者\n    private static class Consumer implements Runnable {\n        private List<Object> products;\n\n        public Consumer(List<Object> products) {\n            this.products = products;\n        }\n\n        public void run() {\n            while (true) {\n                lock.lock(); // 加锁\n                try {\n                    while (products.isEmpty()) { // 没有可消费的产品\n                        try {\n                            empty.await(); // 没有可用的产品了，等待\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    products.remove(0); // 消费一个\n                    full.signalAll(); // 唤醒所有生产者\n                    empty.signalAll(); // 唤醒所有消费者\n                    System.out.println(\"Eat one. Left : \" + products.size());\n                } finally {\n                    lock.unlock(); // 释放锁\n                }\n            }\n        }\n    }\n\n    // 生产者\n    private static class Producer implements Runnable {\n        private final int max;\n        private List<Object> products;\n\n        public Producer(int max, List<Object> products) {\n            this.max = max;\n            this.products = products;\n        }\n\n        public void run() {\n            while (true) {\n                lock.lock(); // 加锁\n                try {\n                    while (products.size() > max) { // 已经达到了最大的产量\n                        try {\n                            full.await(); // 已达最大产量，等待\n                        } catch (InterruptedException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                    products.add(new Object()); // 生产一个\n                    full.signalAll(); \n                    empty.signalAll();\n                    System.out.println(\"Made one. Total : \" + products.size());\n                } finally {\n                    lock.unlock(); // 释放锁\n                }\n            }\n        }\n    }\n\n    public static void main(String...args) {\n        final int MAX_PRODUCTS = 20;\n        List<Object> products = new LinkedList<>();\n        Executor executor = Executors.newCachedThreadPool();\n        executor.execute(new Producer(MAX_PRODUCTS, products));\n        executor.execute(new Consumer(products));\n        executor.execute(new Consumer(products));\n    }\n```\n\n第三自恶法：基于 BlockingQueue\n\n```java\n    private static class Consumer implements Runnable {\n\n        private BlockingQueue<Object> products;\n\n        public Consumer(BlockingQueue<Object> products) {\n            this.products = products;\n        }\n\n        @Override\n        public void run() {\n            while (true) {\n                try {\n                    products.take(); // 取不到数据的时候自动阻塞\n                    System.out.println(\"Consumed one, Total \" + products.size());\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n\n    // 生产者\n    private static class Producer implements Runnable {\n\n        private BlockingQueue<Object> products;\n\n        public Producer(BlockingQueue<Object> products) {\n            this.products = products;\n        }\n\n        @Override\n        public void run() {\n            while (true) {\n                try {\n                    products.put(new Object()); // 当达到了最大数量的时候会阻塞\n                    System.out.println(\"Produced one, Total \" + products.size());\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n\n    public static void main(String...args) {\n        final int MAX_PRODUCTS = 20;\n        BlockingQueue<Object> products = new LinkedBlockingDeque<>(MAX_PRODUCTS);\n        Executor executor = Executors.newCachedThreadPool();\n        executor.execute(new Producer(products));\n        executor.execute(new Consumer(products));\n        executor.execute(new Consumer(products));\n    }\n```\n\n### 2、观察者模式\n\n```java\n    public class ConcreteSubject implements Subject {\n        private List<Observer> observers = new LinkedList<>(); // 维护观察者列表\n\n        @Override\n        public void registerObserver(Observer o) { // 注册一个观察者\n            observers.add(o);\n        }\n\n        @Override\n        public void removeObserver(Observer o) { // 移除一个观察者\n            int i = observers.indexOf(o);\n            if (i >= 0) {\n                observers.remove(o);\n            } \n        }\n\n        @Override\n        public void notifyObservers() { // 通知所有观察者主题的更新\n            for (Observer o : observers) {\n                o.method();\n            }\n        }\n    }\n\n    public class ConcreteObserver implements Observer {\n        private Subject subject; // 该观察者订阅的主题\n\n        public ConcreteObserver(Subject subject) {\n            this.subject = subject;\n            subject.registerObserver(this); // 将当前观察者添加到主题订阅列表中\n        }\n        \n        // 当主题发生变化的时候，主题会遍历观察者列表并通过调用该方法来通知观察者\n        @Override\n        public void method() {\n            // ...  \n        }\n    }\n```"
  },
  {
    "path": "笔试面试/Android高级面试_4_虚拟机相关.md",
    "content": "# Android 高级面试-4：虚拟机相关\n\n## 1、内存管理\n\n**问题：GC 回收策略**    \n**问题：Java 中内存区域与垃圾回收机制**    \n**问题：垃圾回收机制与调用 `System.gc()` 区别**   \n\n1. `标记-清除算法`：这种算法直接在内存中把需要回收的对象“抠”出来。效率不高，清除之后会产生内容碎片，造成内存不连续，当分配较大内存对象时可能会因内存不足而触发垃圾收集动作。\n2. `标记-整理算法`：类似于标记-清除算法，只是回收了之后，它要对内存空间进行整理，以使得剩余的对象占用连续的存储空间。\n3. `复制算法`：将内存分成两块，一次只在一块内存中进行分配，垃圾回收一次之后，就将该内存中的未被回收的对象移动到另一块内存中，然后将该内存一次清理掉。\n4. `分代收集算法`：根据对象存活周期的不同将内存划分成几块，然后根据其特点采用不同的回收算法。\n\n`System.gc()` 函数的作用只是提醒虚拟机：程序员希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行，而且具体什么时候进行是取决于具体的虚拟机的，不同的虚拟机有不同的对策。\n\n**问题：Java 中对象的生命周期**\n\n一个类从被加载到虚拟机内存到卸载的整个生命周期包括：`加载-验证-准备-解析-初始化-使用-卸载` 7 个阶段。其中 `验证-准备-解析` 3 个阶段称为连接。\n\n1. `加载`：发生在类被使用的时候，如果一个类之前没有被加载，那么就会执行加载逻辑，比如当使用new 创建类、调用静态类对象和使用反射的时候等。加载过程主要工作包括：1). 从磁盘或者网络中获取类的二进制字节流；2). 将该字节流的静态存储结构转换为方法取的运行时数据结构；3). 在内存中生成表示这个类的 Class 对象，作为方法区访问该类的各种数据结构的入口。\n2. `验证`：对加载的字节流中的信息进行各种校验以确保它符合 JVM 的要求。\n3. `准备`：正式为类变量分配内存并设置类变量的初始值。注意这里分配内存的只包括类变量，也就是静态的变量（实例变量会在对象实例化的时候分配在堆上），并且这里的设置初始值是指‘零值’，比如int类型的会被初始化为 0，引用类型的会被初始化为 null，即使你在代码中为其赋了值。\n4. `解析`：将常量池中的符号引用替换为直接引用的过程。符号引用与虚拟机实现的布局无关，引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同，但是它们能接受的符号引用必须是一致的，只要能正确定位到它们在内存中的位置就行。直接引用可以是指向目标的指针，相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用，那引用的目标必定已经在内存中存在。\n5. `初始化`：是执行类构造器 `<client>` 方法的过程。`<client>` 方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证 `<client>` 方法执行之前，父类的 `<client>` 方法已经执行完毕。\n\n**问题：JVM 内存区域，开线程影响哪块内存**\n\n*内存区域大致的分布图，与线程对应之后的分布图*\n\n![JVM内存区域](https://user-gold-cdn.xitu.io/2018/7/12/1648eca898209c6c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n图中`由浅蓝色标识的部分是所有线程共享的数据区；淡紫色标识的部分是每个线程私有的数据区域`。\n\n1. `程序计数器`：`线程私有`，用来指示当前线程所执行的字节码的行号，就是用来标记线程现在执行的代码的位置；对 Java 方法，它存储的是字节码指令的地址；对于 Native 方法，该计数器的值为空。\n2. `栈`：`线程私有`，与线程同时创建，总数与线程关联，代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。一个方法的执行和退出就是用一个栈帧的入栈和出栈表示的。通常我们不允许你使用递归就是因为，方法就是一个栈，太多的方法只执行而没有退出就会导致栈溢出，不过可以通过尾递归优化。栈又分为虚拟机栈和本地方法栈，一个对应 Java 方法，一个对应 Native 方法。\n3. `堆`：用来给对象分配内存的，`几乎所有的对象实例（包括数组）都在上面分配`。它是垃圾收集器的主要管理区域，因此也叫 GC 堆。它实际上是一块内存区域，由于一些收集算法的原因，又将其细化分为新生代和老年代等。如果在堆中没有内存完成实例分配，并且堆也无法再扩展时，将会抛出 OutOfMemoryError 异常。\n4. `方法区`：方法区由多线程共享，用来存储类信息、常量、静态变量、即使编译后的代码等数据。`运行时常量池`是方法区的一部分，它用于存放编译器生成的各种字面量和符号引用，比如字符串常量等。根据 Java 虚拟机规范的规定，当方法区无法满足内存分配需求时，将抛出 OutOfMemoryError 异常。\n\n**问题：软引用、弱引用区别**    \n**问题：Java 中的四种引用**    \n**问题：强引用置为 null，会不会被回收？**\n\n四种引用类型：强引用、软引用、弱引用和虚引用。    \n\n1. 当使用 `new` 关键字创建一个对象的时候，这个对象就是`强引用`的，它绝对不会被回收，即使内存耗尽。你可以通过将其置为 `null` 来弱化对其的引用，但什么时候被回收还要取决于 GC 算法。    \n2. 软引用和弱引用相似，你可以分别通过 `SoftReference<T>` 和 `WeakReference<T>` 来使用它们，它们的区别在于后者更弱一些。`当 JVM 进行垃圾回收时，无论内存是否充足，都会回收被弱引用关联的对象；而软引用关联着的对象，只有在内存不足的时候 JVM 才会回收该对象`。软引用可以用来做缓存，因为当 JVM 内存不足的时候才会被回收；而弱引用适合 Android 上面引用 Activity 等的时候使用，因为 Activity 被销毁不一定是因为内存不足，可能是正常的生命周期结束。如果此时使用软引用，而 JVM 内存仍然足够，则仍然会持有 Activity 的引用而造成内存泄漏。\n3. `虚引用`在任何时候都可能被垃圾回收器回收。\n\n当一个对象不再被引用的时候，该对象也不一定被回收，理论上它还有一次救赎的机会，即通过覆写 `finilize()` 方法把对自己的引用从弱变强，即把自己赋值给全局的对象等。因为当对象不可达的时候，只有当 `finilize()` 没被覆写，或者 `finilize()` 已经被调用过，则该对象会被回收。否则，它会被放在一个队列中，并在稍后由一个低优先级的 Finilizer 线程执行它。\n\n**问题：垃圾收集机制 对象创建，新生代与老年代**\n\n实际虚拟机的内存区域就是一整块内存，不区分新生代与老年代。新生代与老年代是垃圾收集器为了使用不同收集策略而定义的名称。\n\n![JVM 内存各个区域的名称](res/jvm_heap_region.jpg)\n\n内存分配的策略是：1). 对象优先在 Eden 分配；2). 大对象直接进入老年代；3). 长期存活对象将进入老年代。\n\n我们之前有一次线上的问题就是代码中查询了太多的数据，导致大对象直接进入了老年代，查询频繁，导致虚拟机 GC 频繁，进入假死状态（停顿）。\n\n`新生代`：主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象，所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。\n\n1. `Eden 区`：Java 新对象的出生地（如果新创建的对象占用内存很大，则直接分配到老年代）。当Eden 区内存不够的时候就会触发 MinorGC，对新生代区进行一次垃圾回收。    \n2. `ServivorTo`：保留了一次 MinorGC 过程中的幸存者。    \n3. `ServivorFrom`：上一次 GC 的幸存者，作为这一次 GC 的被扫描者。\n\n`MinorGC 的过程`：MinorGC 采用复制算法。首先，把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域（如果有对象的年龄以及达到了老年的标准，则赋值到老年代区），同时把这些对象的年龄+1（如果 ServicorTo 不够位置了就放到老年区）；然后，清空 Eden 和 ServicorFrom 中的对象；最后，ServicorTo 和 ServicorFrom 互换，原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。\n\n`老年代`：主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定，所以 MajorGC 不会频繁执行。`MajorGC` 前一般都先进行了一次 MinorGC，使得有新生代的对象进入老年代，导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。当老年代也满了装不下的时候，就会抛出 `OOM` 异常。\n\n至于老年代究竟使用哪种垃圾收集算法实际上是由垃圾收集器来决定的。老年代、新生代以及新生代的各个内存区域之间的比例并不是固定的，我们可以使用参数来配置。\n\n## 2、虚拟机执行系统\n\n**问题：谈谈类加载器 Classloader**\n**问题：类加载机制，双亲委派模型**\n\nAndroid 中的类加载器与 Java 中的类加载器基本一致，都分成`系统类加载器`和`用户自定义类加载器`两种类型。Java 中的系统类加载器包括，`Bootstrap 类加载器`，主要用来加载 java 运行时下面的 lib 目录；`拓展类加载器 ExtClassLoader`，用来加载 Java 运行时的 lib 中的拓展目录；`应用程序类加载器`，用来加载当前程序的 ClassPath 目录下面的类。其中，引导类加载器与其他两个不同，它是在 C++ 层实现的，没有继承 ClassLoader 类，也无法获取到。\n\nAndroid 中的系统类加载器包括，`BootClassLoader`, 用来加载常用的启动类；`DexClassLoader` 用来加载 dex 及包含 dex 的压缩文件；`PathClassLoader` 用来加载系统类和应用程序的类。    \n三种类加载器都是在系统启动的过程中创建的。    \nDexClassLoader 和 PathClassLoader 都继承于 BaseDexClassLoader. 区别在于调用父类构造器时，DexClassLoader 多传了一个 optimizedDirectory 参数，`这个目录必须是内部存储路径`，用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null，只能加载内部存储目录的 Dex 文件。所以我们可以用 DexClassLoader 去加载外部的 Apk. \n\n```java\npublic DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {\n    super(dexPath, new File(optimizedDirectory), libraryPath, parent);\n}\n```\n\nDexClassLoader 重载了 `findClass()` 方法，在加载类时会调用其内部的 `DexPathList` 去加载。DexPathList 是在构造 DexClassLoader 时生成的，其内部包含了 DexFile。腾讯的 qq 空间热修复技术正是利用了 DexClassLoader 的加载机制，将需要替换的类添加到 dexElements 的前面，这样系统会使用先找到的修复过的类。\n\n```java\n    private final DexPathList pathList;\n    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {\n        super(parent);\n        this.originalPath = dexPath;\n        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);\n    }\n    @Override\n    protected Class<?> findClass(String name) throws ClassNotFoundException {\n        Class clazz = pathList.findClass(name);\n        return clazz;\n    }\n```\n\n本质上不论哪种加载器加载类的时候的都是分成两个步骤进行的，第一是使用双亲委派模型的规则从资源当中加载类的数据到内存中。通常类都被存储在各种文件中，所以这无非就是一些文件的读写操作。当将数据读取到内存当中之后会调用 `defineClass()` 方法，并返回类的类型 Class 对象。这个方法底层会调用一个 native 的方法来最终完成类的加载工作。\n\n至于`双亲委派模型`，这是 Java 中类加载的一种规则，比较容易理解。前提是类加载器之间存在着继承关系，那么当一个类进行加载之前会先判断这个类是否已经存在。如果已经存在，则直接返回对应的 Class 即可。如果不存在则`先交给自己的父类进行加载，父类加载不到，然后自己再进行加载`。这样一层层地传递下去，一个类的加载将是从父类开始到子类的过程，所以叫双亲委派模型。这种加载机制的好处是：第一，`它可以避免重复加载`，已经加载一次的类就无需再次加载；第二，更加`安全`，因为类优先交给父类进行加载，按照传递规则，也就是先交给系统的类进行加载。那么如果有人想要伪造一个 Object 类型，想要蒙混过关的话，显然是逃不过虚拟机的法眼了。\n\n*Android 的 ClassLoader 定义在 Dalivk 目录下面，这里是它在 AOSP 中的位置：[dalvik-system](https://android.googlesource.com/platform/libcore-snapshot/+/ics-mr1/dalvik/src/main/java/dalvik/system)*\n\n**问题：动态加载**    \n**问题：对动态加载（OSGI）的了解？**\n\nOSGI 一种用来实现 Java 模块化的方式，在 2010 年左右的时候比较火，现在用得比较少了。\n\n## 3、内存模型\n\n**问题：JVM 内存模型，内存区域**    \n**问题：JVM 内存模型**\n\nJava 内存模型，简称 `JMM`，它是一种抽象的概念，或者是一种协议，用来解决在并发编程过程中内存访问的问题，同时又可以兼容不同的硬件和操作系统。\n\n在 Java 内存模型中，所有的变量都存储在`主内存`。每个 Java 线程都存在着自己的`工作内存`，工作内存中保存了该线程用得到的变量的副本，线程对变量的读写都在工作内存中完成，无法直接操作主内存，也无法直接访问其他线程的工作内存。当一个线程之间的变量的值的传递必须经过主内存。\n\n当两个线程 A 和线程 B 之间要完成通信的话，要经历如下两步：首先，线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作，之后将数据重新写回到主内存中；然后，线程 B 从主存中读取最新的共享变量。\n\n此外，内存模型还规定了\n\n1. 主内存和工作内存交互的 `8 种操作`及其规则；\n2. 提供了 `voliate` 关键字用来，保证变量的可见性，和屏蔽指令重排序;\n3. 对 `long 及 double` 的特殊规定：读写操作分成两个 32 位操作；\n4. `先行发生原则` (happens-before) 和 `as-if-serial` 语义（不管怎么重排序，程序的执行结果不能被改变）。\n\n## 4、Android 虚拟机\n\n**问题：ART 和 Dalvik (DVM) 的区别**    \n**问题：对 Dalvik、ART 虚拟机有基本的了解**\n\nART 4.4 时发布，5.0 之后默认使用 ART. \n\n1. ART 在应用安装时会进行`预编译` (ahead of time compilation, AOT)，将字节码编译成机器码并存储在本地，这样每次运行程序时就无需编译了，提升了效率。缺点是：1).安装耗时更长了；2).占用更多存储空间。7.0 之后，ART 引入 JIT，安装时不会将字节码全部编译成机器码，而是运行时将热点代码编译成机器码。\n2. DVM 是为 `32 位` CPU 设计的，ART 支持 `64 位且兼容 32 位 CPU`. \n3. ART 对`垃圾收集机制`进行了改进，将 GC 暂停由 2 次改成了 1 次等。\n4. ART 的`运行时堆空间划分`与 DVM 不同\n\n**问题：DVM 与 JVM 的区别**\n\n1. 基于的架构不同：DVM 基于`寄存器`，相比于 JVM（基于栈），执行速度更快（因为无需到栈中读取数据）。\n2. 执行的字节码不同：DVM 在执行的是 dex 文件，经过 class 经 dx 转换之后的。dex 会对 class 进行优化，整个 class，取出冗余信息，加快加载方式。\n3. DVM 允许在有限的空间内同时运行`多个进程`。\n4. DVM 由 `Zygote` 创建和初始化。Zygote 是一个 DVM 进程，当需要创建一个应用程序时，Zygote 通过 fork 自身来创建新的 DVM 实例。\n5. DVM 有`共享机制`，不同应用在运行时可以共享相同的类。\n6. DVM 早期没有使用 JIT 编译器，JIT 就是`即时编译器`，早期的 DVM 需要经过`解释器`将 dex 码编译成`机器码`，效率不高。2.2 之后使用了 JIT，会对热点代码进行编译，生成本地机器码，下次执行到相同的逻辑时，可以直接执行本地机器码，无需每次编译。\n\nDVM 与 ART 的诞生：init 进程启动 Zygote 时会调用 `app_main.cpp`，它会调用 AndroidRuntime 的 `start()` 函数，在其中通过 `startVM()` 方法启动虚拟机。在启动虚拟机之前会通过读取系统的属性，判断使用 DVM 还是 ART 虚拟机实例。\n\n总结：DVM 2.2 之前边解释边执行，效率低；2.2 之后引入即使编译功能，将热点代码编译成本地机器码；4.4 引入了 ART，5.0 之后默认使用 ART. ART 早期在 APK 安装时预编译，将代码编译成机器码，但是安装时间延长。所以，7.0 上引入了 JIT，运行时编译热点代码。\n\n## 附录\n\n- [《JVM 系列-1：虚拟机内存管理》](https://blog.csdn.net/github_35186068/article/details/83754068)\n- [《全面理解Java内存模型 JMM 及volatile关键字》](https://blog.csdn.net/javazejian/article/details/72772461)"
  },
  {
    "path": "笔试面试/Android高级面试_5_四大组件、系统源码等.md",
    "content": "# Android 高级面试-5：四大组件、系统源码等\n\n## 1、四大组件\n\n### 1.1 Activity\n\n**问题：在两个 Activity 之间传递对象还需要注意什么呢？**\n\n对象的大小。Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的（有些手机是 2 M），而一个进程默认有 16 个 Binder 线程，所以一个线程能占用的缓冲区就更小了（有人以前做过测试，大约一个线程可以占用 128 KB）。所以当你看到 TransactionTooLargeException 异常时，你应该知道怎么解决了。使用 EventBus 等传递大数据。\n\n**问题：onSaveInstanceState() 和 onRestoreInstanceState()**\n\n当 Activity 被销毁的时候回调用 `onSaveInstanceState()` 方法来存储当前的状态。这样当 Activity 被重建的时候，可以在 `onCreate()` 和 `onRestoreInstanceState()` 中恢复状态。对于 targetAPI 为 28 及以后的应用，该方法会在 `onStop()` 方法之后调用；对于之前的设备，这方法会在 `onStop()` 之前调用，但是无法确定是在 `onPause()` 之前还是之后调用。\n\n`onRestoreInstanceState()` 用来恢复之前存储的状态，它会在 `onStart()` 和 `onPostCreate()` 之间被调用。此外，也可以直接在 `onCreate()` 方法中进行恢复，但是基于这个方法调用的时机，如果有特别需求，可以在这个方法中进行处理。\n\n**问题：SingleTask 启动模式**    \n**问题：Activity 启动模式**\n\n1. `standard`：默认，每次启动的时候会创建一个新的实例，并且被创建的实例所在的栈与启动它的 Activity 是同一个栈。比如，A 启动了 B，那么 B 将会与 A 处在同一个栈。假如，我们使用 Application 的 Context 启动一个 Activity 的时候会抛出异常，这是因为新启动的 Activity 不知道自己将会处于哪个栈。可以在启动 Activity 的时候使用 `FLAG_ACTIVITY_NEW_TASK`。这样新启动的 Acitivyt 将会创建一个新的栈。\n2. `singleTop`：栈顶复用，如果将要启动的 Activity 已经位于栈顶，那么将会复用栈顶的 Activity，并且会调用它的 `onNewIntent()`。常见的应用场景是从通知打开 Activity 时。\n3. `singleTask`：单例，如果启动它的任务栈中存在该 Activity，那么将会复用该 Activity，并且会将栈内的、它之上的所有的 Activity 清理出去，以使得该 Activity 位于栈顶。常见的应用场景是启动页面、购物界面、确认订单界面和付款界面等。\n4. `singleInstance`：这种启动模式会在启动的时候为其指定一个单独的栈来执行。如果用同样的intent 再次启动这个 Activity，那么这个 Activity 会被调到前台，并且会调用其 `onNewIntent()` 方法。\n\n**问题：下拉状态栏是不是影响 Activity 的生命周期，如果在 onStop() 的时候做了网络请求，onResume() 的时候怎么恢复**     \n**问题：前台切换到后台，然后再回到前台，Activity 生命周期回调方法。弹出 Dialog，生命值周期回调方法**     \n**问题：Activity 生命周期**     \n**问题：Activity 上有 Dialog 的时候按 Home 键时的生命周期**      \n**问题：横竖屏切换的时候，Activity 各种情况下的生命周期**\n\nAndroid 下拉通知栏不会影响 Activity 的生命周期方法。\n\n弹出 Dialog，生命周期：其实是否弹出 Dialog，并不影响 Activity 的生命周期，所以这时和正常启动时 Activity 的生命回调方法一致: `onCreate() -> onStart() -> onResume()`。\n\n![Activity 的生命周期](https://github.com/Shouheng88/Android-notes/raw/master/%E5%9B%9B%E5%A4%A7%E7%BB%84%E4%BB%B6/res/activity_life.png)\n\n这里我们总结一下在实际的使用过程中可能会遇到的一些 Acitivity 的生命周期过程：\n\n1. `当用户打开新的 Activity 或者切换回桌面`：会经过的生命周期为 `onPause()->onStop()`。因为此时 Activity 已经变成不可见了，当然，如果新打开的 Activity 用了透明主题，那么 onStop() 不会被调用，因此原来的 Activity 只是不能交互，但是仍然可见。\n2. `从新的 Activity 回到之前的 Activity 或者从桌面回到之前的 Activity`：会经过的生命周期为 `onRestart()->onStart()-onResume()`。此时是从 onStop() 经 onRestart() 回到 onResume() 状态。\n3. 如果在上述 1 的情况下，进入后台的 Activity 因为内存不足被销毁了，那么当再次回到该 Activity 的时候，生命周期方法将会从 onCreate() 开始执行到 onResume()。\n4. `当用户按下 Back 键时`：如果当前 Activity 被销毁，那么经过的生命周期将会是 `onPause()->onStop()->onDestroy()`。\n\n具体地，当存在两个 Activity，分别是 A 和 B 的时候，在各种情况下，它们的生命周期将会经过：\n\n1. `Back 键 Home 键`\n    1. 当用户点击 A 中按钮来到 B 时，假设 B 全部遮挡住了 A，将依次执行：`A.onPause()->B.onCreate()->B.onStart()->B.onResume->A.onStop()`。\n    2. 接1，此时如果点击 Back 键，将依次执行：`B.onPause()->A.onRestart()->A.onStart()->A.onResume()->B.onStop()->B.onDestroy()`。\n    3. 接2，此时如果按下 Back 键，系统返回到桌面，并依次执行：`A.onPause()->A.onStop()->A.onDestroy()`。\n    4. 接2，此时如果按下 Home 键（非长按），系统返回到桌面，并依次执行`A.onPause()->A.onStop()`。由此可见，Back 键和 Home 键主要区别在于是否会执行 onDestroy()。\n    5. 接2，此时如果长按 Home 键，不同手机可能弹出不同内容，Activity 生命周期未发生变化。\n\n2. `横竖屏切换时 Activity 的生命周期`\n    1. 不设置 Activity 的 `android:configChanges` 时，切屏会重新调用各个生命周期，切横屏时会执行一次，切竖屏时会执行两次。\n    2. 设置 Activity 的 `android:configChanges=“orientation”` 时，切屏还是会重新调用各个生命周期，切横、竖屏时只会执行一次。\n    3. 设置 Activity 的 `android:configChanges=“orientation|keyboardHidden”` 时，切屏不会重新调用各个生命周期，只会执行 onConfiguration() 方法。\n\n**问题：Activity 之间的通信方式**\n\n1. Intent + `onActivityResult()` + `setResult()`\n2. 静态变量（跨进程不行）\n3. 全局通信，广播或者 EventBus\n\n**问题：AlertDialog, PopupWindow, Activity 区别**\n\nAlertDialog 是 Dialog 的子类，所以它包含了 Dialog 类的很多属性和方法。是弹出对话框的主要方式，对话框分成支持包的和非支持包的，UI 效果上略有区别。\n\n`AlertDialog 与 PopupWindow 之间最本质的差异在于`：\n\n1. `AlertDialog 是非阻塞式对话框；而 PopupWindow 是阻塞式对话框`。AlertDialog 弹出时，后台还可以做事情；PopupWindow 弹出时，程序会等待，在 PopupWindow 退出前，程序一直等待，只有当我们调用了 `dismiss()` 方法的后，PopupWindow 退出，程序才会向下执行。我们在写程序的过程中可以根据自己的需要选择使用 Popupwindow 或者是 Dialog. \n2. `两者最根本的区别在于有没有新建一个 window`，PopupWindow 没有新建，而是通过 WMS 将 View 加到 DecorView；Dialog 是新建了一个 window (PhoneWindow)，相当于走了一遍 Activity 中创建 window 的流程。\n\n`Activity 与 Dialog` 类似，都会使用 PhoneWindow 来作为 View 的容器。Activity 也可以通过设置主题为 Dialog 的来将其作为对话框来使用。Dialog 也可以通过设置 Theme 来表现得像一个 Activity 一样作为整个页面。但 Activity 具有生命周期，并且它的生命周期归 AMS 管，而 Dialog 不具有生命周期，它归 WMS 管。\n\n**问题：Activity 与 Service 通信的方式**\n\n前提是是否跨进程，如果不跨进程的话，EventBus 和 静态变量都能传递信息，否则需要 IPC 才行：\n\n1. `Binder 用于跨进程的通信方式`，AIDL 可以用来进行与远程通信，绑定服务的时候可以拿到远程的 Binder，然后调用它的方法就可以从远程拿数据。那么如果希望对远程的服务进行监听呢？可以使用 AIDL 中的 `oneway` 来定义回调的接口，然后在方法中传入回调即可。也可以使用 Messenger，向远程发送信息的时候，附带本地的 Messenger，然后远程获取本地的 Messenger 然后向其发送信息即可，详见 IPC 相关一文：[《Android 高级面试-2：IPC 相关》](https://juejin.im/post/5c6a9b6a6fb9a049f362a71f)\n2. 广播：使用广播实现跨进程通信\n3. 启动服务的时候传入值，使用 `startService()` 的方式\n\n*关于 Activity 相关的内容可以参考笔者的文章：[《Android 基础回顾：Activity 基础》](https://blog.csdn.net/github_35186068/article/details/86380438)*\n\n### 1.2 Service\n\n**问题：怎么启动 Service**    \n**问题：Service 的开启方式**    \n**问题：Service 生命周期**\n\n![Service的生命周期图](https://github.com/Shouheng88/Android-notes/blob/master/%E5%9B%9B%E5%A4%A7%E7%BB%84%E4%BB%B6/res/service_life.png?raw=true)\n\n1. Service 有绑定模式和非绑定模式，以及这两种模式的混合使用方式。不同的使用方法生命周期方法也不同。 \n    1. **非绑定模式**：当第一次调用 `startService()` 的时候执行的方法依次为 `onCreate()->onStartCommand()`；当 Service 关闭的时候调用 `onDestory()`。\n    2. **绑定模式**：第一次 `bindService()` 的时候，执行的方法为 `onCreate()->onBind()`；解除绑定的时候会执行 `onUnbind()->onDestory()`。\n\n2. 我们在开发的过程中还必须注意 Service 实例只会有一个，也就是说如果当前要启动的 Service 已经存在了那么就不会再次创建该 Service 当然也不会调用 onCreate() 方法。所以，\n    1. 当第一次执行 `startService(intent)` 的时候，会调用该 Service 中的 `onCreate()` 和`onStartCommand()` 方法。\n    2. 当第二次执行 `startService(intent)` 的时候，只会调用该 Service 中的 `onStartCommand()` 方法。（因此已经创建了服务，所以不需要再次调用 `onCreate()` 方法了）。\n\n3. `bindService()` 方法的第三个参数是一个标志位，这里传入 `BIND_AUTO_CREATE` 表示在Activity 和 Service 建立关联后自动创建 Service，这会使得 MyService 中的 `onCreate()` 方法得到执行，但 `onStartCommand()` 方法不会执行。所以，在上面的程序中当调用了`bindService()` 方法的时候，会执行的方法有，Service 的 `onCreate()` 方法，以及 ServiceConnection 的 `onServiceConnected()` 方法。\n\n4. 在 3 中，如果想要停止 Service，需要调用 `unbindService()` 才行。 \n\n5. 如果我们既调用了 `startService()`，又调用 `bindService()` 会怎么样呢？这时不管你是单独调用 `stopService()` 还是 `unbindService()`，Service 都不会被销毁，必须要将两个方法都调用 Service 才会被销毁。也就是说，`stopService()` 只会让 Service 停止，`unbindService()` 只会让 Service 和 Activity 解除关联，一个 Service 必须要在既没有和任何 Activity 关联又处理停止状态的时候才会被销毁。\n\n**问题：进程保活**     \n\n1. Service 设置成 `START_STICKY` 那么 Service 被 kill 之后会被重启；\n2. 通过 `startForeground()` 将进程设置为前台进程，做前台服务，优先级和前台进程一样，除非系统资源紧缺，否则不会 kill 进程；\n3. 双进程守护，其中一个被清理之后，没有被清理的进程可以重启被 kill 的进程；\n4. 在 native 层使用守护进程，native 层的进程会被认为和 Android 进程是两个不同的进程，父进程被杀死，子进程依然可以存活；\n5. 联系厂商，加入白名单；\n6. 启用一个像素的 Activity. \n\n**问题：App 中唤醒其他进程的实现方式**\n\n### 1.3 Broadcast\n\n**问题：BroadcastReceiver，LocalBroadcastReceiver 区别**    \n**问题：广播的使用场景**     \n**问题：广播的使用方式，场景**     \n**问题：广播的分类？**     \n**问题：广播（动态注册和静态注册区别，有序广播和标准广播）**\n\n1. 按照注册方式：`静态注册和动态注册`两种：\n\n    1. 静态广播直接在 manifest 中注册。限制：\n        1. 在 Android 8.0 的平台上，应用不能对大部分的广播进行静态注册，也就是说，不能在 AndroidManifest 文件对`有些`广播进行静态注册；\n        2. 当程序运行在后台的时候，静态广播中不能启动服务。\n    2. 动态广播与静态广播相似，但是不需要在 Manifest 中进行注册。`注意当页面被销毁的时候需要取消注册广播！`\n\n2. 按照作用范围：`本地广播和普通广播`两种，\n\n    1. 普通广播是全局的，所有应用程序都可以接收到，容易会引起安全问题。\n    2. 本地广播只能够在应用内传递，广播接收器也只能接收应用内发出的广播。本地广播的核心类是 LocalBroadcastManager，使用它的静态方法 `getInstance()` 获取一个单例之后就可以使用该单例的 `registerReceiver()`、`unregisterReceiver()` 和 `sendBroadcast()` 等方法来进行操作了。\n\n3. 按照是否有序：`有序广播和无序广播`两种，无序广播各接收器接收的顺序无法确定，并且在广播发出之后接收器只能接收，不能拦截和进行其他处理，两者的区别主要体现在发送时调用的方法上。优先级高的会先接收到，优先级相等的话则顺序不确定。并且前面的广播可以在方法中向 Intent 写入数据，后面的广播可以接收到写入的值。\n\n### 1.4 ContentProvider\n\n**问题：Android 系统为什么会设计 ContentProvider，进程共享和线程安全问题**\n\nContentProvider 在 Android 中的作用是对外共享数据，提供了数据访问接口，用于在不同应用程序之间共享数据，同时还能保证被访问数据的安全性。它通常用来提供一些公共的数据，比如用来查询文件信息，制作音乐播放器的时候用来读取系统中的音乐文件的信息。\n\n与 SQLiteDatabase 不同，ContentProvider 中的 CRUD 不接收表名参数，而是 Uri 参数。内容 URI 是内容提供器中数据的唯一标识符，包括权限和路径。\n\n并发访问时，不论是不同的进程还是同一进程的不同线程，当使用 AMS 获取 Provider 的时候返回的都是同一实例。我们使用 Provider 来从远程访问数据，当 `query()` 方法运行在不同的线程，实际上是运行在 Provider 方的进程的 Binder 线程池中。通过 Binder 的线程池来实现多进程和多线程访问的安全性。\n\n### 1.5 Fragment\n\n**问题：Fragment 各种情况下的生命周期**    \n**问题：Activity 与 Fragment 之间生命周期比较**\n\n![](https://github.com/Shouheng88/Android-notes/raw/master/%E5%9B%9B%E5%A4%A7%E7%BB%84%E4%BB%B6/res/activity_fragment_lifecycle.png)\n\n**问题：Fragment 之间传递数据的方式？**\n\n1. 同一 Activity 的 Fragment 之间可以使用 ViewModel 来交换数据；\n2. 使用 EventBus，广播，静态的；\n3. 通过 Activity 获取到另一个 Fragment，强转之后使用它对外提供的 public 方法进行通信；\n4. 通过 Activity 获取到另一个 Fragment，该 Fragment 实现某个接口，然后转成接口之后进行通信（也适用于 Activity 与 Fragment 之间），强转之后使用它对外提供的 public 方法进行通信；\n\n**问题：如何实现 Fragment 的滑动**\n\n### 1.6 Context\n\n**问题：Application 和 Activity 的 Context 对象的区别**\n\nContext 的继承关系如下所示，所以，`Android 中的 Context 数量 = Activity 的数量 + Service 的数量 + 1 (Application)`\n\n![Context 的继承关系](res/QQ截图20190226123631.png)\n\nContext 的用途比较广，比如用来获取图片、字符串，显式对话框等，`大部分情况下，使用哪个 Context 都可以，少数情况下只能使用特定的 Context`. 比如启动 Activity 的时候，要求传入 Activity 的 Context，因为 AMS 需要直到启动指定 Activity 的 Activity 的栈。一般情况下，能使用 Application 的 Context 尽量使用它的，因为它的生命周期更长。\n\nContext 之间使用的是`装饰者设计模式`，其中 Context 是一个抽象的类。ContextWrapper 内部实际使用 ContextImpl 实现的，因此所有的逻辑基本是在 ContextImpl 中实现的。然后对于 ContextThemeWrapper，它在 ContextWrapper 的基础之上又进行了一层装饰，就是与主题相关的东西。\n\n新版的 Activity 启动中将 Activity 的各个回调执行的逻辑放在了各个 ClientTransactionItem 中，比如 LaunchActivityItem 表示用来启动 Activity。 最终执行逻辑的时候是调用它们的 execute() 方法并使用传入的 ClientTransactionHandler 真正执行任务。而这里的 ClientTransactionHandler 实际上就是 ActivityThread，所以它将调用到 Activity 的 `handleLaunchActivity()` 启动 Activity. 然后程序进入到 `performLaunchActivity()` 中。这个方法中会创建上面的 Application 和 Activity 对应的 Context：\n\n```java\n    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {\n        // ...\n\n        // 创建 Activity 的 Context\n        ContextImpl appContext = createBaseContextForActivity(r);\n        Activity activity = null;\n        try {\n            java.lang.ClassLoader cl = appContext.getClassLoader();\n            // 创建新的 Activity\n            activity = mInstrumentation.newActivity(\n                    cl, component.getClassName(), r.intent);\n            // ...\n        } catch (Exception e) {\n            // ... handle exception\n        }\n\n        try {\n            // 创建应用的 Application\n            Application app = r.packageInfo.makeApplication(false, mInstrumentation);\n\n            if (activity != null) {\n                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());\n                // Activity 的配置\n                Configuration config = new Configuration(mCompatConfiguration);\n                if (r.overrideConfig != null) {\n                    config.updateFrom(r.overrideConfig);\n                }\n                // 创建窗口\n                Window window = null;\n                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {\n                    window = r.mPendingRemoveWindow;\n                    r.mPendingRemoveWindow = null;\n                    r.mPendingRemoveWindowManager = null;\n                }\n                // 关联 Activity 和 Context\n                appContext.setOuterContext(activity);\n                activity.attach(appContext, this, getInstrumentation(), r.token,\n                        r.ident, app, r.intent, r.activityInfo, title, r.parent,\n                        r.embeddedID, r.lastNonConfigurationInstances, config,\n                        r.referrer, r.voiceInteractor, window, r.configCallback);\n\n                // ...\n                // 设置 Activity 的主题\n                int theme = r.activityInfo.getThemeResource();\n                if (theme != 0) {\n                    activity.setTheme(theme);\n                }\n\n                // 回调 Activity 的生命周期方法\n                activity.mCalled = false;\n                if (r.isPersistable()) {\n                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);\n                } else {\n                    mInstrumentation.callActivityOnCreate(activity, r.state);\n                }\n                r.activity = activity;\n            }\n            r.setState(ON_CREATE);\n            mActivities.put(r.token, r);\n        } catch (SuperNotCalledException e) {\n            throw e;\n        } catch (Exception e) {\n            // ... handle exception\n        }\n\n        return activity;\n    }\n\n    public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {\n        // ...\n\n        try {\n            // 创建 Application 的 Context\n            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);\n            app = mActivityThread.mInstrumentation.newApplication(\n                    cl, appClass, appContext);\n            // 关联 Application 和 Context\n            appContext.setOuterContext(app);\n        } catch (Exception e) {\n            // ... handle exception\n        }\n        // ...\n        return app;\n    }\n```\n\n### 1.7 其他\n\n**问题：AndroidManifest 的作用与理解**\n\n声明了四大组件、应用版本、权限等的配置信息，会在解析 APK 的时候由 PMS 进行解析，然后解析的结果会被缓存到 PMS 中。\n\n## 2、Android API\n\n### 2.1 AsyncTask\n\n**问题：AsyncTask 机制，如何取消 AsyncTask**     \n**问题：多线程（关于 AsyncTask 缺陷引发的思考）**    \n**问题：Asynctask 有什么优缺点**    \n**问题：AsyncTask 机制、原理及不足？**\n\nAsyncTask 是 Android 提供的用来执行异步操作的 API，我们可以通过它来执行异步操作，并在得到结果之后将结果放在主线程当中进行后续处理。\n\nAsyncTask 的缺点是在使用多个异步操作和并需要进行 Ui 变更时，就变得复杂起来（会导致多个 AsyncTask 进行嵌套）。如果有多个地方需要用到 AsyncTask，可能需要定义多个 AsyncTask 的实现。\n\n如果 AsyncTask 以一个`非静态的内部类`的形式声明在 Activity 中，那么它会持有 Activity 的匿名引用，如果销毁 Activity 时 AsyncTask 还在执行异步任务的话，Activity 就不能销毁，会造成内存泄漏。解决方式是，要么将 AsyncTask 定义成静态内部类，要么在 Activity 销毁的时候调用 `cancel()` 方法取消 AsyncTask.在屏幕旋转或 Activity 意外结束时，Activity 被创建，而 AsyncTask 会拥有之前 Activity 的引用，会导致结果丢失。\n\nAsyncTask 在 1.6 之前是串行的，1.6 之后是并行的，3.0 之后又改成了串行的。不过我们可以通过调用 `executeOnExecutor()` 方法并传入一个线程池，来让 AsyncTask 在某个线程池中并行执行任务。\n\nAsyncTask 的源码就是将一个任务封装成 Runnable 之后放进线程池当中执行，执行完毕之后调用主线程的 Handler 发送消息到主线程当中进行处理。任务在默认线程池当中执行的时候，会被加入到一个双端队列中执行，执行完一个之后再执行下一个，以此来实现任务的串行执行。\n\n**问题：介绍下 SurfaceView**\n\n`SurfaceView` 以及 TextureView 均继承于 `android.view.View`，它们都在`独立的线程`中绘制和渲染，常被用在对绘制的速率要求比较高的应用场景中，用来解决普通 View 因为绘制的时间延迟而带来的`掉帧`的问题，比如用作相机预览、视频播放的媒介等。\n\nSurfaceView 提供了`嵌入在视图层次结构内`的专用绘图图层 (Surface)。图层 (Surface) 处于 `Z 轴`，位于持有 SurfaceView 的窗口之后。SurfaceView 在窗口上开了一个透明的 “洞” 以展示图面。Surface 的排版显示受到视图层级关系的影响，它的兄弟视图结点会在顶端显示。注意，如果 Surface 上面有透明控件，那么每次 Surface 变化都会引起框架重新计算它和顶层控件的透明效果，这会影响性能。SurfaceView 使用`双缓冲`，SurfaceView 自带一个 Surface，这个 Surface 在 WMS 中有自己对应的 WindowState，在 `SurfaceFlinger` 中也会有自己的 Layer。这样的好处是对这个 Surface 的渲染可以放到单独线程去做。因为这个 `Surface 不在 View hierachy` 中，它的显示也不受 View 的属性控制，所以不能进行平移，缩放等变换，也不能放在其它 ViewGroup 中，一些 View 中的特性也无法使用。\n\nTextureView 在 Andriod 4.0 之后的 API 中才能使用，并且必须在`硬件加速`的窗口中。和 SurfaceView 不同，它不会在 WMS 中单独创建窗口，而是作为 `View hierachy` 中的一个普通 View，因此可以和其它`普通 View 一样进行移动，旋转，缩放，动画等`变化。它占用内存比 SurfaceView 高，在 5.0 以前在主线程渲染，5.0 以后有单独的渲染线程。\n\n### 2.2 View 体系\n\n#### 2.2.1 事件分发机制\n\n**问题：Android 事件分发机制**    \n**问题：事件传递机制的介绍**     \n**问题：View 事件传递**     \n**问题：触摸事件的分发？**\n**问题：点击事件被拦截，但是想传到下面的 View，如何操作？**    \n\n1. 触摸事件首先被 Activity 接收，然后它调用自己的 `dispatchTouchEvent(MotionEvent)` 传递给 PhoneWindow；\n2. PhoneWindow 将事件传递给 DecorView；\n3. DecorView 将事件交给 ViewGroup，接下来事件的传递在 ViewGroup 和 View 之间进行，即按照 View 树进行传递。\n4. 事件分发机制本质上是一个`深度优先`的遍历算法。\n\n事件分发机制的核心代码：\n\n```java\n    // ViewGroup:\n    boolean dispatchTouchEvent(MotionEvent e) {\n        boolean result;\n        if (onInterceptTouchEvent(e)) { // 父控件可以覆写并返回 true 以拦截\n            result = super.dispatchTouchEvent(e); // 调用 View 中该方法的实现\n        } else {\n            for (child in children) {\n                result = child.dispatchTouchEvent(e); // 这里的 child 分成 View 和 ViewGroup 两者情形\n                if (result) break; // 被子控件消费，停止分发\n            }\n        }\n        return result;\n    }\n    // View:\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        if (mOnTouchListener.onTouch(this, event)) {\n            return true;\n        } else {\n            return onTouchEvent(event); // 判断事件类型，是长按还是单击并进行回调\n        }\n    }\n```\n\n对于 `dispatchTouchEvent()` 方法，在 View 的默认实现中，会先交给触摸事件进行处理，若它返回了 true 就消费了，否则根据触摸的类型，决定是交给 `OnClickListener` 还是 `OnLongClickListener` 继续处理。\n\n**问题：封装 View 的时候怎么知道 View 的大小**     \n**问题：计算一个 view 的嵌套层级**\n\n按照广度优先算法进行遍历。\n\n**问题：postinvalidate() 和 invalidate() 的区别**\n**问题：invalidate() 和 requestLayout() 的实现原理**\n\nAndroid 中实现 view 的更新有两组方法，一组是 invalidate，另一组是 postInvalidate，其中前者是在UI线程自身中使用，而后者在非 UI 线程中使用。\n\n调用 `invalidate()` 的时候会调用内部的 `invalidateInternal()` 并将要重绘的区域信息传递进去：\n\n```java\n    // View:\n    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,\n            boolean fullInvalidate) {\n        // ...\n        if (skipInvalidate()) {\n            return;\n        }\n        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) ==....) {\n            ...\n            if (invalidateCache) {\n                mPrivateFlags |= PFLAG_INVALIDATED; // 增加 PFLAG_INVALIDATED 标志\n                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; // 删除 PFLAG_DRAWING_CACHE_VALID 标志\n            }\n            final AttachInfo ai = mAttachInfo;\n            final ViewParent p = mParent;\n            if (p != null && ai != null && l < r && t < b) {\n                final Rect damage = ai.mTmpInvalRect;\n                damage.set(l, t, r, b);\n                // 告诉父 View 绘制的区域，向上传递请求\n                p.invalidateChild(this, damage);\n            }\n            ...\n        }\n    }\n```\n\n这里的 p 就是指它的父控件，即 ViewGroup：\n\n```java\n// ViewGroup:\npublic final void invalidateChild(View child, final Rect dirty) {\n    ViewParent parent = this;\n    final AttachInfo attachInfo = mAttachInfo;\n    // AttachInfo 不能是空\n    if (attachInfo != null) {\n        // 如果子 View 正在动画\n        final boolean drawAnimation = (child.mPrivateFlags &  PFLAG_DRAW_ANIMATION) == PFLAG_DRAW_ANIMATION;\n        // View 是否存在矩阵变换\n        Matrix childMatrix = child.getMatrix();\n        //完全不透明，没有动画\n        final boolean isOpaque = child.isOpaque() && !drawAnimation &&\n                child.getAnimation() == null && childMatrix.isIdentity();\n        //子 View 设置标志 dirty\n        int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;\n        // LayerType 不是 LAYER_TYPE_NONE\n        if (child.mLayerType != LAYER_TYPE_NONE) {\n            mPrivateFlags |= PFLAG_INVALIDATED;\n            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;\n        }\n\n        final int[] location = attachInfo.mInvalidateChildLocation;\n        location[CHILD_LEFT_INDEX] = child.mLeft;\n        location[CHILD_TOP_INDEX] = child.mTop;\n        // ...\n        do {\n            View view = null;\n            if (parent instanceof View) {\n                view = (View) parent;\n            }\n            // ...\n            if (view != null) {\n                if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&\n                        view.getSolidColor() == 0) {\n                    opaqueFlag = PFLAG_DIRTY;\n                }\n                // ...\n            }\n            // 继续向上进行传递\n            parent = parent.invalidateChildInParent(location, dirty);\n            if (view != null) {\n                // 矩阵变换相关\n                // ...\n                Matrix m = view.getMatrix();\n                if (!m.isIdentity()) {\n                    RectF boundingRect = attachInfo.mTmpTransformRect;\n                    // ...\n                    dirty.set(...);\n                }\n            }\n        } while (parent != null);\n    }\n}\n\n// ViewGroup:\npublic ViewParent invalidateChildInParent(final int[] location,  final Rect dirty) {\n    if ((mPrivateFlags & PFLAG_DRAWN) == ....) {\n        if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | ...) {\n            // 变换成相对父视图的坐标系的值。\n            dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,\n                            location[CHILD_TOP_INDEX] - mScrollY);\n            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {\n                 // 与父 View 坐标系联合，dirty 可能变为父亲视图的区域。\n                 dirty.union(0, 0, mRight - mLeft, mBottom - mTop);\n            }\n            // 获取父 View 相对它的父节点的左侧和上侧距离\n            final int left = mLeft;\n            final int top = mTop;\n\n            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {\n                // 有此标志，dirty 区域已经是相对父视图的坐标系的值了，\n                // 与父视图相交，区域有交集。在没有 Scroll 的情况下。\n                // 其实就是视图自己的区域(相对父视图坐标系)\n                if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {\n                    dirty.setEmpty();\n                }\n            }\n            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;\n            // location 设置相对父视图的位置。\n            location[CHILD_LEFT_INDEX] = left;\n            location[CHILD_TOP_INDEX] = top;\n            ...\n            // 返回它的父视图。\n            return mParent;\n        } else {\n            .....\n            location[CHILD_LEFT_INDEX] = mLeft;\n            location[CHILD_TOP_INDEX] = mTop;\n            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {\n                dirty.set(0, 0, mRight - mLeft, mBottom - mTop);\n            } else {\n                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);\n            }\n            ....\n            return mParent;\n        }\n    }\n    return null;\n}\n```\n\n这里的 parent 就是指 ViewParent，ViewGroup 以及 ViewRootImpl 都是它的子类。因此，按照上述传递的逻辑，事件最终将进入到 ViewRootImpl 中:\n\n```java\n// ViewRootImpl:\npublic ViewParent invalidateChildInParent(int[] location, Rect dirty) {\n   // 保证是创建视图树结构的线程\n   checkThread();\n   if (dirty == null) {\n       invalidate();\n       return null;\n   } else if (dirty.isEmpty() && !mIsAnimating) {\n       //如果 dirty 内容为空，什么都不做，返回。\n       return null;\n   }\n\n   if (mCurScrollY != 0 || mTranslator != null) {\n       mTempRect.set(dirty);\n       dirty = mTempRect;\n       if (mCurScrollY != 0) {\n           dirty.offset(0, -mCurScrollY);\n       }\n       if (mTranslator != null) {\n           mTranslator.translateRectInAppWindowToScreen(dirty);\n       }\n       if (mAttachInfo.mScalingRequired) {\n           dirty.inset(-1, -1);\n       }\n   }\n\n   invalidateRectOnScreen(dirty);\n   return null;\n}\n\nprivate void invalidateRectOnScreen(Rect dirty) {\n    final Rect localDirty = mDirty;\n    // ...\n    localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);\n    final float appScale = mAttachInfo.mApplicationScale;\n    final boolean intersected = localDirty.intersect(0, 0,\n                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));\n    if (!intersected) {\n        localDirty.setEmpty();\n    }\n    if (!mWillDrawSoon && (intersected || mIsAnimating)) {\n        // 遍历 View 树了\n        scheduleTraversals();\n    }\n}\n```\n\n**问题：三种测量方式之间的区别**\n\n1. `UNSPECIFIED`：默认值，父控件没有给子 View 任何限制，子 View 可以设置为任意大小，没有任何限制。这种情况比较少见，不太会用到。\n2. `EXACTLY`：表示父控件已经确切的指定了子 View 的大小。父视图希望子视图的大小应该是由 specSize 的值来决定的，系统默认会按照这个规则来设置子视图的大小，简单的说当设置 width 或 height 为 match_parent 时，模式为 EXACTLY，因为子 view 会占据剩余容器的空间，所以它大小是确定的。\n3. `AT_MOST`：表示子 View 具体大小没有尺寸限制，但是存在上限，最多只能是 specSize 中指定的大小。（当设置为 width 或 height 为 wrap_content 时，模式为 AT_MOST, 表示子 view 的大小最多是多少，这样子 view 会根据这个上限来设置自己的尺寸）。\n\n**问题：为什么不能在子线程中访问 UI？**    \n**问题：线程是检查是在哪里进行的？**\n\nAndroid 中的控件不是线程安全的，之所以这样设计是为了：\n\n1. 设计成同步的可以简化使用的复杂度；\n2. 可以提升控件的性能（异步加锁在非多线程环境是额外的开销）。\n\n线程检查是在 ViewRootImpl 当中进行的，当我们调用比如 `invalidate()` 和 `requestLayout()` 的时候，请求会沿着 View 树向上传递到 ViewRootImpl 中。然后在该类的方法中会调用 `checkThread()` 方法进行线程检查：\n\n```java\npublic final class ViewRootImpl implements ViewParent {\n    public ViewRootImpl(Context context, Display display) {\n        // ...\n        mThread = Thread.currentThread();\n        // ...\n    }\n\n    // 调用这个方法进行线程安全校验\n    void checkThread() {\n        if (mThread != Thread.currentThread()) {\n            throw new CalledFromWrongThreadException(\n                    \"Only the original thread that created a view hierarchy can touch its views.\");\n        }\n    }\n}\n```\n\n可以看出，这里判断的线程并不一定是主线程，只是绝大部分情况下，我们在主线程中创建 View.\n\n**问题：View.post() 和 Handler.post() 之间的区别**\n\n我们可以在 Activity 的 `onCreate()` 方法中使用 View 的 `post()` 方法获取控件大小，而无需进行延时。原因是：\n\n```java\npublic class View {\n    public boolean post(Runnable action) {\n        // 关联的信息，此时并没有添加到 Window 里，所以这里 attachInfo 是 null\n        final AttachInfo attachInfo = mAttachInfo;\n        if (attachInfo != null) {\n            return attachInfo.mHandler.post(action);\n        }\n        // 将消息缓存起来\n        getRunQueue().post(action);\n        return true;\n    }\n    // 单例的 HandlerActionQueue\n    private HandlerActionQueue getRunQueue() {\n        if (mRunQueue == null) {\n            mRunQueue = new HandlerActionQueue();\n        }\n        return mRunQueue;\n    }\n}\n\npublic class HandlerActionQueue {\n    private HandlerAction[] mActions;\n    private int mCount;\n\n    public void post(Runnable action) {\n        postDelayed(action, 0);\n    }\n\n    public void postDelayed(Runnable action, long delayMillis) {\n        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);\n\n        synchronized (this) {\n            if (mActions == null) {\n                // 单例的数组\n                mActions = new HandlerAction[4];\n            }\n            // 将其添加到数组里面\n            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);\n            mCount++;\n        }\n    }\n\n    // 消息被执行的时机，我们需要知道这个方法什么时候被调用\n    public void executeActions(Handler handler) {\n        synchronized (this) {\n            final HandlerAction[] actions = mActions;\n            for (int i = 0, count = mCount; i < count; i++) {\n                final HandlerAction handlerAction = actions[i];\n                // 这里进行了消息处理\n                handler.postDelayed(handlerAction.action, handlerAction.delay);\n            }\n            mActions = null;\n            mCount = 0;\n        }\n    }\n```\n\n从上面看出当调用 View 的 post() 方法的时候，消息不会被立即执行，而是添加到了数组里面。然后当 `executeActions()` 被调用的时候再执行，下面是 `executeActions()` 执行的时机：\n\n```java\npublic class View {\n    void dispatchAttachedToWindow(AttachInfo info, int visibility) {\n        // ... 这里进行了赋值\n        mAttachInfo = info;\n        // ... 这里执行了消息\n        if (mRunQueue != null) {\n            mRunQueue.executeActions(info.mHandler);\n            mRunQueue = null;\n        }\n        // ...\n    }\n}\n```\n\n可见，当调用 View 的 post() 方法的时候，这个消息会根据 View 是否被添加到窗口里面来决定是否立即执行。如果该 View 已经添加到了窗口中，那么该消息会被立即执行，否则等到 View 被添加到窗口的时候执行。执行事件也是通过 Handler 来执行的。\n\n### 2.3 列表控件\n\n**问题：ListView 的优化**    \n**问题：ListView 重用的是什么**\n\nListView 默认缓存一页的 View，也就是你当前 Listview 界面上有几个 Item 可以显示，Lstview 就缓存几个。当现实第一页的时候，由于没有一个 Item 被创建，所以第一页的 Item 的 `getView()` 方法中的第二个参数都是为 null 的。\n\nViewHolder 同样也是为了提高性能。就是用来在缓存使用 `findViewById()` 方法得到的控件，下次的时候可以直接使用它而不用再进行 `find` 了。\n\n**问题：RecycleView 的使用，原理，RecycleView 优化**    \n**问题：Recycleview Listview 的区别，性能**  \n\n1. 装饰；\n2. 手势滑动、拖拽；\n3. 顶部悬浮效果；\n\n**问题：RecyclerView 的缓存的实现机制**\n\n\n\n**问题：Listview 图片加载错乱的原理和解决方案**\n\n### 2.4 其他控件\n\n**问题：LinearLayout、RelativeLayout、FrameLayout 的特性、使用场景**     \n**问题：ViewPager 使用细节，如何设置成每次只初始化当前的 Fragment，其他的不初始化**\n\n### 2.5 数据存储\n\n**问题：Android 中数据存储方式**\n\nSP，SQLite，ContentProvider，File，Server\n\n## 3、架构相关\n\n**问题：模块化实现（好处，原因）**     \n**问题：项目组件化的理解**    \n**问题：模式 MVP、MVC 介绍**    \n**问题：MVP 模式**\n\n`MVC (Model-View-Controller, 模型-视图-控制器)`，标准的 MVC 是这个样子的：\n\n1. `模型层 (Model)`：业务逻辑对应的数据模型，无 View 无关，而与业务相关；\n2. `视图层 (View)`：一般使用 XML 或者 Java 对界面进行描述；\n3. `控制层 (Controllor)`：在 Android 中通常指 Activity  和Fragment，或者由其控制的业务类。\n\n在 Android 开发中，就是指直接使用 Activity 并在其中写业务逻辑的开发方式。显然，一方面 Activity 本身就是一个视图，另一方面又要负责处理业务逻辑，因此逻辑会比较混乱。这种开发方式不太适合 Android 开发。\n\n`MVP (Model-View-Presenter)`：\n\n1. `模型层 (Model)`：主要提供数据存取功能。\n2. `视图层 (View)`：处理用户事件和视图。在 Android 中，可能是指 Activity、Fragment 或者 View。\n3. `展示层 (Presenter)`：负责通过 Model 存取书数据，连接 View 和 Model，从 Model 中取出数据交给 View。\n\n实际开发中会像下面这样定义一个契约接口\n\n```java\n    public interface HomeContract {\n\n        interface IView extends BaseView {\n            void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists);\n            void setNextPage(List<HomeBean.IssueList.ItemList> itemLists);\n            void onError(String msg);\n        }\n\n        interface IPresenter extends BasePresenter {\n            void requestFirstPage();\n            void requestNextPage();\n        }\n    }\n```\n\n然后让 Fragment 或者 Activity 等继承 IView，实例化一个 IPresenter，并在构造方法中将自己引入到 IPresenter 中。这样 View 和 Presenter 就相互持有了对方的引用。当要发起一个网络请求的时候，View 中调用 Presenter 的方法，Presenter 拿到了结果之后回调 View 的方法。这样就使得 View 和 Presenter 只需要关注自身的责任即可。\n\nMVP 缺点：1). Presenter 中除了应用逻辑以外，还有大量的 View->Model，Model->View 的手动同步逻辑，`造成 Presenter 比较笨重`，维护起来会比较困难；2). 由于对视图的渲染放在了 Presenter 中，所以`视图和 Presenter 的交互会过于频繁`；3). `如果 Presenter 过多地渲染了视图，往往会使得它与特定的视图的联系过于紧密`，一旦视图需要变更，那么 Presenter 也需要变更了。\n\nMVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化，让我们将视图 UI 和业务逻辑分开。\n\n1. `模型层 (Model)`：负责从各种数据源中获取数据；\n2. `视图层 (View)`：在 Android 中对应于 Activity 和 Fragment，用于展示给用户和处理用户交互，会驱动 ViewModel 从 Model 中获取数据；\n3. `ViewModel 层`：用于将 Model 和 View 进行关联，我们可以在 View 中通过 ViewModel 从 Model 中获取数据；当获取到了数据之后，会通过自动绑定，比如 DataBinding，来将结果自动刷新到界面上。\n\n优点：\n\n1. `低耦合`：视图（View）可以独立于Model变化和修改，一个 ViewModel 可以绑定到不同的 View 上，当 View 变化的时候 Model 可以不变，当 Model 变化的时候 View 也可以不变。\n2. `可重用性`：你可以把一些视图逻辑放在一个 ViewModel 里面，让很多 view 重用这段视图逻辑。\n3. `独立开发`：开发人员可以专注于业务逻辑和数据的开发（ViewModel），设计人员可以专注于页面设计。\n4. `可测试`：界面素来是比较难于测试的，而现在测试可以针对 ViewModel 来写。\n\n## 4、系统源码\n\n**问题：画出 Android 的大体架构图**\n\n<div align=\"center\"><img src=\"res/QQ截图20190225213434.png\" width=\"300\"/></div>\n\n**问题：App 启动流程，从点击桌面开始**    \n**问题：Activity 栈**    \n**问题：简述 Activity 启动全部过程？**    \n**问题：ActicityThread 相关？**\n\n `startActivity()` -> `Process.start()` -> Socket -> (SystemServer) -> Zygote.fork() -> VM,Binder 线程池 -> ActivityThread.main()\n\n`Instrumentation.execStartActivity()` -> `IApplicationThread+AMS+H` -> 校验用户信息等 -> 解析 Intent -> 回调 `IApplicationThread.scheduleTransaction()`\n\n**问题：App 是如何沙箱化，为什么要这么做**\n\nAndroid 是一个权限分离的系统，这是利用 Linux 已有的权限管理机制，通过为每一个 Application 分配不同的 uid 和 gid，从而使得不同的 Application 之间的私有数据和访问（native 以及 java 层通过这种 sandbox 机制，都可以）达到隔离的目的 。与此同时，Android 还在此基础上进行扩展，提供了 permission 机制，它主要是用来对 Application 可以执行的某些具体操作进行权限细分和访问控制，同时提供了 per-URI permission 机制，用来提供对某些特定的数据块进行 ad-hoc 方式的访问。 \n\n**问题：权限管理系统（底层的权限是如何进行 grant 的）**    \n**问题：动态权限适配方案，权限组的概念**\n\n**问题：大体说清一个应用程序安装到手机上时发生了什么**    \n**问题：应用安装过程**\n\n应用安装的时候涉及几个类，分别时 PackageManager, ApplicationPackageManager 和 PMS. 它们之间的关系是，PackageManager 是一个抽象类，它的具体实现是 ApplicationPackageManager，而后者的所有实现都是靠 PMS 实现的。PMS 是一种远程的服务，与 AMS 相似，在同一方法中启动。另外，还有 `Installer` 它是安装应用程序的辅助类，它也是一种系统服务，与 PMS 在同一方法中启动。它会`通过 Socket 与远程的 Installd` 建立联系。这是因为权限的问题，`PMS 只有 system 权限。installd 却是具有 root 权限`。（Installd 的作用好像就是创建一个目录）\n\ninstalld 是由 Android 系统 init 进程 (pid=1)，在解析 init.rc 文件的代码时，通过 fork 创建用户空间的守护进程 intalld。启动时，进入监听 socket，当客户端发送过来请求时，接收客户端的请求，并读取客户端发送过来的命令数据，并根据读取客户端命令来执行命令操作。\n\nAndroid 上应用安装可以分为以下几种方式：\n\n1. 系统安装：开机的时候，没有安装界面\n2. adb 命令安装：通过 abd 命令行安装，没有安装界面\n3. 应用市场安装，这个要视应用的权限，有系统的权限无安装界面(例如MUI的小米应用商店)\n4. 第三方安装，有安装界面，通过 packageinstaller.apk 来处理安装及卸载的过程的界面\n\nApk 的大体流程如下：\n\n1. 第一步：拷贝文件到指定的目录：在 Android 系统中，apk 安装文件是会被保存起来的，默认情况下，用户安装的 apk 首先会被拷贝到 `/data/app` 目录下，/data/app 目录是用户有权限访问的目录，在安装 apk 的时候会自动选择该目录存放用户安装的文件，而系统出场的 apk 文件则被放到了 `/system` 分区下，包括 `/system/app`，`/system/vendor/app`，以及 `/system/priv-app` 等等，该分区只有 ROOT 权限的用户才能访问，这也就是为什么在没有 Root 手机之前，我们没法删除系统出场的 app 的原因了。\n2. 第二步：解压缩 Apk，拷贝文件，创建应用的数据目录：为了加快 app 的启动速度，apk 在安装的时候，会首先将 app 的可执行文件 dex 拷贝到 `/data/dalvik-cache` 目录，缓存起来。然后，在 `/data/data/` 目录下创建应用程序的数据目录(以应用的包名命名)，存放在应用的相关数据，如数据库、xml文件、cache、二进制的 so 动态库等。\n3. 第三步：`解析 apk 的 AndroidManifest.xml 文件`：提取出这个 apk 的重要信息写入到 packages.xml 文件中，这些信息包括：权限、应用包名、APK 的安装位置、版本、userID 等等。\n4. 第四步：`显示快捷方式`：Home 应用程序，负责从 PackageManagerService 服务中把这些安装好的应用程序取出来。在 Android 系统中，负责把系统中已经安装的应用程序在桌面中展现出来的 Home 应用就是 Launcher 了。\n\n普通安装：\n\nPackagInstaller 是安卓上默认的应用程序，用它来安装普通文件。PackageInstaller 调用一个叫做 InstallAppProgress 的 activity 来获取用户发出的指令。InstallAppProgress 会请求Package Manager 服务，然后通过 installed 来安装包文件。它提供了安装的页面和安装进度相关页面，我们平时安装应用时显式的就是它。（[源码](https://android.googlesource.com/platform/packages/apps/PackageInstaller)）\n\n最终的安卓过程则是交给 PMS 的 `installPackageLI()` 方法来完成，它也会先对 manifest 进行解析，然后将解析的结果添加到 PMS 的缓存中，并注册四大组件。\n\n如果是第一次安装的时候就会调用 `scanPackageLI()` 方法来进行安装。\n\n![安装大致流程图](https://upload-images.jianshu.io/upload_images/5713484-70c0a869e6c3ad26.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/806/format/webp)\n\n1. PMS 在启动 SystemSever 时启动，调用构造方法，对目录进行扫描，包括系统、供应商等的目录，复制 APK，解析 APK，缓存 APK 信息；\n2. `ADB -> pm.jar -> PMS -> Installd（） -> Installer（系统服务）`\n\n**问题：系统启动流程 Zygote 进程 –> SystemServer 进程 –> 各种系统服务 –> 应用进程**\n\n按下电源之后，首先加载引导程序 BootLoader 到 RAM；然后，执行引导程序 BootLoader 以把系统 OS 拉起来；接着，启动 Linux 内核；内核中启动的第一个用户进程是 init 进程，init 进程会通过解析 init.rc 来启动 zygote 服务；Zygote 又会进一步的启动 SystemServer；在 SystemServer 中，Android 会启动一系列的系统服务供用户调用。\n\nInit 进程会启动之后会解析 `init.rc` 文件，该文件由固定格式的指令组成，在 AOSP 中有说明它的规则。其中的每个指令对应系统中的类。在解析文件的时候会将其转换成对应的类的实例来保存，比如 service 开头的指令会被解析成 Service 对象。在解析 `init.rc` 文件之前会根据启动时的属性设置加载指定的 rc 文件到系统中。当解析完毕之后系统会触发初始化操作。它会通过调用 Service 的 `start()` 方法来启动属性服务，然后进入 `app_main.cpp` 的 main() 方法。这个方法中会根据 service 指令的参数来决定调用 Java 层的 ZygoteInit 还是 RuntimeInit. 这里会调用后者的 ZygoteInit 初始化 Zygote 进程。\n\n```c++\n    if (zygote) {\n        runtime.start(\"com.android.internal.os.ZygoteInit\", args, zygote);\n    } else if (className) {\n        runtime.start(\"com.android.internal.os.RuntimeInit\", args, zygote);\n    } else {\n        app_usage();\n    }\n```\n\n它调用的方式是通过 runtime 的 `start()` 方法，而这个 runtime 就是 AndroidRuntime. 但是在执行 Java 类之前得先有虚拟机，所以它会先启动虚拟机实例，然后再调用 ZygoteInit 的方法。所以，AndroidRuntime 的 `start()` 方法中主要做了三件事情：1).调用函数 `startVM()` 启动虚拟机；2).调用函数 `startReg()` 注册 JNI 方法；3).调用 `com.android.internal.os.ZygoteInit` 类的 `main()` 函数。\n\nZygoteInit 用来初始化 Zygote 进程的，它的 `main()` 函数中主要做了三件事情：\n\n1. 调用 `registerZygoteSocket()` 函数创建了一个 socket 接口，用来和 AMS 通讯；（Android 应用程序进程启动过程中，AMS 是通过 `Process.start()` 函数来创建一个新的进程的，而 `Process.start()` 函数会首先通过 Socket 连接到 Zygote 进程中，最终由 Zygote 进程来完成创建新的应用程序进程，而 Process 类是通过 `openZygoteSocketIfNeeded()` 函数来连接到 Zygote 进程中的 Socket.）\n2. 调用 `startSystemServer()` 函数来启动 SystemServer 组件，Zygote 进程通过 `Zygote.forkSystemServer()` 函数来创建一个新的进程来启动 SystemServer 组件；（SystemServer 的 main 方法将会被调用，并在这里启动 Binder 中的 ServiceManager 和各种系统运行所需的服务，PMS 和 AMS 等）\n3. 调用 `runSelectLoopMode()` 函数进入一个无限循环在前面创建的 socket 接口上等待 AMS 请求创建新的应用程序进程。\n\n总结一下：\n\n1. 系统启动时 init 进程会创建 Zygote 进程，Zygote 进程负责后续 Android 应用程序框架层的其它进程的创建和启动工作。\n2. Zygote 进程会首先创建一个 SystemServer 进程，SystemServer 进程负责启动系统的关键服务，如包管理服务 PMS 和应用程序组件管理服务 AMS。\n3. 当我们需要启动一个 Android 应用程序时，AMS 会通过 Socket 进程间通信机制，通知 Zygote 进程为这个应用程序创建一个新的进程。\n\n**问题：描述清点击 Android Studio 的 build 按钮后发生了什么**\n\n编译打包的过程->adb->安装过程 PMS->应用启动过程 AMS\n\n## 附录\n\n1. *参考：[Android ContentProvider的线程安全（一）](https://blog.csdn.net/zhanglianyu00/article/details/78362960)*\n2. *了解 AsyncTask 的源码，可以参考笔者的这篇文章：[《Android AsyncTask 源码分析》](https://juejin.im/post/5b65c71af265da0f9402ca4a)*\n3. *更多内容请参考：[Android：解析 SurfaceView & TextureView](https://blog.csdn.net/github_35186068/article/details/87895365)*\n4. *事件分发机制和 View 的体系请参考笔者文章：[《View 体系详解：坐标系、滑动、手势和事件分发机制》](https://juejin.im/post/5bbb5fdce51d450e942f6be4)*\n5. *关于 ListView 的 ViewHolder 等的使用，可以参考这篇文章：[ListView 复用和优化详解](https://blog.csdn.net/u011692041/article/details/53099584)*\n6. *关于移动应用架构部分内容可以参考笔者的文章：[《Android 架构设计：MVC、MVP、MVVM和组件化》](https://juejin.im/post/5b7c1706f265da436d7e408e)*\n7. 关于 MVVM 架构设计中的 ViewModel 和 LiveData 的机制可以参考：[《浅谈 ViewModel 的生命周期控制》](https://juejin.im/post/5c3dacde518825247c723ab5)\n8. *[浅谈 LiveData 的通知机制](https://juejin.im/post/5c40a95a6fb9a049bc4cf0a8)*\n\n\n\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_6_性能优化.md",
    "content": "# Android 高级面试-6：性能优化\n\n## 1、内存优化\n\n### 1.1 OOM\n\n**问题：OOM 的几种常见情形？**\n\n1. `数据太大`：比如加载图片太大，原始的图片没有经过采样，完全加载到内存中导致内存爆掉。\n2. `内存泄漏`\n3. `内存抖动`：内存抖动是指内存频繁地分配和回收，而`频繁的 GC `会导致卡顿，严重时还会导致 OOM。一个很经典的案例是 String 拼接时创建大量小的对象。此时由于大量小对象频繁创建，导致内存不连续，无法分配大块内存，系统直接就返回 OOM 了。\n\n**问题：OOM 是否可以 Try Catch ？**\n\nCatch 是可以 Catch 到的，但是这样不符合规范，Error 说明程序中发生了错误，我们应该使用引用四种引用、增加内存或者减少内存占用来解决这个问题。\n\n### 1.2 内存泄漏\n\n**问题：常见的内存泄漏的情形，以及内存泄漏应该如何分析？**\n\n1. `单例` 引用了 Activity 的 Context，可以使用 `Context.getApplicationContext()` 获取整个应用的 Context 来使用；\n2. `静态变量` 持有 Activity 的引用，原因和上面的情况一样，比如为了避免反复创建一个内部实例的时候使用静态的变量；\n3. `非静态内部类` 导致内存泄露，典型的有：\n    1. Handler：Handler 默认持有外部 Activity 的引用，发送给它的 Message 持有 Handler 的引用，Message 会被放入 MQ 中，因此可能会造成泄漏。解决方式是使用弱引用来持有外部 Activity 的引用。另一种方式是在 Activity 的 `onDestroy()` 方法中调用 `mHandler.removeCallbacksAndMessages(null)` 从 MQ 中移除消息。 后者更好一些！因为它移除了 Message. \n    2. 另一种情形是使用非静态的 Thread 或者 AsyncTask，因为它们持有 Activity 的引用，解决方式是使用 `静态内部类+弱引用`。\n4. `广播`：未取消注册广播。在 Activity 中注册广播，如果在 Activity 销毁后不取消注册，那么这个刚播会一直存在系统中，同上面所说的非静态内部类一样持有 Activity 引用，导致内存泄露。\n5. `资源`：未关闭或释放导致内存泄露。使用 IO、File 流或者 Sqlite、Cursor 等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲，如果及时不关闭，这些缓冲对象就会一直被占用而得不到释放，以致发生内存泄露。\n6. `属性动画`：在 Activity 中启动了属性动画（ObjectAnimator），但是在销毁的时候，没有调用 cancle 方法，虽然我们看不到动画了，但是这个动画依然会不断地播放下去，动画引用所在的控件，所在的控件引用 Activity，这就造成 Activity 无法正常释放。因此同样要在 Activity 销毁的时候 cancel 掉属性动画，避免发生内存泄漏。\n7. `WebView`：WebView 在加载网页后会长期占用内存而不能被释放，因此我们在 Activity 销毁后要调用它的 destory() 方法来销毁它以释放内存。\n\n### 1.3 内存优化相关的工具\n\n1. `检查内存泄漏`：Square 公司开源的用于检测内存泄漏的库，[LeakCanary](https://github.com/square/leakcanary).\n2. `Memory Monitor`：AS 自带的工具，可以用来主动触发 GC，获取堆内存快照文件以便进一步进行分析（通过叫做 Allocation Tracker 的工具获取快照）。（属于开发阶段使用的工具，开发时应该多使用它来检查内存占用。）\n3. `Device Monitor`：包含多种分析工具：线程，堆，网络，文件等（位于 sdk 下面的 tools 文件夹中）。可以通过这里的 Heap 选项卡的 Cause GC 按钮主动触发 GC，通过内存回收的状态判断是否发生了内存泄漏。\n4. `MAT`：首先通过 DDMS 的 Devices 选项卡下面的 Dump HPROF File 生成 hrpof 文件，然后用 SDK 的 hprof-conv 将该文件转成标准 hprof 文件，导入 MAT 中进行分析。 \n\n## 3、ANR\n\n**问题：ANR 的原因**    \n**问题：ANR 怎么分析解决**\n\n满足下面的一种情况系统就会弹出 ANR 提示\n\n1. `输入事件 (按键和触摸事件) 5s` 内没被处理；\n2. BroadcastReceiver 的事件 ( onRecieve() 方法) 在规定时间内没处理完 (`前台广播为 10s，后台广播为 60s`)；\n3. Service `前台 20s 后台 200s` 未完成启动；\n4. ContentProvider 的 `publish() 在 10s` 内没进行完。\n\n最终弹出 ANR 对话框的位置是与 AMS 同目录的类 `AppErrors 的 handleShowAnrUi()` 方法。最初抛出 ANR 是在 InputDispatcher.cpp 中。后回在上述方法调用 AMS 的 inputDispatchingTimedOut() 方法继续处理，并最终在 inputDispatchingTimedOut() 方法中将事件传递给 AppErrors。\n\n解决方式:\n\n1. 使用 adb 导出 ANR 日志并进行分析，发生 ANR的时候系统会记录 ANR 的信息并将其存储到 /data/anr/traces.txt 文件中（在比较新的系统中会被存储都 /data/anr/anr_* 文件中）。或者在开发者模式中选择将日志导出到 sdcard 之后再从 sdcard 将日志发送到电脑端进行查看\n2. 使用 DDMS 的 `traceview` 进行分析：到 SDK 安装目录的 tools 目录下面使用 monitor.bat 打开 DDMS。使用 TraceView 来通过耗时方法调用的信息定位耗时操作的位置。\n3. 使用开源项目 `ANR-WatchDog` 来检測 ANR：创建一个检测线程，该线程不断往 UI 线程 post 一个任务，然后睡眠固定时间，等该线程又一次起来后检測之前 post 的任务是否运行了，假设任务未被运行，则生成 ANRError，并终止进程。\n\n常见的 ANR 场景：\n\n1. `I/O 阻塞`\n2. `网络阻塞`\n3. `多线程死锁`\n4. `由于响应式编程等导致的方法死循环`\n5. `由于某个业务逻辑执行的时间太长`\n\n避免 ANR 的方法：\n\n1. UI 线程尽量只做跟 UI 相关的工作；\n2. 耗时的工作 (比如数据库操作，I/O，网络操作等)，采用`单独的工作线程`处理；\n3. 用 `Handler` 来处理 UI 线程和工作线程的交互；\n4. 使用 `RxJava` 等来处理异步消息。\n\n## 4、性能调优工具\n\n## 5、优化经验\n\n### 5.1 优化经验\n\n虽然一直强调优化，但是许多优化应该是在开发阶段就完成的，程序逻辑的设计可能会影响程序的性能。如果开发完毕之后再去考虑对程序的逻辑进行优化，那么阻力会比较大。因此，编程的时候应该养成好的编码习惯，同时注意收集性能优化的经验，在开发的时候进行避免。\n\n代码质量检查工具：\n\n1. 使用 `SonarLint` 来对代码进行静态检查，使代码更加符合规范；\n2. 使用`阿里的 IDEA 插件`对 Java 的代码质量进行检查；\n\n在 Android4.4 以上的系统上，对于 Bitmap 的解码，decodeStream() 的效率要高于 decodeFile() 和 decodeResource(), 而且高的不是一点。所以解码 Bitmap 要使用 decodeStream()，同时传给 decodeStream() 的文件流是 BufferedInputStream：\n\n```kotlin\nval bis =  BufferedInputStream(FileInputStream(filePath))\nval bitmap = BitmapFactory.decodeStream(bis,null,ops)\n```\n\nJava 相关的优化：\n\n1. `静态优于抽象`：如果你并不需要访问一个对系那个中的某些字段，只是想调用它的某些方法来去完成一项通用的功能，那么可以将这个方法设置成静态方法，调用速度提升 15%-20%，同时也不用为了调用这个方法去专门创建对象了，也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。\n\n2. `多使用系统封装好的 API`：系统提供不了的 Api 完成不了我们需要的功能才应该自己去写，因为使用系统的 Api 很多时候比我们自己写的代码要快得多，它们的很多功能都是通过底层的汇编模式执行的。举个例子，实现数组拷贝的功能，使用循环的方式来对数组中的每一个元素一一进行赋值当然可行，但是直接使用系统中提供的 `System.arraycopy()` 方法会让执行效率快 9 倍以上。\n\n3. `避免在内部调用 Getters/Setters 方法`：面向对象中封装的思想是不要把类内部的字段暴露给外部，而是提供特定的方法来允许外部操作相应类的内部字段。但在 Android 中，字段搜寻比方法调用效率高得多，我们直接访问某个字段可能要比通过 getters 方法来去访问这个字段快 3 到 7 倍。但是编写代码还是要按照面向对象思维的，我们应该在能优化的地方进行优化，比如避免在内部调用 getters/setters 方法。\n\n4. `使用 static final 修饰常量`：因为常量会在 dex 文件的初始化器当中进行初始化。当我们调用 intVal 时可以直接指向 42 的值，而调用 strVal 会用一种相对轻量级的字符串常量方式，而不是字段搜寻的方式。这种优化方式只对基本数据类型以及 String 类型的常量有效，对于其他数据类型的常量无效。\n\n5. `合理使用数据结构`：比如 `android.util` 下面的 `Pair<F, S>`，在希望某个方法返回的数据恰好是两个的时候可以使用。显然，这种返回方式比返回数组或者列表含义清晰得多。延申一下：`有时候合理使用数据结构或者使用自定义数据结构，能够起到化腐朽为神奇的作用`。\n\n6. `多线程`：不要开太多线程，如果小任务很多建议使用线程池或者 AsyncTask，建议直接使用 RxJava 来实现多线程，可读性和性能更好。\n\n7. `合理选择数据结构`：根据具体应用场景选择 LinkedList 和 ArrayList，比如 Adapter 中查找比增删要多，因此建议选择 ArrayList. \n\n8. `合理设置 buffer`：在读一个文件我们一般会设置一个 buffer。即先把文件读到 buffer 中，然后再读取 buffer 的数据。所以: 真正对文件的次数 = 文件大小 / buffer大小 。 所以如果你的 buffer 比较小的话，那么读取文件的次数会非常多。当然在写文件时 buffer 是一样道理的。很多同学会喜欢设置 1KB 的 buffer，比如 byte buffer[] = new byte[1024]。如果要读取的文件有 20KB， 那么根据这个 buffer 的大小，这个文件要被读取 20 次才能读完。\n\n9. ListView 复用，`getView()` 里尽量复用 conertView，同时因为 `getView()` 会频繁调用，要避免频繁地生成对象。\n\n10. `谨慎使用多进程`，现在很多App都不是单进程，为了保活，或者提高稳定性都会进行一些进程拆分，而实际上即使是空进程也会占用内存(1M左右)，对于使用完的进程，服务都要及时进行回收。\n\n11. 尽量使用系统资源，系统组件，图片甚至控件的 id.\n\n12. `数据相关`：序列化数据使用 protobuf 可以比 xml 省 30% 内存，慎用 shareprefercnce，因为对于同一个 sp，会将整个 xml 文件载入内存，有时候为了读一个配置，就会将几百 k 的数据读进内存，数据库字段尽量精简，只读取所需字段。\n\n13. `dex优化，代码优化，谨慎使用外部库`，有人觉得代码多少于内存没有关系，实际会有那么点关系，现在稍微大一点的项目动辄就是百万行代码以上，多 dex 也是常态，不仅占用 rom 空间，实际上运行的时候需要加载 dex 也是会占用内存的(几 M )，有时候为了使用一些库里的某个功能函数就引入了整个庞大的库，此时可以考虑抽取必要部分，开启 proguard 优化代码，使用 Facebook redex 使用优化 dex (好像有不少坑)。\n\n常用的程序性能测试方法\n\n1. `时间测试`：方式很简单只要在代码的上面和下面定义一个long型的变量，并赋值给当前的毫秒数即可。比如\n    \n    ```java\n    long sMillis = System.currentTimeMillis();\n    // ...代码块\n    long eMillis = System.currentTimeMillis();\n    ```\n然后两者相减即可得到程序的运行时间。\n\n2. `内存消耗测试`：获取代码块前后的内存，然后相减即可得到这段代码当中的内存消耗。获取当前内存的方式是\n\n    ```java\n    long total = Runtime.getRuntime().totalMemory(); // 获取系统中内存总数\n    long free = Runtime.getRuntime().freeMemory(); // 获取剩余的内存总数\n    long used = total - free; // 使用的内存数\n    ```\n\n在使用的时候只要在代码块的两端调用 `Runtime.getRuntime().freeMemory()` 然后再相减即可得到使用的内存总数。\n\n### 5.2 布局优化\n\n1. 在选择使用 Android 中的布局方式的时候应该遵循：尽量少使用性能比较低的容器控件,比如 RelativeLayout，但如果使用 RelativeLayout 可以降低布局的层次的时候可以考虑使用。\n2. 使用 `<include>` 标签复用布局：多个地方共用的布局可以使用 `<include>` 标签在各个布局中复用；\n3. 可以通过使用 `<merge>` 来降低布局的层次。 `<merge>` 标签通常与 `<include>` 标签一起使用， `<merge>` 作为可以复用的布局的根控件。然后使用 `<include>` 标签引用该布局。\n4. 使用 `<ViewStub>` 标签动态加载布局：`<ViewStub>` 标签可以用来在程序运行的时候决定加载哪个布局，而不是一次性全部加载。\n5. 性能分析：使用 `Android Lint` 来分析布局；\n6. 性能分析：避免过度绘制，在手机的开发者选项中的绘图选项中选择显示布局边界来查看布局\n7. 性能分析：`Hierarchy View`，可以通过 Hierarchy View 来获取当前的 View 的层次图\n8. 使用 `ConstaintLayout`：用来降低布局层次；\n9. 性能分析：使用 `systrace` 分析 UI 性能；\n10. onDraw() 方法会被频繁调用，因此不应该在其中做耗时逻辑和声明对象\n\n### 5.3 内存优化\n\n1. `防止内存泄漏`：见内存泄漏；\n\n2. `使用优化过的集合`；\n\n3. `使用优化过的数据集合`：如 `SparseArray`、`SparseBooleanArray`等来替换 HashMap。因为 HashMap 的键必须是对象，而对象比数值类型需要多占用非常多的空间。\n\n4. `少使用枚举`：枚举可以合理组织数据结构，但是枚举是对象，比普通的数值类型需要多使用很多空间。\n\n5. `当内存紧张时释放内存`：`onTrimMemory()` 方法还有很多种其他类型的回调，可以在手机内存降低的时候及时通知我们，我们应该根据回调中传入的级别来去决定如何释放应用程序的资源。\n\n6. 读取一个 Bitmap 图片的时候，不要去加载不需要的分辨率。可以压缩图片等操作，使用性能稳定的图片加载框架，比如 Glide.\n\n7. `谨慎使用抽象编程`：在 Android 使用抽象编程会带来额外的内存开支，因为抽象的编程方法需要编写额外的代码，虽然这些代码根本执行不到，但是也要映射到内存中，不仅占用了更多的内存，在执行效率上也会有所降低。所以需要合理的使用抽象编程。\n\n8. `尽量避免使用依赖注入框架`：使用依赖注入框架貌似看上去把 findViewById() 这一类的繁琐操作去掉了，但是这些框架为了要搜寻代码中的注解，通常都需要经历较长的初始化过程，并且将一些你用不到的对象也一并加载到内存中。这些用不到的对象会一直站用着内存空间，可能很久之后才会得到释放，所以可能多敲几行代码是更好的选择。\n\n9. `使用多个进程`：谨慎使用，多数应用程序不该在多个进程中运行的，一旦使用不当，它甚至会增加额外的内存而不是帮我们节省内存。这个技巧比较适用于哪些需要在后台去完成一项独立的任务，和前台是完全可以区分开的场景。比如音乐播放，关闭软件，已经完全由 Service 来控制音乐播放了，系统仍然会将许多 UI 方面的内存进行保留。在这种场景下就非常适合使用两个进程，一个用于 UI 展示，另一个用于在后台持续的播放音乐。关于实现多进程，只需要在 Manifast 文件的应用程序组件声明一个`android:process` 属性就可以了。进程名可以自定义，但是之前要加个冒号，表示该进程是一个当前应用程序的私有进程。\n\n10. `分析内存的使用情况`：系统不可能将所有的内存都分配给我们的应用程序，每个程序都会有可使用的内存上限，被称为堆大小。不同的手机堆大小不同，如下代码可以获得堆大小 `int heapSize = AMS.getMemoryClass()` 结果以 MB 为单位进行返回，我们开发时应用程序的内存不能超过这个限制，否则会出现 OOM。\n\n11. `节制的使用 Service`：如果应用程序需要使用 Service 来执行后台任务的话，只有当任务正在执行的时候才应该让 Service 运行起来。当启动一个 Service 时，系统会倾向于将这个 Service 所依赖的进程进行保留，系统可以在 LRUcache 当中缓存的进程数量也会减少，导致切换程序的时候耗费更多性能。我们可以使用 IntentService，当后台任务执行结束后会自动停止，避免了 Service 的内存泄漏。\n\n12. 字符串优化：[Android 性能优化之String篇](https://blog.csdn.net/vfush/article/details/53038437)\n\n### 5.4 异常崩溃 & 稳定性\n\n**问题：如何保持应用的稳定性**\n**问题：App 启动崩溃异常捕捉**\n\n1. 使用热补丁\n2. 自己写代码捕获异常\n3. 使用异常收集工具\n4. 开发就是测试，自己的逻辑自己先测一边\n\n### 5.5 优化工具\n\n**问题：性能优化如何分析 systrace？**\n\n下面将简单介绍几个主流的辅助分析内存优化的工具，分别是 \n\n1. MAT (Memory Analysis Tools)\n2. Heap Viewer\n3. Allocation Tracker\n4. Android Studio 的 Memory Monitor\n5. LeakCanary\n\nhttps://www.jianshu.com/p/0df5ad0d2e6a\n\nMAT (Memory Analysis Tools)，作用：查看当前内存占用情况。通过分析 Java 进程的内存快照 HPROF 分析，快速计算出在内存中对象占用的大小，查看哪些对象不能被垃圾收集器回收 & 可通过视图直观地查看可能造成这种结果的对象\n\n- [MAT - Memory Analyzer Tool 使用进阶](http://www.lightskystreet.com/2015/09/01/mat_usage/)    \n- [MAT使用教程](https://blog.csdn.net/itomge/article/details/48719527)\n\nHeap Viewer，定义：一个的 Java Heap 内存分析工具。作用：查看当前内存快照。可查看分别有哪些类型的数据在堆内存总以及各种类型数据的占比情况。\n\n### 5.6 启动优化\n\n**问题：能优化，怎么保证应用启动不卡顿**\n**问题：统计启动时长,标准**\n\n1. 方式 1：使用 ADB：获取启动速度的第一种方式是使用 ADB，使用下面的指令的时候在启动应用的时候会使用 AMS 进行统计。但是缺点是统计时间不够准确：`adb shell am start -n ｛包名｝/｛包名｝.{活动名}`\n2. 方式 2：代码埋点：在 Application 的 attachBaseContext() 方法中记录开始时间，第一个 Activity 的 onWindowFocusChanged() 中记录结束时间。缺点是统计不完全，因为在 attachBaseContext() 之前还有许多操作。\n3. 方式 3：TraceView：在 AS 中打开 DDMS，或者到 SDK 安装目录的 tools 目录下面使用 monitor.bat 打开 DDMS。通过 TraceView 主要可以得到两种数据：单次执行耗时的方法以及执行次数多的方法。但 TraceView 性能耗损太大，不能比较正确反映真实情况。\n4. 方式 4：Systrace：Systrace 能够追踪关键系统调用的耗时情况，如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。\n5. 方式 5：Systrace + 插桩：类似于 AOP，通过切面为每个函数统计执行时间。这种方式的好处是能够准确统计各个方法的耗时。`TraceMethod.i(); /* do something*/ TraceMethod.o();`\n6. 方式 6：录屏：录屏方式收集到的时间，更接近于用户的真实体感。可以在录屏之后按帧来进行统计分析。\n\n启动优化\n\n1. `延迟初始化`：一些逻辑，如果没必要在程序启动的时候就立即初始化，那么可以将其推迟到需要的时候再初始化。比如，我们可以使用单例的方式来获取类的实例，然后在获取实例的时候再进行初始化操作。`但是需要注意的是，懒加载要防止集中化，否则容易出现首页显示后用户无法操作的情形。可以按照耗时和是否必要将业务划分到四个维度：必要且耗时，必要不耗时，非必要但耗时，非必要不耗时。` 然后对应不同的维度来决定是否有必要在程序启动的时候立即初始化。\n2. `防止主线程阻塞`：一般我们也不会把耗时操作放在主线程里面，毕竟现在有了 RxJava 之后，在程序中使用异步代价并不高。这种耗时操作包括，大量的计算、IO、数据库查询和网络访问等。另外，关于开启线程池的问题下面的话总结得比较好，除了一般意义上线程池和使用普通线程的区别，还要考虑应用启动这个时刻的特殊性，特定场景下单个时间点的表现 Thread 会比 ThreadPoolExecutor 好：同样的创建对象，ThreadPoolExecutor 的开销明显比 Thread 大。\n3. `布局优化`：如，之前我在使用 Fragment 和 ViewPager 搭配的时候，发现虽然 Fragment 可以被复用，但是如果通过 Adapter 为 ViewPager 的每个项目指定了标题，那么这些标题控件不会被复用。当 ViewPager 的条目比较多的时候，甚至会造成 ANR.\n4. `使用启动页面防止白屏`：这种方法只是治标不治本的方法，就是在应用启动的时候避免白屏，可以通过设置自定义主题来实现。\n\n其他借鉴办法\n\n1. 使用 BlockCanary 检测卡顿：它的原理是对 Looper 中的 loop() 方法打处的日志进行处理，通过一个自定义的日志输出 Printer 监听方法执行的开始和结束。（更加详细的源码分析参考这篇文章：[Android UI卡顿监测框架BlockCanary原理分析](https://www.jianshu.com/p/e58992439793)）\n2. GC 优化：减少垃圾回收的时间间隔，所以在启动的过程中不要频繁创建对象，特别是大对象，避免进行大量的字符串操作，特别是序列化跟反序列化过程。一些频繁创建的对象，例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象，可以考虑移到 Native 实现。\n3. 类重排：如果我们的代码在打包的时候被放进了不同的 dex 里面，当启动的时候，如果需要用到的类分散在各个 dex 里面，那么系统要花额外的时间到各个 dex 里加载类。因此，我们可以通过类重排调整类在 Dex 中的排列顺序，把启动时用到的类放进主 dex 里。目前可以使用 ReDex 的 Interdex 调整类在 Dex 中的排列顺序。\n4. 资源文件重排：这种方案的原理时先通过测试找出程序启动过程中需要加载的资源，然后再打包的时候通过修改 7z 压缩工具将上述热点资源放在一起。这样，在系统进行资源加载的时候，这些资源将要用到的资源会一起被加载进程内存当中并缓存，减少了 IO 的次数，同时不需要从磁盘读取文件，来提高应用启动的速度。\n\n### 5.7 网络优化\n\n1. Network Monitor: Android Studio 内置的 Monitor工具中就有一个 Network Monitor;\n2. 抓包工具：Wireshark, Fiddler, Charlesr 等抓包工具，Android 上面的无 root 抓包工具；\n3. Stetho：Android 应用的调试工具。无需 Root 即可通过 Chrome，在 Chrome Developer Tools 中可视化查看应用布局，网络请求，SQLite，preference 等。\n4. Gzip 压缩：使用 Gzip 来压缩 request 和 response, 减少传输数据量, 从而减少流量消耗.\n5. 数据交换格式：JSON 而不是 XML，另外 Protocol Buffer 是 Google 推出的一种数据交换格式.\n6. 图片的 Size：使用 WebP 图片，修改图片大小；\n7. 弱网优化\n    1. 界面先反馈, 请求延迟提交例如, 用户点赞操作, 可以直接给出界面的点赞成功的反馈, 使用JobScheduler在网络情况较好的时候打包请求.\n    2. 利用缓存减少网络传输；\n    3. 针对弱网(移动网络), 不自动加载图片\n    4. 比方说 Splash 闪屏广告图片, 我们可以在连接到 Wifi 时下载缓存到本地; 新闻类的 App 可以在充电, Wifi 状态下做离线缓存\n8. IP 直连与 HttpDns：DNS 解析的失败率占联网失败中很大一种，而且首次域名解析一般需要几百毫秒。针对此，我们可以不用域名，才用 IP 直连省去 DNS 解析过程，节省这部分时间。HttpDNS 基于 Http 协议的域名解析，替代了基于 DNS 协议向运营商 Local DNS 发起解析请求的传统方式，可以避免 Local DNS 造成的域名劫持和跨网访问问题，解决域名解析异常带来的困扰。\n9. 请求频率优化：可以通过把网络数据保存在本地来实现这个需求，缓存数据，并且把发出的请求添加到队列中，当网络恢复的时候再及时发出。\n10. 缓存：App 应该缓存从网络上获取的内容，在发起持续的请求之前，app 应该先显示本地的缓存数据。这确保了 app 不管设备有没有网络连接或者是很慢或者是不可靠的网络，都能够为用户提供服务。\n\n### 5.8 电量优化\n\n### 5.9 RV 优化\n\n1. 数据处理和视图加载分离：从远端拉取数据肯定是要放在异步的，在我们拉取下来数据之后可能就匆匆把数据丢给了 VH 处理，其实，数据的处理逻辑我们也应该放在异步处理，这样 Adapter 在 notify change 后，ViewHolder 就可以简单无压力地做数据与视图的绑定逻辑。比如：`mTextView.setText(Html.fromHtml(data).toString());`这里的 Html.fromHtml(data) 方法可能就是比较耗时的，存在多个 TextView 的话耗时会更为严重，而如果把这一步与网络异步线程放在一起，站在用户角度，最多就是网络刷新时间稍长一点。\n2. 数据优化：页拉取远端数据，对拉取下来的远端数据进行缓存，提升二次加载速度；对于新增或者删除数据通过 DiffUtil 来进行局部刷新数据，而不是一味地全局刷新数据。\n3. 减少过渡绘制：减少布局层级，可以考虑使用自定义 View 来减少层级，或者更合理地设置布局来减少层级，不推荐在 RecyclerView 中使用 ConstraintLayout，有很多开发者已经反映了使用它效果更差。\n4. 减少 xml 文件 inflate 时间：xml 文件 inflate 出 ItemView 是通过耗时的 IO 操作，尤其当 Item 的复用几率很低的情况下，随着 Type 的增多，这种 inflate 带来的损耗是相当大的，此时我们可以用代码去生成布局，即 new View() 的方式。\n5. 如果 Item 高度是固定的话，可以使用 RecyclerView.setHasFixedSize(true); 来避免 requestLayout 浪费资源；\n6. 如果不要求动画，可以通过 `((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false);` 把默认动画关闭来提升效率。\n7. 对 TextView 使用 String.toUpperCase 来替代 `android:textAllCaps=\"true\"`；\n8. 通过重写 RecyclerView.onViewRecycled(holder) 来回收资源。\n9. 通过 RecycleView.setItemViewCacheSize(size); 来加大 RecyclerView 的缓存，用空间换时间来提高滚动的流畅性。\n10. 如果多个 RecycledView 的 Adapter 是一样的，比如嵌套的 RecyclerView 中存在一样的 Adapter，可以通过设置 RecyclerView.setRecycledViewPool(pool); 来共用一个 RecycledViewPool。\n11. 对 ItemView 设置监听器，不要对每个 Item 都调用 addXxListener，应该大家公用一个 XxListener，根据 ID 来进行不同的操作，优化了对象的频繁创建带来的资源消耗。\n\n### 6.0 APK 优化\n\n1. 开启混淆：哪些配置？\n2. 资源混淆：AndRes\n3. 只支持 armeabi-v7 架构的 so 库\n4. 手动 Lint 检查，手动删除无用资源：删除没有必要的资源文件\n5. 使用 Tnypng 等图片压缩工具对图片进行压缩\n6. 大部分图片使用 Webp 格式代替：可以给UI提要求，让他们将图片资源设置为 Webp 格式，这样的话图片资源会小很多。如果对图片颜色通道要求不高，可以考虑转 jpg，最好用 webp，因为效果更佳。\n7. 尽量不要在项目中使用帧动画\n8. 使用 gradle 开启 `shrinkResources ture`：但有一个问题，就是图片 id 没有被引用的时候会被变成一个像素，所以需要在项目代码中引用所有表情图片的 id。\n9. 减小 dex 的大小：\n    1. 尽量减少第三方库的引用\n    2. 避免使用枚举\n    3. 避免重复功能的第三方库\n10. 其他\n    1. 用 7zip 代替压缩资源。\n    2. 删除翻译资源，只保留中英文 \n    3. 尝试将 `andorid support` 库彻底踢出你的项目。\n    4. 尝试使用动态加载 so 库文件，插件化开发。\n    5. 将大资源文件放到服务端，启动后自动下载使用。\n\n## 6、相机优化\n\n参考相机优化相关的内容。\n\n### Bitmap 优化\n\nhttps://blog.csdn.net/carson_ho/article/details/79549382\n\n1. 使用完毕后 释放图片资源，优化方案： \n    1. 在 Android2.3.3（API 10）前，调用 Bitmap.recycle()方法 \n    2. 在 Android2.3.3（API 10）后，采用软引用（SoftReference）\n\n2. 根据分辨率适配 & 缩放图片\n\n3. 按需 选择合适的解码方式\n\n4. 设置 图片缓存\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_7_网络相关.md",
    "content": "# Android 高级面试-7：网络相关的三方库和网络协议等\n\n## 1、网络框架\n\n**问题：HttpUrlConnection, HttpClient, Volley 和 OkHttp 的区别？**\n\nHttpUrlConnection 的基本使用方式如下：\n\n```java\n    URL url = new URL(\"http://www.baidu.com\"); // 创建 URL\n    HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 获取 HttpURLConnection\n    connection.setRequestMethod(\"GET\"); // 设置请求参数\n    connection.setConnectTimeout(5 * 1000);\n    connection.connect();\n    InputStream inputStream = connection.getInputStream(); // 打开输入流\n    byte[] data = new byte[1024];\n    StringBuilder sb = new StringBuilder();\n    while (inputStream.read(data) != -1) { // 循环读取\n        String s = new String(data, Charset.forName(\"utf-8\"));\n        sb.append(s);\n    }\n    message = sb.toString();\n    inputStream.close(); // 关闭流\n    connection.disconnect(); // 关闭连接\n```\n\nHttpURLConnect 和 HttpClient 的对比：\n\n1. **功能方面**：HttpClient 库要`丰富很多，提供了很多工具`，封装了 http 的请求头，参数，内容体，响应，还有一些高级功能，代理、COOKIE、鉴权、压缩、连接池的处理。HttpClient 高级功能`代码写起来比较复杂`，对开发人员的要求会高一些，而 HttpURLConnection 对大部分工作进行了包装，屏蔽了不需要的细节，`适合开发人员直接调用`。另外，HttpURLConnection 在 2.3 版本增加了一些 HTTPS 方面的改进，4.0 版本增加一些响应的缓存。\n2. **稳定性上**：HttpURLConnect 是一个通用的、适合大多数应用的轻量级组件。这个类起步比较晚，很容易在主要 API 上做稳步的改善。但是 HttpURLConnection 在 Android 2.2 及以下版本上`存在一些 bug`，尤其是在读取 InputStream 时调用 `close()` 方法，可能会导致连接池失效。Android `2.3 及以上版本`建议选用 HttpURLConnection，2.2 及以下版本建议选用 HttpClient。\n3. **拓展方面**：HttpClient 的 API 数量过多，使得我们很难在不破坏兼容性的情况下对它进行升级和扩展，所以，目前 Android 团队在提升和优化 HttpClient 方面的`工作态度并不积极`。\n\nOkHttp 和 Volley 的对比：\n\n1. OkHttp：现代、快速、高效的 Http 客户端，支持 `HTTP/2 以及 SPDY`. Android 4.4 的源码中可以看到 HttpURLConnection 已经替换成 OkHttp 实现了。OkHttp `处理了很多网络疑难杂症`：会从很多常用的连接问题中自动恢复。OkHttp 还处理了代理服务器问题和 SSL 握手失败问题。\n2. Volley：适合进行`数据量不大，但通信频繁的网络操作`；内部分装了异步线程；支持 Get，Post 网络请求和图片下载；可直接在主线程调用服务端并处理返回结果。缺点是：`1).对大文件下载 Volley 的表现非常糟糕；2).只支持 http 请求`。Volley 封装了访问网络的一些操作，底层在 Android 2.3 及以上版本，使用的是 HttpURLConnection，而在 Android 2.2 及以下版本，使用的是 HttpClient. \n\n**问题：OkHttp 源码？**\n\n首先从整体的架构上面看，OkHttp 是基于`责任链设计模式`设计的，责任链的每一个链叫做一个拦截器。OkHttp 的请求是依次通过`重试、桥接、缓存、连接和访问服务器`五个责任链，分别用来：1).根据请求的`错误码`决定是否需要对连接进行重试；2).根据请求信息构建一个 key 用来从 DiskLruCache 中获取缓存，然后根据缓存的响应的信息判断该响应是否可用；3).缓存不可用的时候，使用连接拦截器建立服务器连接；4).最终在最后一个责任链从服务器中拿到请求结果。当从网络当中拿到了数据之后，会回到缓存连接器，然后在这里根据响应的信息和用户的配置决定是否缓存本次请求。除此默认的连接器，我们还可以自定义自己的拦截器。\n\nOkHttp 的网络访问并没有直接使用 HttpUrlConnection 或者 HttpClient，而是直接使用 Socket 建立网络连接，对于流的读写，它使用了第三方的库 `okio`。在拿到一个请求的时候，OkHttp 首先会到连接池中寻找可以复用的连接。这里的连接池是使用双端队列维护的一个列表。当从连接池中获取到一个连接之后就使用它来进行网络访问。\n\n**问题：Volley 实现原理？**\n\n```java\n    RequestQueue queue = Volley.newRequestQueue(this);\n    // 针对不同的请求类型，Volley 提供了不同的 Request\n    queue.add(new StringRequest(Request.Method.POST, \"URL\", new Response.Listener<String>() {\n        @Override\n        public void onResponse(String response) {\n        }\n    }, new Response.ErrorListener() {\n        @Override\n        public void onErrorResponse(VolleyError error) {\n        }\n    }));\n```\n\n底层在 Android 2.3 及以上版本，使用的是 HttpURLConnection，而在 Android 2.2 及以下版本，使用的是 HttpClient. 当创建一个 RequestQueue 的时候会同时创建 4 条线程用于从网络中请求数据，一条缓存线程用来从缓存中获取数据。因此不适用于数据量大、通讯频繁的网络操作，因为会占用网络请求的访问线程。\n\n当调用 `add()` 方法的时候，会先判断是否可以使用缓存，如果可以则将其添加到缓存队列中进行处理。否则将其添加到网络请求队列中，用来从网络中获取数据。在缓存的分发器中，会开启一个无限的循环不断进行工作，它会先从阻塞队列中获取一个请求，然后判断请求是否可用，如果可用的话就将其返回了，否则将请求添加到网络请求队列中进行处理。\n\n网络请求队列与之类似，它也是在 `run()` 方法中启动一个无限循环，然后使用阻塞队列获取请求，拿到请求之后来从网络中获取响应结果。\n\n**问题：网络请求缓存处理，OkHttp 如何处理网络缓存的？**    \n**问题：Http 请求头中都有哪些字段是与缓存相关的？**    \n**问题：缓存到磁盘上的时候，缓存的键是根据哪些来决定的？**\n\nOkHttp 的缓存最终是使用的 `DiskLruCache` 将请求的请求和响应信息存储到磁盘上。当进入到缓存拦截器的时候，首先会先从缓存当中获取请求的请求信息和响应信息。它会从响应信息的头部获取本次请求的缓存信息，比如过期时间之类的，然后判断该响应是否可用。如果可用，或者处于未连网状态等，则将其返回。否则，再从网络当中获取请求的结果。当拿到了请求的结果之后，还会再次回到缓存拦截器。缓存拦截器在拿到了响应之后，再根据响应和请求的信息决定是否将其持久化到磁盘上面。\n\nhttp 请求头中用来控制决定缓存是否可用的信息：\n1. `Expires`：缓存过期时间，用来指定资源到期的时间，是服务器端的具体的时间点；\n2. `Cache-Control`：相对时间，例如 Cache-Control:3600，代表着资源的有效期是 3600 秒，Cache-Control 与 Expires 可以在服务端配置同时启用或者启用任意一个，同时启用的时候Cache-Control 优先级高。\n3. `Last-Modify/If-Modify-Since`：Last-modify 是一个时间标识该资源的最后修改时间。\n4. `ETag/If-None-Match`：一个校验码，ETag 可以保证每一个资源是唯一的，资源变化都会导致ETag 变化。ETag 值的变更则说明资源状态已经被修改。服务器根据浏览器上发送的 If-None-Match 值来判断是否命中缓存。\n\n客户端第一次请求的时候，服务器会返回上述各种 http 信息，第二次请求的时候会根据下面的流程进行处理：\n\n![客户端第二次请求时的情况](res/940884-20180423141951735-912699213.png)\n\n**问题：Retrofit 源码？**\n\nRetrofit 的源码的两个核心地方：\n\n1. 代理设计模式：JDK 动态代理，核心的地方就是调用 `Proxy.newProxyInstance()` 方法，来获取一个代理对象，但是这个方法要求传入的类的类型必须是接口类型，然后通过传入的 `InvocationHandler` 接口对我们定义的 Service 接口的方法进行解析，获取方法的注解等信息，并将其缓存起来。\n2. 适配器设计模式和策略模式：设配器主要两个地方，也是 Retrofit 为人称道的地方，一个是对结果类型的适配，一个是对服务端返回类型的处理。前面的更像是适配器，后面的更像是策略接口。\n     1. 以 RxJava 为例，当代理类的方法被调用的时候会返回一个 Observable. 然后，当我们对 Observable 进行订阅的时候将会调用 `subscribeActual()`，在该方法中根据之前解析的接口方法信息，将它们拼接成一个 OkHttp 的请求，然后使用 OkHttp 从网络中获取数据。\n    2. 当拿到了数据之后就是如何将数据转换成我们期望的类型。这里 Retrofit 也将其解耦了出来。Retrofit 提供了 `Converter` 用作 OkHttp 的响应到我们期望类型的转换器。我们可以通过自己定义来实现自己的转换器，并选择自己满意的 Json 等转换框架。\n\n## 2、网络基础\n\n### 2.1 TCP 和 UDP\n\n**问题：TCP 与 UDP 区别与应用？**    \n**问题：TCP 中 3 次握手和 4 次挥手的过程**：\n\nTCP，`传输控制协议`，`面向连接`，`可靠的`，基于`字节流`的传输层通信协议；    \nUDP，`用户数据报协议`，`面向无连接`，`不可靠`，基于`数据报`的传输层协议。   \n\n应用场景：\n\nTCP 被用在对不能容忍数据丢失的场景中，比如用来发送 Http；    \nUDP 用来可以容忍丢失的场景，比如网络视频流的传输。\n\n具体区别：\n\n1. TCP 协议是`有连接的`，有连接的意思是开始传输实际数据之前 TCP 的客户端和服务器端必须通过三次握手建立连接，会话结束之后也要结束连接。而 UDP 是无连接的。\n2. TCP 协议保证数据`按序发送`，`按序到达`，提供`超时重传`来保证可靠性，但是 UDP `不保证按序到达`，甚至`不保证到达`，只是努力交付，即便是按序发送的序列，也不保证按序送到。\n3. TCP 协议`所需资源多`，TCP 首部需 20 个字节（不算可选项），UDP 首部字段只需8个字节。\n4. TCP 有`流量控制和拥塞控制`，UDP 没有，网络拥堵不会影响发送端的发送速率。\n5. TCP 是`一对一`的连接，而 UDP 则可以支持`一对一、多对多、一对多`的通信。\n6. TCP 面向的是`字节流`的服务，UDP 面向的是`报文`的服务。\n\nTCP 握手过程：\n\n![3 次握手](res/tcp_3_hello.png)\n\n1. 客户端通过 TCP 向服务器发送`SYN 报文段`。它不包含应用层信息，其中的`SYN 标志位为 1`，然后选择一个`初始序号 (client_isn)`，并将其放置在报文段的序号字段中。\n2. 当 SYN 报文段到达服务器之后，服务器为该 TCP 连接分配 TCP 缓存和变量，并向该客户端发送`SYNACK 报文段`。它不包含应用层信息，其中个的`SYN 置为 1`，`确认号字段`被置为`client_isn+1`，最后服务器选择自己的`初始序号 (server_isn)` 放在序号字段中。\n3. 客户端收到 SYNACK 报文段之后，为连接分配缓存和变量，然后向服务器发送另一个报文段，其中将 `server_isn+1` 放在`确认字段`中，并将`SYN 位置为 0`.\n\n**问题：为什么要三次握手？**\n\n三次握手的目的是建立可靠的通信信道，说到通讯，简单来说就是数据的发送与接收，而三次握手最主要的目的就是`双方确认自己与对方的发送与接收是正常的`：\n\n1. 第一次握手：Client 什么都不能确认；Server 确认了对方发送正常。\n2. 第二次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：自己接收正常，对方发送正常。\n3. 第三次握手：Client 确认了：自己发送、接收正常，对方发送、接收正常；Server 确认了：自己发送、接收正常，对方发送接收正常。\n\n![4 次挥手](res/tcp_4_bye.png)\n\n1. 客户端向服务器发送关闭连接报文段，其中 `FIN` 置为 1。\n2. 服务器接收到该报文段之后向发送方会送一个确认字段。\n3. 服务器向客户端发送自己的终止报文段。\n4. 客户端对服务器终止报文段进行确认。\n\n**问题：为什么要四次挥手？**\n\n任何一方都可以在数据传送结束后发出连接释放的通知，待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候，则发出连接释放通知，对方确认后就完全关闭了 TCP 连接。举个例子：A 和 B 打电话，通话即将结束后，A 说“我要挂了”，B 回答 “我知道了”，但是 A 可能还会有要说的话，所以隔一段时间，B 再问 “真的要挂吗”，A 确认之后通话才算结束。\n\n**问题：三次握手建立连接时，发送方再次发送确认的必要性？**\n\n主要是为了防止已失效的连接请求报文段突然又传到了 B，因而产生错误。假定出现一种异常情况，即 A 发出的第一个连接请求报文段并没有丢失，而是在某些网络结点长时间滞留了，一直延迟到连接释放以后的某个时间才到达 B，本来这是一个早已失效的报文段。但 B 收到此失效的连接请求报文段后，就误认为是 A 又发出一次新的连接请求，于是就向 A 发出确认报文段，同意建立连接。假定不采用三次握手，那么只要 B 发出确认，新的连接就建立了，这样一直等待 A 发来数据，B 的许多资源就这样白白浪费了。\n\n### 2.2 Http\n\n#### 2.2.1 Http 协议\n\n1. 全称`超文本传输协议`**`；\n2. HTTP使用`TCP`作为它的支撑协议，TCP 默认使用 `80` 端口；\n3. 它是`无状态的`，就是说它不会记录你之前是否访问过某个对象，它不保存任何关于客户的信息；\n4. 它有两种连接方式，`非持续连接和持续连接`。它们的区别在于，非持续连接发送一个请求获取内容之后，对内容里的链接会再分别发送 TCP 请求获取；持续连接当获取到内容之后，`复用之前的 TCP` 获取相关的内容。后者节省了建立连接的时间，效率更高。\n\n#### 2.2.2 HTTP 请求报文\n\n```http\nGET /somedir/page.jsp HTTP/1.1    部分 1：请求方法-统一资源标识符(URI)-协议/版本\nAccept: text/plain; text/html     部分 2：请求头\nAccept-Language: en-gb\nConnection: keep-Alive\nHost: localhost\nUser-Agent: Mozilla/4.0\nContent-Length: 33\nContent-Type: application/x-www-form-urlencoded\nAccept-Encoding: gzip, deflate    \n    \nlastName=Franks&firstName=Michael 部分 3：实体\n```\n\n1. 请求方法共有`GET、POST、HEAD、PUT 和 DELETE`等，其中 `GET` 大约占 90%；HEAD 类似 GET，但不返回请求对象；PUT 表示上传对象到服务器；DELETE 表示删除服务器上的对象。\n2. `URI` 是相应的 `URI` 的后缀，通常被解释为相对于服务器根目录的路径；\n3. 请求头包含客户端和实体正文的相关信息，各个请求头之间使用 `“换行/回车”符(CRLF)` 隔开；\n4. 请求头和实体之间有一个空行，该空行只有 CRLF 符，对 HTTP 格式非常重要。\n5. `Host` 指明对象主机，它在 Web 代理高速缓存中有作用；\n6. `Connection` 可取的值有 keep-Alive 和 close，分别对应持续连接和非持续连接；\n7. `User-Agent` 指明向服务器发送请求的浏览器。\n\n#### 2.2.3 HTTP 响应报文\n\n```http\nHTTP/1.1 200 OK                         部分 1：协议-状态码-描述\nServer: Microft-IIS/4.0                 部分 2：响应头\nDate: Mon, 5 Jan 2004 12:11：22 GMT\nContent-Type: text/html\nLast-Modified: Mon, 5 Jan 2004 11:11:11 GMT\nContent-Length: 112                     \n\n<html>.....</html>                      部分 3：响应实体段\n```\n\n1. 响应头和响应实体之间使用一个 CRLF 符分隔;\n2. `Last-Modified` 缓存服务器中有作用；\n3. 状态码的五种可能取值：\n\t1. `1xx`：指示信息--表示请求已接收，继续处理\n\t2. `2xx`：成功--表示请求已被成功接收、理解、接受\n\t3. `3xx`：重定向--要完成请求必须进行更进一步的操作\n\t4. `4xx`：客户端错误--请求有语法错误或请求无法实现\n\t5. `5xx`：服务器端错误--服务器未能实现合法的请求\n3. 常见的状态码:\n\t1. `200` OK：请求成功；\n\t2. `301` Moved Permanelty: 请求对象被永久转移；\n    3. `302` 重定向只是暂时的重定向，搜索引擎会抓取新的内容而保留旧的地址，搜索搜索引擎认为新的网址是暂时的。而 301 重定向是永久的，搜索引擎在抓取新的内容的同时也将旧的网址替换为了重定向之后的网址。\n\t4. `400` Bad Request: 请求不被服务器理解；\n\t5. `404` Not Found: 请求的文档不在服务器；\n    6. `503` Service Unavailable：服务器出错的一种返回状态；\n\t7. `505` HTTP Version Not Supperted： 服务器不支持的HTTP协议。\n\n#### 2.2.4 HTTP 1.0 与 2.0 的区别\n\n1. HTTP/2 采用`二进制格式`而非文本格式；\n2. HTTP/2 是完全`多路复用`的，而非有序并阻塞的——只需一个连接即可实现并行（多路复用允许单一的 HTTP/2 连接同时发起多重的请求-响应消息）；\n3. 使用`报头压缩`，HTTP/2 降低了开销（不使用原来的头部的字符串，比如 UserAgent 等，而是从`字典`中获取，这需要在支持 HTTP/2 的浏览器和服务端之间运行）；\n4. HTTP/2 让服务器可以将响应主动`推送`到客户端缓存中（说白了，就是 HTTP2.0 中，浏览器在请求 HTML 页面的时候，服务端会推送 css、js 等其他资源给浏览器，减少网络空闲浪费）。\n\n#### 2.2.5 Http 长连接\n\n在`HTTP/1.0`中默认使用短连接。也就是说，客户端和服务器每进行一次 HTTP 操作，就建立一次连接，任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源（如 JavaScript 文件、图像文件、CSS 文件等），每遇到这样一个 Web 资源，浏览器就会重新建立一个 HTTP 会话。\n\n从`HTTP/1.1`起，默认使用长连接，用以保持连接特性。使用长连接的 HTTP 协议，会在响应头加入这行代码 `Connection:keep-alive`。在使用长连接的情况下，当一个网页打开完成后，客户端和服务器之间用于传输 HTTP 数据的 `TCP 连接不会关闭`，客户端再次访问这个服务器时，会继续使用这一条已经建立的连接。`Keep-Alive 不会永久保持连接，它有一个保持时间`，可以在不同的服务器软件（如 Apache）中设定这个时间。实现长连接`需要客户端和服务端都支持长连接`。\n\n`HTTP 协议的长连接和短连接，实质上是 TCP 协议的长连接和短连接。`\n\n长连接可以省去较多的 TCP 建立和关闭的操作，减少浪费，节约时间。对于频繁请求资源的客户来说，较适用长连接。不过这里存在一个问题，存活功能的探测周期太长，还有就是它只是探测 TCP 连接的存活，属于比较斯文的做法，遇到恶意的连接时，保活功能就不够使了。在长连接的应用场景下，client 端一般不会主动关闭它们之间的连接，Client 与 server 之间的连接如果一直不关闭的话，会存在一个问题，随着客户端连接越来越多，server 早晚有扛不住的时候，这时候 server 端需要采取一些策略，如关闭一些长时间没有读写事件发生的连接，这样可以避免一些恶意连接导致 server 端服务受损；如果条件再允许就可以以客户端机器为颗粒度，限制每个客户端的最大长连接数，这样可以完全避免某个客户端连累后端服务。\n\n### 2.3 Https\n\n**问题：Https 请求慢的解决办法？DNS，携带数据，直接访问 IP**    \n**问题：Http 与 Https 的区别以及如何实现安全性？**     \n**问题：Https 原理？**    \n**问题：Https 相关，如何验证证书的合法性，Https 中哪里用了对称加密，哪里用了非对称加密，对加密算法（如 RSA）等是否有了解？**\n\n#### 2.3.1 Https 连接的过程\n\n![Https 连接的过程](res/bg2014092004.png)\n\nSSL 协议的握手过程共分成 5 各步骤，\n\n1. 第一步，客户端给出`协议版本号`、一个客户端生成的`随机数`，以及客户端支持的`加密方法`；\n2. 第二步，服务器确认双方使用的`加密方法`，并给出`数字证书`、以及一个服务器生成的`随机数`；\n3. 第三步，客户端确认`数字证书`有效，然后生成一个新的`随机数`，并使用数字证书中的`公钥`加密这个随机数，发给服务器。\n4. 第四步，服务器使用自己的`私钥`，获取客户端发来的随机数。\n5. 第五步，客户端和服务器根据约定的`加密方法`，使用前面的`三个随机数`，生成`对话密钥`来加密接下来的整个对话过程。\n\n握手阶段有三点需要注意。\n\n1. 生成对话密钥一共需要`三个随机数`，然后使用这三个随机数来最终确定通话使用的算法；\n2. 握手之后的对话使用对话密钥，服务器的公钥和私钥只用于加密和解密对话密钥，无其他作用；（握手之后开启的正式对话使用的是`对称加密`，即双方都能通过密钥进行解密；握手的过程中，协商最终使用哪种加密算法通话的时候是`非对称加密`，即私钥加密后的密文，只要是公钥，都可以解密，但是公钥加密后的密文，只有私钥可以解密。这样，服务端发送给客户端的消息不安全，但是客户端回复给服务端的消息是安全的。因为最后还要发送一个随机数用来确定最终的算法，所以这个过程安全就保证了最终的通话密钥是安全的。）\n3. 服务器公钥放在服务器的数字证书之中。\n\n然而直接使用非对称加密的过程本身也`不安全`，会有中间人`篡改公钥`的可能性，所以客户端与服务器不直接使用公钥，而是使用`数字证书签发机构颁发的证书`来保证非对称加密过程本身的安全。`第三方使用自己的私钥对公钥进行加密`，生成一个`证书`。然后`客户端从该证书中读取出服务器的公钥`。那么证书的合法性如何确认呢？我们可以使用浏览器或操作系统中维护的权威的第三方颁发机构的公钥，验证证书的编号是否正确。然后再使用第三方结构的公钥解密出我们服务器的公钥即可。\n\n#### 2.3.2 HTTPS 与 HTTP 的一些区别\n\n1. HTTPS 协议需要到 CA 申请证书，一般免费证书很少，需要`交费`。\n2. HTTP 协议运行在 `TCP` 之上，所有传输的内容都是`明文`，HTTPS 运行在 `SSL/TLS` 之上，所有传输的内容都经过`加密`的。\n3. HTTP 和 HTTPS 使用的是完全不同的连接方式，用的端口也不一样，前者是 `80`，后者是 `443`。\n4. HTTPS 可以有效的`防止运营商劫持`，解决了防劫持的一个大问题。\n\n### 2.4 其他网络相关\n\n**问题：描述一次网络请求的流程?**\n\n![描述一次网络请求的流程](res/687472.webp)\n\n浏览器输入域名之后，首先通过 DNS 查找该域名对应的 IP 地址。查找的过程会使用多级的缓存，包括浏览器、路由器和 DNS 的缓存。查找的 IP 地址之后，客户端向 web 服务器发送一个 HTTP 连接请求。服务器收到客户端的请求之后处理请求，并返回处理结果。客户端收到服务端返回的结果后将视图呈现给用户。\n\n**问题：WebSocket 相关以及与 Socket 的区别**\n**问题：谈谈你对 WebSocket 的理解**\n**问题：WebSocket 与 socket 的区别**\n\nWebSocket 是 HTML5 提供的一种在`单个 TCP 连接`上进行`全双工`通讯的协议，允许服务端主动向客户端推送数据。浏览器和服务器只需要完成`一次握手`，两者之间就直接可以创建持久性的连接，并进行`双向数据传输`。WebSocket 的请求和响应`报文的结构与 Http 相似`。相比于 ajax 这种通过不断`轮询`的方式来从服务端获取请求的方式，它通过类似于`推送`的方式通知客户端，可以节省更多的网络资源。\n\n跟 Socket 的区别：Socket 其实`并非一个协议`，是应用层与 TCP/IP 协议族通信的中间软件抽象层，它是一组接口。当两台主机通信时，让 Socket 去组织数据，以符合指定的协议。TCP 连接则更依靠于底层的 IP 协议，IP 协议的连接则依赖于链路层等更低层次。WebSocket 则是一个典型的`应用层协议`。总的来说：Socket 是`传输控制层协议`，WebSocket 是`应用层协议`。\n\n## 参考：\n\n1. [《Volley使用及其原理解析》](https://www.jianshu.com/p/fbbf2b1dfa46)\n2. [《也许，这样理解HTTPS更容易》](http://blog.jobbole.com/110354/)\n"
  },
  {
    "path": "笔试面试/Android高级面试_8_热修补插件化等.md",
    "content": "# Android 高级面试：插件化和热修复相关\n\n## 1、dex 和 class 文件结构\n\nclass 是 JVM 可以执行的文件类型，由 javac 编译生成；dex 是 DVM 执行的文件类型，由 dx 编译生成。\n\nclass 文件结构的特点：\n\n1. 是一种 8 位二进制字节流文件；\n2. 各个数据按顺序紧密的排列，无间隙；\n3. 每个类或者借口都单独占据一个 class 文件；\n\nclass 文件的文件结构：\n\n```\n1. magic                           加密字段\n2. minor_version                   支持最低版本的jdk\n3. major_version                   编译使用的jdk版本\n4. constant_pool_count             常量池的数量\n5. cp_info constant_pool           常量池的结构体，数量不定\n6. access_flags                    访问级别\n7. this_class                      当前类\n8. super_class                     父类\n9. interfaces_count                类实现接口的数量\n10. fields_count                   类成员变量的数量\n11. methods_count                  类方法的数量\n12. method_info methods            类方法的结构体\n13. attributes_count               类属性的数量\n14. attribute_info attributes      类属性的结构体\n```\n\ndex 文件的结构的特点：\n\n```\n1. 是一种 8 位二进制字节流文件；\n2. 各个数据按顺序紧密的排列，无间隙；\n3. 一般情况下，整个应用所有 java 源文件都放在一个 dex 文件中。\n```\n\ndex 的文件结构分成 3 个区：\n\n```\n1. 第一个区是 header，包括：\n    1. header_item dex_header 这个结构体，\n2. 第二个区是索引区，包括：\n    1. string_id_list dex_string_ids（字符串索引\n    2. type_id_list dex_type_ids（类型索引）\n    3. proto_id_list dex_proto_ids（方法原型索引）\n    4. field_id_list dex_field_ids（域索引）\n    5. method_id_list dex_method_ids（方法索引）\n3. 第三个区是数据区，包括：\n    1. class_def_item_list dex_class_defs（类的定义）\n    2. data\n    3. link_data(so)\n```\n\n两者的主要区别：\n\n1. class 中只包含了一个 java 文件的信息，dex 中包含了多个 java 文件的的信息；\n2. dex 中包含了很多类的信息，它会把类的信息进行拆分，然后把拆分后的信息分配到指定的索引区域中。比如方法索引区域就包含了所有类的方法的索引。\n\n## 2、加固的原理\n\n加固的过程分成几个步骤：\n\n1. 要加固的 APK + 壳程序 dex 合成新的 dex；\n2. 然后用新合成的 dex 替换克 apk 中的 dex 得到新的 APK.\n\n第一步的时候可以对 APK 进行加密，然后在运行时对 APK 进行解密。合成新 APK 的本质过程是对 dex 进行拼接，将 APK 文件附加到 dex 文件后面。因为此时 dex 的信息已经发生了变化，所以需要对 dex 的文件头进行修改，包括魔数、检验码和 SHA-1 签名的修改。本质上拼接的过程可以通过读取二进制数组，然后通过数组拷贝将 APK 附加到 dex 末尾。\n\n第二部就是一个打包的过程。不过这个过程会修改 Manifest 文件，将程序中的 Application 替换掉。替换后的 Application 会在程序启动的时候将我们的 APK 加载进来。下面是 360 加固之后的 Manifest 文件：\n\n![加固的原理](res/QQ图片20190425213636.jpg)\n\n加固的过程有些类似于插件化的流程，也是将 APK 解压到磁盘之后，通过反射替换掉 AssertsManager，并将其指向我们解压之后的资源的路径。\n\n360 加固的核心算法在 native 层实现，里面针对不同的平台提供了各种 so 库，运行时在 java 层判断平台版本，调用 so 库。\n\n## 3、热修复的原理\n\n根据修复的类型分成几种：类的修复，资源修复和 so 修复。\n\n类的修复：\n\n## 4、插件化的原理\n\n\n"
  },
  {
    "path": "笔试面试/Android高级面试_9_网络基础.md",
    "content": "# Android 高级面试-5：网络基础\n\n\n\n"
  },
  {
    "path": "笔试面试/README.md",
    "content": "# Android高级工程师学习清单\n\n## 1、Java语言\n\n*重点：Java容器、Java多线程编程、Java并发编程*\n\n### 1.1 Java容器\n\n- [ ] SpareArray原理\n- [x] LRUCache原理：[LruCache源码分析](https://juejin.im/post/5afc29caf265da0b7f44be28)\n- [ ] HashMap原理\n- [ ] ConcurrentHashMap原理\n- [x] ThreadLocal原理：[ThreadLocal的使用及其源码实现](https://juejin.im/post/5b44cd7c6fb9a04f980cb065)\n\n### 1.2 Java多线程和并发编程\n\n- [ ] CAS介绍\n- [ ] volatile用法\n- [ ] 线程间操作 List\n\n### 1.3 其他Java知识点\n\n- [ ] OSGI\n- [ ] synchronized与Lock的区别\n- [ ] 抽象类和接口的区别\n- [ ] 集合 Set实现 Hash 怎么防止碰撞 \n- [ ] 死锁，线程死锁的4个条件？\n- [ ] 进程状态\n- [ ] 并发集合了解哪些\n\n\n- [ ] 开启线程的三种方式,run()和start()方法区别\n- [ ] Java线程池\n- [ ] 多线程（关于AsyncTask缺陷引发的思考）\n- [ ] java注解 \n- [ ] static synchronized 方法的多线程访问和作用，同一个类里面两个synchronized方法，两个线程同时访问的问题\n- [ ] 对Java中String的了解\n- [ ] ArrayList与LinkedList区别\n- [ ] string to integer\n- [ ] volatile的原理\n- [ ] synchronize的原理\n- [ ] lock原理\n- [ ] AsyncTask机制，如何取消AsyncTask\n- [ ] 手写生产者/消费者模式\n- [ ] 如何实现线程同步？\n- [ ] arraylist 与 linkedlist 异同？\n- [ ] object类的equal 和hashcode 方法重写，为什么？\n- [ ] hashmap如何put数据（从hashmap源码角度讲解）？\n- [ ] String 为什么要设计成不可变的？\n- [ ] List 和 Map 的实现方式以及存储方式。\n- [ ] 静态内部类的设计意图\n- [ ] 线程如何关闭，以及如何防止线程的内存泄漏\n- [ ] NIO\n- [ ] List,Set,Map的区别\n- [ ] HashSet与HashMap怎么判断集合元素重复\n- [ ] wait/notify\n- [ ] 多线程：怎么用、有什么问题要注意；Android线程有没有上限，然后提到线程池的上限\n- [ ] ReentrantLock 、synchronized和volatile（n面）\n- [ ] Java中同步使用的关键字，死锁\n- [ ] HashMap的实现，与HashSet的区别\n- [ ] 死锁的概念，怎么避免死锁\n- [ ] 内部类和静态内部类和匿名内部类，以及项目中的应用\n- [ ] ReentrantLock的内部实现\n- [ ] String buffer 与string builder 的区别？\n- [ ] 集合的接口和具体实现类，介绍\n- [ ] TreeMap具体实现\n- [ ] synchronized与ReentrantLock\n\n## JVM\n\n- [ ] 谈谈类加载器classloader\n- [ ] 动态加载\n- [ ] GC回收策略\n- [ ] Java中对象的生命周期\n- [ ] 类加载机制，双亲委派模型\n- [ ] JVM 内存区域 开线程影响哪块内存\n- [ ] 垃圾收集机制 对象创建，新生代与老年代\n- [ ] JVM内存模型\n- [ ] 垃圾回收机制与调用System.gc()区别\n- [ ] 软引用、弱引用区别\n- [ ] 垃圾回收\n- [ ] java四中引用\n- [ ] 垃圾收集器\n- [ ] 强引用置为null，会不会被回收？\n- [ ] Java中内存区域与垃圾回收机制\n- [ ] OOM，内存泄漏\n- [ ] JVM内存模型，内存区域\n\n## Android系统\n\n- [ ] 动态布局\n- [ ] 热修复,插件化\n- [ ] 性能优化,怎么保证应用启动不卡顿\n- [ ] 怎么去除重复代码\n- [ ] SP是进程同步的吗?有什么方法做到同步\n- [ ] 介绍下SurfView\n- [ ] 图片加载原理\n- [ ] 模块化实现（好处，原因）\n- [ ] 视频加密传输\n- [ ] 统计启动时长,标准\n- [ ] 如何保持应用的稳定性\n- [ ] BroadcastReceiver，LocalBroadcastReceiver 区别\n- [ ] Bundle 机制\n- [ ] Handler 机制\n- [ ] Binder相关？\n- [ ] Android事件分发机制\n- [ ] App启动流程，从点击桌面开始\n- [ ] 画出 Android 的大体架构图\n- [ ] 描述清点击 Android Studio 的 build 按钮后发生了什么\n- [ ] 大体说清一个应用程序安装到手机上时发生了什么\n- [ ] 对 Dalvik、ART 虚拟机有基本的了解\n- [ ] Android 上的 Inter-Process-Communication 跨进程通信时如何工作的\n- [ ] App 是如何沙箱化，为什么要这么做\n- [ ] 权限管理系统（底层的权限是如何进行 grant 的）\n- [ ] 进程和 Application 的生命周期\n- [ ] 系统启动流程 Zygote进程 –> SystemServer进程 –> 各种系统服务 –> 应用进程\n- [ ] recycleview listview 的区别,性能\n- [ ] 消息机制\n- [ ] 进程调度\n- [ ] 线程和进程的区别？\n- [ ] 动态权限适配方案，权限组的概念\n- [ ] 图片加载库相关，bitmap如何处理大图，如一张30M的大图，如何预防OOM\n- [ ] 进程保活\n- [ ] 广播（动态注册和静态注册区别，有序广播和标准广播）\n- [ ] listview图片加载错乱的原理和解决方案\n- [ ] service生命周期\n- [ ] handler实现机制（很多细节需要关注：如线程如何建立和退出消息循环等等）\n- [ ] 数据库数据迁移问题\n- [ ] 是否熟悉Android jni开发，jni如何调用java层代码\n- [ ] 进程间通信的方式\n- [ ] 计算一个view的嵌套层级\n- [ ] 项目组件化的理解\n- [ ] Android系统为什么会设计ContentProvider，进程共享和线程安全问题\n- [ ] Android相关优化（如内存优化、网络优化、布局优化、电量优化、业务优化） \n- [ ] EventBus实现原理\n- [ ] 四大组件\n- [ ] Android中数据存储方式\n- [ ] ActicityThread相关？\n- [ ] Android中进程内存的分配，能不能自己分配定额内存\n- [ ] 序列化，Android为什么引入Parcelable\n- [ ] 有没有尝试简化Parcelable的使用\n- [ ] ViewPager使用细节，如何设置成每次只初始化当前的Fragment，其他的不初始化\n- [ ] ListView重用的是什么\n- [ ] 进程间通信的机制\n- [ ] AIDL机制\n- [ ] 应用安装过程\n- [ ] 简述IPC？\n- [ ] fragment之间传递数据的方式？\n- [ ] OOM的可能原因？\n- [ ] 为什么要有线程，而不是仅仅用进程？\n- [ ] 内存泄漏的可能原因？\n- [ ] 用IDE如何分析内存泄漏？\n- [ ] 触摸事件的分发？\n- [ ] 简述Activity启动全部过程？\n- [ ] 性能优化如何分析systrace？\n- [ ] 广播的分类？\n- [ ] 点击事件被拦截，但是相传到下面的view，如何操作？\n- [ ] 如何保证多线程读写文件的安全？\n- [ ] Activity启动模式\n- [ ] 广播的使用方式，场景\n- [ ] App中唤醒其他进程的实现方式\n- [ ] Android中开启摄像头的主要步骤\n- [ ] Activity生命周期\n- [ ] AlertDialog,popupWindow,Activity区别\n- [ ] fragment 各种情况下的生命周期\n- [ ] Activity 上有 Dialog 的时候按 home 键时的生命周期\n- [ ] 横竖屏切换的时候，Activity 各种情况下的生命周期\n- [ ] Application 和 Activity 的 context 对象的区别\n- [ ] 序列化的作用，以及 Android 两种序列化的区别。\n- [ ] ANR怎么分析解决\n- [ ] LinearLayout、RelativeLayout、FrameLayout的特性、使用场景\n- [ ] 如何实现Fragment的滑动\n- [ ] AndroidManifest的作用与理解\n- [ ] Jni 用过么？\n- [ ] 多进程场景遇见过么？\n- [ ] 关于handler，在任何地方new handler都是什么线程下\n- [ ] sqlite升级，增加字段的语句\n- [ ] bitmap recycler 相关\n- [ ] Activity与Fragment之间生命周期比较\n- [ ] 广播的使用场景\n- [ ] Bitmap 使用时候注意什么？\n- [ ] Oom 是否可以try catch ？\n- [ ] 内存泄露如何产生？\n- [ ] 适配器模式，装饰者模式，外观模式的异同？\n- [ ] ANR 如何产生？\n- [ ] 如何保证线程安全？\n- [ ] 事件传递机制的介绍\n- [ ] handler发消息给子线程，looper怎么启动\n- [ ] View事件传递\n- [ ] activity栈\n- [ ] 封装view的时候怎么知道view的大小\n- [ ] 怎么启动service，service和activity怎么进行数据交互\n- [ ] 下拉状态栏是不是影响activity的生命周期，如果在onStop的时候做了网络请求，onResume的时候怎么恢复\n- [ ] view渲染\n- [ ] singleTask启动模式\n- [ ] 消息机制实现\n- [ ] App启动崩溃异常捕捉\n- [ ] ListView的优化\n- [ ] Android进程分类\n- [ ] 前台切换到后台，然后再回到前台，Activity生命周期回调方法。弹出Dialog，生命值周期回调方法。\n- [ ] RecycleView的使用，原理，RecycleView优化\n- [ ] ANR的原因\n- [ ] Service的开启方式\n- [ ] Activity与Service通信的方式\n- [ ] Activity之间的通信方式\n\n## 网络\n\n- [ ] Https请求慢的解决办法，DNS，携带数据，直接访问IP\n- [ ] TCP/UDP的区别\n- [ ] https相关，如何验证证书的合法性，https中哪里用了对称加密，哪里用了非对称加密，对加密算法（如RSA）等是否有了解\n- [ ] TCP与UDP区别与应用（三次握手和四次挥手）涉及到部分细节（如client如何确定自己发送的消息被server收到） HTTP相关 提到过Websocket 问了WebSocket相关以及与socket的区别\n- [ ] 多线程断点续传原理\n\n## 算法\n\n- [ ] 排序，快速排序的实现\n- [ ] 树：B树、B+树的介绍\n- [ ] 图：有向无环图的解释\n- [ ] 二叉树 深度遍历与广度遍历\n- [ ] 常用数据结构简介\n- [ ] 判断环（猜测应该是链表环）\n- [ ] 排序，堆排序实现\n- [ ] 链表反转\n- [ ] x个苹果，一天只能吃一个、两个、或者三个，问多少天可以吃完\n- [ ] 堆排序过程，时间复杂度，空间复杂度\n- [ ] 快速排序的时间复杂度，空间复杂度\n- [ ] 翻转一个单项链表\n- [ ] 两个不重复的数组集合中，求共同的元素\n- [ ] 上一问扩展，海量数据，内存中放不下，怎么求出\n- [ ] 合并多个单有序链表（假设都是递增的）\n- [ ] 算法判断单链表成环与否？\n- [ ] 二叉树，给出根节点和目标节点，找出从根节点到目标节点的路径\n- [ ] 一个无序，不重复数组，输出N个元素，使得N个元素的和相加为M，给出时间复杂度、空间复杂度。手写算法\n- [ ] 数据结构中堆的概念，堆排序\n\n## 第三方库\n\n- [ ] 网络请求缓存处理，okhttp如何处理网络缓存的\n- [ ] RxJava的作用，与平时使用的异步操作来比，优势\n- [ ] RxJava的作用，优缺点\n- [ ] Glide源码？\n- [ ] okhttp源码？\n- [ ] RxJava简介及其源码解读？\n- [ ] glide 使用什么缓存？\n- [ ] Glide 内存缓存如何控制大小？\n- [ ] 用到的一些开源框架，介绍一个看过源码的，内部实现过程\n- [ ] EventBus作用，实现方式，代替EventBus的方式\n\n## 架构和设计模式\n\n- [ ] 设计模式相关（例如Android中哪里使用了观察者模式，单例模式相关）\n- [ ] MVP模式\n- [ ] Java设计模式，观察者模式\n- [ ] 模式MVP，MVC介绍\n\n"
  },
  {
    "path": "笔试面试/java/ArrayList、LinkedList、Vector.md",
    "content": "# 比较ArrayList、LinkedList、Vector\n\n它们之间的比较还是比较简单的：\n\n1. **同步性能**：ArrayList和LinkedList是非同步的，而Vector是同步的。也就是Vector在所有的操作方法上面都加了sychronized来进行修饰。这在多线程环境中可以保证被sychronized修饰的方法的安全性，但是在非多线程环境中就会成为一种没有必要的开销（加锁和释放锁是需要消耗一些性能的）。另外，要注意的是加了sychronized充其量只能保证指定的方法本身的原子性，当多个方法组合起来使用的时候，就不一定是原子的了。所以，即使想要在多线程中使用，Vector也不是个好的选择。因此，Vector被抛弃了。\n2. **数据结构**:ArrayList和Vector是基于数组的，而LinkedList是基于链表的。所以，它们之间的区别也就是链表数据结构和数组数据结构的区别（比如，链表查找的效率是O(n)，数组的查找效率是O(1)等等）。在ArrayList和Vector的底层实现中，本质上都是使用了System.arraycopy()方法来实现数组的拷贝。\n3. **应用场景**：Vector基本不要用。如果应用场景中插入和删除操作比较频繁，就使用LinkedList。如果查询操作比较频繁就使用ArrayList. 另外，可以将LinkedList当成队列来使用。\n\n------\n\n### 更多\n\n关于Java容器更多的内容参考：\n[https://github.com/Shouheng88/Java-Programming/blob/master/Java%E8%AF%AD%E8%A8%80/%E5%AE%B9%E5%99%A8.md](https://github.com/Shouheng88/Java-Programming/blob/master/Java%E8%AF%AD%E8%A8%80/%E5%AE%B9%E5%99%A8.md)\n\n或者\n\n[https://juejin.im/post/5a12d4c95188255ea95b908e](https://juejin.im/post/5a12d4c95188255ea95b908e)\n"
  },
  {
    "path": "笔试面试/java/Collection包结构，与Collections的区别.md",
    "content": "Collection是集合类的上级接口，子接口主要有Set 和List、Map。 \nCollections是针对集合类的一个帮助类，提供了操作集合的工具方法：一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。"
  },
  {
    "path": "笔试面试/java/HashMap和ConcurrentHashMap的区别，HashMap的底层源码.md",
    "content": "\n关于HashMap的底层原理可以参考：[https://github.com/Shouheng88/Java-Programming/blob/master/Java%E8%AF%AD%E8%A8%80/%E5%AE%B9%E5%99%A8.md](https://github.com/Shouheng88/Java-Programming/blob/master/Java%E8%AF%AD%E8%A8%80/%E5%AE%B9%E5%99%A8.md)\n\n\n\n\n\n"
  },
  {
    "path": "笔试面试/java/HashMap和HashTable的区别.md",
    "content": "1. HashMap可以接受null键值和值，而Hashtable则不能。\n2. Hashtable是线程安全的，通过synchronized实现线程同步。而HashMap是非线程安全的，但是速度比Hashtable快。\n3. HashTable不会调整链表的结构；而在HashMap中，当链表的长度大于等于7的时候就会调整为红黑树结构。\n4. 它直接采用除留余数法计算指定哈希码的索引，所以在这方面比HashMap也会性能略差一些。\n\n\n"
  },
  {
    "path": "笔试面试/java/Hashcode的作用.md",
    "content": "hashCode()方法用来根据对象的字段信息生产对象的哈希码，主要用在比如HashMap和HashTable这样的散列表中。当对象不同的时候返回不同的哈希码有助于增强散列表的性能。\n\n"
  },
  {
    "path": "笔试面试/java/Java1.7与1.8新特性.md",
    "content": "# Java1.7与1.8新特性\n\n"
  },
  {
    "path": "笔试面试/java/Java的四种引用，强弱软虚，用到的场景.md",
    "content": "## Java的四种引用，强软弱虚，用到的场景\n\nJava在JDK1.2之后引入了强引用、软引用、弱引用和虚引用4种概念，来表示引用的状态。四种引用强度依次减弱。\n\n### 1、强引用（StrongReference）\n\n强引用不会被GC回收，当使用\n\n    Object obj = new Object();\n\n声明一个对象的时候，这里的obj引用便是一个强引用。如果一个对象是强引用，那垃圾回收器绝不会回收它。当内存空 间不足，Java虚拟机宁愿抛出OutOfMemoryError错误，使程序异常终止，也不会靠随意回收具有强引用的对象来解决内存不足问题。\n\n如果不使用时，要通过如下方式来弱化引用，如下：\n\n    o = null; // 帮助垃圾收集器回收此对象\n\n### 2、软引用（SoftReference）\n\n如果一个对象只具有软引用，则内存空间足够，垃圾回收器就不会回收它；如果内存空间不足了，就会回收这些对象的内存。只要垃圾回收器没有回收它，该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。\n\n    String str = new String(\"abc\");                               // 强引用\n    SoftReference<String> softRef=new SoftReference<String>(str); // 软引用\n\n### 3、弱引用（WeakReference）\n\n弱引用与软引用的区别在于：只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中，**一旦发现了只具有弱引用的对象，不管当前内存空间足够与否，都会回收它的内存**。不过，由于垃圾回收器是一个优先级很低的线程，因此不一定会很快发现那些只具有弱引用的对象。 \n\n    String str = new String(\"abc\");    \n    WeakReference<String> abcWeakRef = new WeakReference<String>(str);\n\n用法，比如Java API中的WeakHashMap，其中的Entry，即哈希表中的元素，的定义方式是：\n\n    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {}\n \n### 4、虚引用（PhantomReference）\n\n“虚引用”顾名思义，就是形同虚设，虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它就和没有任何引用一样，**在任何时候都可能被垃圾回收器回收**。\n\n-----\n\n### 更多\n\n更多关于JVM的内容参考：[https://github.com/Shouheng88/Java-Programming/tree/master/JVM](https://github.com/Shouheng88/Java-Programming/tree/master/JVM)\n\n"
  },
  {
    "path": "笔试面试/java/Map、Set、List、Queue、Stack的特点与用法.md",
    "content": "## Map、Set、List、Queue、Stack的特点与用法\n\nMap与其他的几个不同。Map是基于键值对的，键Key是唯一不能重复的，一个键对应一个值，值可以重复。对于Java中的Map，它有几个实现，如HashMap和TreeMap等。可以根据Key到指定的Map中找指定的Value. \n\n剩下的几个，都是普通的列表。Set列表中的元素是不允许重复的，在Java中的Set都是借助于Map来实现的，即将要添加到Set中的元素放在Map的Key上面，而Value设置为一个默认的对象。\n\nList是一个列表，它的实现方式通常有基于数组的，如ArrayList；和基于链表的，如LinkedList. \n\nQueue是基于队列的概念，而Stack是基于栈的概念。\n\n"
  },
  {
    "path": "笔试面试/java/Object有哪些公用方法.md",
    "content": "## Object有哪些公用方法\n\n答案 7 个：getClass() hashCode() equals() toString() notify() notifyAll() wait()\n\n    public final native Class<?> getClass();\n\n    public native int hashCode();\n\n    public boolean equals(Object obj) {\n        return (this == obj);\n    }\n\n    public String toString() {\n        return getClass().getName() + \"@\" + Integer.toHexString(hashCode());\n    }\n\n    public final native void notify();\n\n    public final native void notifyAll();\n\n    public final native void wait(long timeout) throws InterruptedException;\n\n    public final void wait(long timeout, int nanos) throws InterruptedException {/..../}\n\n    public final void wait() throws InterruptedException {\n        wait(0);\n    }\n"
  },
  {
    "path": "笔试面试/java/Override和Overload的含义去区别.md",
    "content": "## 1.Override 特点   \n\n1. 覆盖的方法的标志必须要和被覆盖的方法的标志完全匹配，才能达到覆盖的效果；   \n2. 覆盖的方法的返回值必须和被覆盖的方法的返回一致；   \n3. 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致，或者是其子类； \n4. 方法被定义为final不能被重写。  \n5. 对于继承来说，如果某一方法在父类中是访问权限是private，那么就不能在子类对其进行重写覆盖，如果定义的话，也只是定义了一个新方法，而不会达到重写覆盖的效果。（通常存在于父类和子类之间。） \n\n## 2.Overload 特点   \n\n1. 在使用重载时只能通过不同的参数样式。例如，不同的参数类型，不同的参数个数，不同的参数顺序（当然，同一方法内的几个参数类型必须不一样，例如可以是fun(int, float)， 但是不能为fun(int, int)）；   \n2. 不能通过访问权限、返回类型、抛出的异常进行重载；   \n3. 方法的异常类型和数目不会对重载造成影响；   \n4. 重载事件通常发生在同一个类中，不同方法之间的现象。 \n5. 存在于同一类中，但是只有虚方法和抽象方法才能被覆写。 "
  },
  {
    "path": "笔试面试/java/Static class 与 non static class的区别.md",
    "content": "### static class\n\n1. 用static修饰的是内部类，此时这个内部类变为静态内部类；对测试有用；\n2. 内部静态类不需要有指向外部类的引用；\n3. 静态类只能访问外部类的静态成员，不能访问外部类的非静态成员\n\n### non static class\n\n1. 非静态内部类需要持有对外部类的引用；\n2. 非静态内部类能够访问外部类的静态和非静态成员；\n3. 一个非静态内部类不能脱离外部类实体被创建；\n4. 一个非静态内部类可以访问外部类的数据和方法；"
  },
  {
    "path": "笔试面试/java/String、StringBuffer与StringBuilder的区别.md",
    "content": "## String、StringBuffer与StringBuilder的区别\n\nString类型和StringBuilder及StringBuffer类型的主要性能区别其实在于String是不可变的对象, 因此在每次对String类型进行改变的时候其实都等同于生成了一个新的String对象，然后将指针指向新的String对象，所以经常改变内容的字符串最好不要用String，因为每次生成对象都会对系统性能产生影响，特别当内存中无引用对象多了以后，JVM的GC就会开始工作，那速度是一定会相当慢的。\n\nStringBuilder和StringBuffer与String不同，它内部维护了一个字符串数组`char[] value;`。在每次调用append()方法的时候会进行如下处理：\n\n    public AbstractStringBuilder append(char c) {\n        ensureCapacityInternal(count + 1);\n        value[count++] = c;\n        return this;\n    }\n这里的ensureCapacityInternal()方法是先保证数组的长度满足要求，如果不足以满足要求就对数组进行扩容。然后，将指定的值添加到数组中。最后，当全部的值，赋值完毕之后，可以调用toString()方法:\n\n    public String toString() {\n        return new String(value, 0, count);\n    }\n将拼接好的字符串返回过去。我们可以看到实际上是在该方法内部调用了String的构造方法构建String之后返回。\n\nStringBuilder和StringBuffer不同之处就在于StringBuffer的全部方法都使用了sychronized进行修饰。"
  },
  {
    "path": "笔试面试/java/Switch能否用string做参数.md",
    "content": "在jdk 7之前，switch 只能支持 byte、short、char、int 这几个基本数据类型和其对应的封装类型。switch后面的括号里面只能放int类型的值，但由于byte，short，char类型，它们会自动转换为int类型（精精度小的向大的转化），所以它们也支持。\n\n注意，对于精度比int大的类型，比如long、float，doulble，不会自动转换为int，如果想使用，就必须强转为int，如(int)float;\n\njdk1.7后，整形，枚举类型，boolean，字符串都可以。\n\n其实，jdk1.7并没有新的指令来处理switch string，而是通过调用switch中string.hashCode,将string转换为int从而进行判断。"
  },
  {
    "path": "笔试面试/java/TreeMap、HashMap、LindedHashMap.md",
    "content": "## TreeMap、HashMap、LindedHashMap\n\n通常在开发的过程中使用HashMap比较多，在Map中在Map中插入、删除和定位元素，HashMap是最好的选择。但如果您要按自然顺序或自定义顺序遍历键，那么TreeMap会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列。\n\n### 关于HashMap\n\n可以参考：[https://juejin.im/post/5a12d4c95188255ea95b908e](https://juejin.im/post/5a12d4c95188255ea95b908e)\n\n### 关于TreeMap\n\nTreeMap是支持排序的，它内部使用的数据结构是红黑树，所以在这种Map中实现排序就容易得多。下面是TreeMap的put方法，它根据传入的结点的键进行比较来排序。我们只需要关注两个地方就好了：\n\n    public V put(K key, V value) {\n        Entry<K,V> t = root;\n        if (t == null) {\n            compare(key, key); // type (and possibly null) check\n\n            root = new Entry<>(key, value, null);\n            size = 1;\n            modCount++;\n            return null;\n        }\n        int cmp;\n        Entry<K,V> parent;\n        // 在这里判断有没有为该Map设置比较器，如果设置了比较器，就使用比较器进行比较\n        Comparator<? super K> cpr = comparator;\n        if (cpr != null) {\n            do {\n                parent = t;\n                // 这里和下面的一样，其实就是使用比较器比较的结果决定将结点插入到左边还是右边\n                cmp = cpr.compare(key, t.key);\n                if (cmp < 0)\n                    t = t.left;\n                else if (cmp > 0)\n                    t = t.right;\n                else\n                    return t.setValue(value);\n            } while (t != null);\n        } else {\n            if (key == null)\n                throw new NullPointerException();\n            @SuppressWarnings(\"unchecked\")\n                Comparable<? super K> k = (Comparable<? super K>) key;\n            do {\n                parent = t;\n                cmp = k.compareTo(t.key);\n                if (cmp < 0)\n                    t = t.left;\n                else if (cmp > 0)\n                    t = t.right;\n                else\n                    return t.setValue(value);\n            } while (t != null);\n        }\n        Entry<K,V> e = new Entry<>(key, value, parent);\n        if (cmp < 0)\n            parent.left = e;\n        else\n            parent.right = e;\n        fixAfterInsertion(e);\n        size++;\n        modCount++;\n        return null;\n    }\n\n### 关于LinkedHashMap\n\n下面是LinkedHashMap的定义：\n\n    public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {}\n\n所以，它继承了HashMap，其内部的许多方法都是直接使用了HashMap中的方法。因为我们知道每次当我们使用HashMap.put()方法向HashMap中插入值的时候，都会对要插入的键值对使用\n\n    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {\n        LinkedHashMap.Entry<K,V> p =\n            new LinkedHashMap.Entry<K,V>(hash, key, value, e);\n        linkNodeLast(p);\n        return p;\n    }\n\n\n方法来生成一个新的结点。所以在LinkedHashMap中就在这个方法上面做文章。在LinkedHashMap中定义了如下两个变量：\n\n    transient LinkedHashMap.Entry<K,V> head;\n    transient LinkedHashMap.Entry<K,V> tail;\n\n也就是说LinkedHashMap实际用的是双向链表的结构。在newNode()方法中调用了linkNodeLast()方法：\n\n    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {\n        LinkedHashMap.Entry<K,V> last = tail;\n        tail = p;\n        if (last == null)\n            head = p;\n        else {\n            p.before = last;\n            last.after = p;\n        }\n    }\n\n从上面可以看出，它是将指定的结点添加到上面的链表中，这样，整个数据结构既维护了一份数据在HashMap中，又维护了一份在LinkedHashMap中。所以，这个类具有HashMap所不具有的属性——双向链表。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现. 比如应用场景：购物车等需要顺序的。\n"
  },
  {
    "path": "笔试面试/java/equals与==的区别.md",
    "content": "对这个问题，如果要回答的全面的话，我们应该这么讲：\n\n1. 当参与==比较的两个元素中有一个是值类型的，那么就按照值类型来比较。而引用类型按照值来比较的时候使用的是它们的hashCode的返回结果。只有当参与比较的两个元素都是引用类型的，那么才按照引用类型来比较，即比较它们的hashCode的返回结果。\n2. 当使用equals方法进行比较的时候，实际的比较结果取决于equals方法的具体实现，在Object的默认实现中，是使用==来实现的。也就是说使用了按引用来比较的方式。不过，比如Integer和String等，它们在自己的类中又实现了该方法，而它们实现该方法的时候是按照值来进行比较的。\n3. 另外就是关于覆写equals和hashCode方法的问题，覆写它们要遵循一定的原则，不过这些工作完全可以由IDEA代劳。\n\n    public static void main(String...args) {\n        Integer integer = new Integer(2);\n        int hash = integer.hashCode();\n\n        // case1:结果为true，说明当参与==比较的两个中有一个是基本类型，就按照基本类型来比较\n        System.out.println(hash == integer);\n\n        Integer integer1 = new Integer(2);\n        // case2:结果为false，说明是按照两个对象的引用来比较的\n        System.out.println(integer == integer1);\n        // Case3:结果为true，说明是使用equals方法进行比较的，而equals是用了它们的值来比较\n        System.out.println(integer.equals(integer1));\n\n        MyObj myObj = new MyObj(2);\n        MyObj myObj1 = new MyObj(2);\n        // case4:比较结果为false，说明是按照两个对象的引用来比较的\n        System.out.println(myObj == myObj1);\n        // case5:比较结果为false，因为用了Object的equals方法实现，而该方法实现中使用==比较的\n        System.out.println(myObj.equals(myObj1));\n    }\n\n    private static class MyObj {\n        private int o;\n\n        public MyObj(int o) {\n            this.o = o;\n        }\n    }\n\n输出结果：\n\n\ttrue\n\tfalse\n\ttrue\n\tfalse\n\tfalse"
  },
  {
    "path": "笔试面试/java/jvm-java 内存模型 以及各个分区具体内容.md",
    "content": "# jvm-java 内存模型 以及各个分区具体内容\n\n1. 每一个应用程序都有一个JVM，而不是多个应用程序共享一个JVM；\n2. 首先通过编译器，把java源文件编译成 jvm语法的字节码文件。这个过程，是不涉及到JVM的；然后，jvm通过类加载，把需要的类字节码文件，加载进内存中。\n3. jvm运行时内存分为两部分：线程共享内存和线程私有内存\n\t1. 线程共享内存包括：堆、方法区（包含运行时常量池）；\n\t2. 线程非共享内存包括：虚拟机栈，本地方法栈，PC程序寄存器； \n\t3. 栈里面存储着一个个栈帧，每一个栈帧可看做一个方法的调用，包含方法信息；\n\t4. 方法区不是执行方法的，执行方法的内存在java栈中；\n\t5. 方法区是涉及到类加载的时候，加载进来的类的信息、常量、字段、方法代码等的信息；\n\t6. 一旦线程结束，线程私有内存也将释放，所以这部分内存不需要关心回收。\n\n\n参考：\n\n1. [http://blog.csdn.net/steady_pace/article/details/51254740](http://blog.csdn.net/steady_pace/article/details/51254740)\n2. [https://github.com/Shouheng88/Java-Programming/blob/master/JVM/2.Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E5%92%8C%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%BC%82%E5%B8%B8.md](https://github.com/Shouheng88/Java-Programming/blob/master/JVM/2.Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E5%92%8C%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%BC%82%E5%B8%B8.md)"
  },
  {
    "path": "笔试面试/java/throw和throws有什么区别.md",
    "content": "## throw和throws有什么区别\n\n下面是throw和throws的主要用法：\n\n    public void sort() throws IllegalAccessException {\n        throw new IllegalAccessException();\n    }\n\n我们可以总结一下它们的区别：\n\n1. throw代表动作，表示抛出一个异常的动作；throws代表一种状态，代表方法可能有异常抛出\n2. throw用在方法实现中，而throws用在方法声明中\n3. throw只能用于抛出一种异常，而throws可以抛出多个异常"
  },
  {
    "path": "笔试面试/java/wait()和sleep()的区别.md",
    "content": "## Java中sleep和wait的区别\n\n1. **所属的类不同**：这两个方法来自不同的类分别是，sleep来自Thread类，和wait来自Object类。\n\n    sleep是Thread的静态类方法，调用该方法的线程会进入睡眠状态，即使在A线程里调用B的sleep方法，实际上还是A去休眠，要让B线程睡觉要在b的代码中调用sleep。\n\n2. **锁**::最主要是sleep方法没有释放锁，而wait方法释放了锁，使得其他线程可以使用同步控制块或者方法。\n\n    sleep不出让系统资源；wait是进入线程等待池等待，出让系统资源，其他线程可以占用CPU。一般wait不会加时间限制，因为如果wait线程的运行资源不够，再出来也没用，要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程，才会进入就绪队列等待OS分配系统资源。\n\n    sleep(milliseconds)可以用时间指定使它自动唤醒过来，如果时间不到只能调用interrupt()强行打断。\n\n3. **使用范围**：wait，notify和notifyAll只能在同步控制方法或者同步控制块里面使用，而sleep可以在任何地方使用。\n\n        synchronized(x){ \n            x.notify() \n           //或者wait() \n        }\n\n\n\n\n\n\n"
  },
  {
    "path": "笔试面试/java/九种基本数据类型的大小以及他们的封装类.md",
    "content": "\n\t基本类型 大小(字节)   默认值         封装类\n\tbyte    1           (byte)0       Byte\n\tshort   2           (short)0      Short\n\tint     4           0             Integer\n\tlong    8           0L            Long\n\tfloat   4           0.0f          Float\n\tdouble  8           0.0d          Double\n\tboolean -           false         Boolean\n\tchar    2           \\u0000(null)  Character\n\tvoid    -           -             Void\n\n1. int默认值是0，而Integer默认值是null，所以Integer能区分出0和null的情况。一旦java看到null，就知道这个引用还没有指向某个对象，再任何引用使用前，必须为其指定一个对象，否则会报错。\n2. 基本数据类型在堆栈中创建，直接存储值；而对象类型，对象在堆中创建，对象的引用在堆栈中创建。\n3. 基本类型由于在堆栈中，效率高，但可能发生内存泄漏。\n4. 基本数据类型在声明时系统自动为其分配空间，而引用类型声明时只是分配了引用空间，必须通过实例化开辟数据空间后才能赋值。\n5. 基本数据类型只能按值传递；封装类按引用传递。\n\n\n"
  },
  {
    "path": "笔试面试/java/内存溢出和内存泄露的区别.md",
    "content": "**内存溢出Out of Memory**：是指程序在申请内存时，没有足够的内存空间供其使用，出现Out Of Memory；比如申请了一个Integer，但给它存了long才能存下的数，那就是内存溢出。\n\n**内存泄露Memory Leak**：是指程序在申请内存后，无法释放已申请的内存空间，一次内存泄露危害可以忽略，但内存泄露堆积后果很严重，无论多少内存,迟早会被占光。\n\n**Memory Leak**会最终会导致**Out of Memory**！\n\n以发生的方式来分类，内存泄漏可以分为4类： \n\n1. **常发性内存泄漏**：发生内存泄漏的代码会被多次执行到，每次被执行的时候都会导致一块内存泄漏。 \n2. **偶发性内存泄漏**：发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境，偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 \n3. **一次性内存泄漏**：发生内存泄漏的代码只会被执行一次，或者由于算法上的缺陷，导致总会有一块仅且一块内存发生泄漏。比如，在类的构造函数中分配内存，在析构函数中却没有释放该内存，所以内存泄漏只会发生一次。 \n4. **隐式内存泄漏**：程序在运行过程中不停的分配内存，但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏，因为最终程序释放了所有申请的内存。但是对于一个服务器程序，需要运行几天，几周甚至几个月，不及时释放内存也可能导致最终耗尽系统的所有内存。所以，我们称这类内存泄漏为隐式内存泄漏。 \n\n"
  },
  {
    "path": "笔试面试/java/解析XML的几种方式的原理与特点：DOM、SAX、PULL.md",
    "content": "# 解析XML的几种方式的原理与特点：DOM、SAX、PULL\n    \nDOM：消耗内存：先把xml文档都读到内存中，然后再用DOM API来访问树形结构，并获取数据。这个写起来很简单，但是很消耗内存。要是数据过大，手机不够牛逼，可能手机直接死机\n\nSAX：解析效率高，占用内存少，基于事件驱动的：更加简单地说就是对文档进行顺序扫描，当扫描到文档(document)开始与结束、元素(element)开始与结束、文档(document)结束等地方时通知事件处理函数，由事件处理函数做相应动作，然后继续同样的扫描，直至文档结束。\n\nSAX：与SAX类似，也是基于事件驱动，我们可以调用它的next()方法，来获取下一个解析事件（就是开始文档，结束文档，开始标签，结束标签），当处于某个元素时可以调用XmlPullParser的getAttributte()方法来获取属性的值，也可调用它的nextText()获取本节点的值。\n"
  },
  {
    "path": "笔试面试/今日头条Android面试.md",
    "content": "连接 http://blog.51cto.com/13127751/2117127\n\n第一面 \n\n1，请编程实现单例模式，懒汉和饱汉写法。\n\n2，请编程实现Java的生产者-消费者模型 \n\n3，HashMap的内部结构？ 内部原理? \n\n关于HashMap的问题，不再详述，这方面的资料也挺多，不多需要注意的是Java1.7和1.8版本HashMap内部结构的区别。 \n\n4，请简述Android事件传递机制， ACTION_CANCEL事件何时触发？ \n\n关于第一个问题，不做任何解释。     \n关于ACTION_CANCEL何时被触发，系统文档有这么一种使用场景：在设计设置页面的滑动开关时，如果不监听ACTION_CANCEL，在滑动到中间时，如果你手指上下移动，就是移动到开关控件之外，则此时会触发ACTION_CANCEL，而不是ACTION_UP，造成开关的按钮停顿在中间位置。 \n意思是当滑动的时候就会触发，不知道大家搞没搞过微信的长按录音，有一种状态是“松开手指，取消发送”，这时候就会触发ACTION_CANCEL。\n\n5，Android的进程间通信，Liunx操作系统的进程间通信。 \n关于这个问题也是被问的很多，此处也不做解释。\n\n6，JVM虚拟机内存结构，以及它们的作用。 \n这个问题也比较基础，JVM的内存结构如下图所示。 \n\n![](https://wx2.sinaimg.cn/large/9ccc0ca9gy1frdahlva35j20u00kr3zl.jpg)\n\n可以通过下面的问题来学习： \n\nhttps://www.cnblogs.com/jiyukai/p/6665199.html   \nhttps://www.zhihu.com/question/65336620\n\n7，简述Android的View绘制流程，Android的wrap_content是如何计算的。\n\n8，有一个×××数组，包含正数和负数，然后要求把数组内的所有负数移至正数的左边，且保证相对位置不变，要求时间复杂度为O(n), 空间复杂度为O(1)。例如，{10, -2, 5, 8, -4, 2, -3, 7, 12, -88, -23, 35}变化后是{-2, -4，-3, -88, -23,5, 8 ,10, 2, 7, 12, 35}。\n\n要实现上面的效果有两种方式： \n第一种：两个变量，一个用来记录当前的遍历点，一个用来记录最左边的负数在数组中的索引值。然后遍历整个数组，遇到负数将其与负数后面的数进行交换，遍历结束，即可实现负数在左，正数在右。\n\n第二种：两个变量记录左右节点，两边分别开始遍历。左边的节点遇到负值继续前进，遇到正值停止。右边的节点正好相反。然后将左右节点的只进行交换，然后再开始遍历直至左右节点相遇。这种方式的时间复杂度是O(n).空间复杂度为O(1)\n\n第二面 \n1，bundle的数据结构，如何存储，既然有了Intent.putExtra，为啥还要用bundle。\n\nbundle的内部结构其实是Map，传递的数据可以是boolean、byte、int、long、float、double、string等基本类型或它们对应的数组，也可以是对象或对象数组。当Bundle传递的是对象或对象数组时，必须实现Serializable 或Parcelable接口。\n\n2，android的IPC通信方式，是否使用过 \n这方面的资料比较多，也不方便阐述\n\n3，Android的多点触控如何传递 \n核心类\n\n4，asynctask的原理 \nAsyncTask是对Thread和Handler的组合包装。 \nhttps://blog.csdn.net/iispring/article/details/50670388 \n5，android 图片加载框架有哪些，对比下区别 主要有4种：Android-Universal-Image-Loader、Picasso、Glide和Fresco \n\nAndroid-Universal-Image-Loader \n优点：支持下载进度监听（ImageLoadingListener） * 可在View滚动中暂停图片加载（PauseOnScrollListener） * 默认实现多种内存缓存算法（最大最先删除，使用最少最先删除，最近最少使用，先进先删除，当然自己也可以配置缓存算法） \n缺点：2015年之后便不再维护，该库使用前需要进行配置。 \n\n\nPicasso    \n优点：包较小（100k） * 取消不在视野范围内图片资源的加载 * 使用最少的内存完成复杂的图片转换 * 自动添加二级缓存 * 任务调度优先级处理 * 并发线程数根据网络类型调整 * 图片的本地缓存交给同为Square出品的okhttp处理，控制图片的过期时间。   \n缺点： \n功能较为简单，自身无法实现“本地缓存”功能。 \n\n\nGlide    \n优点：多种图片格式的缓存，适用于更多的内容表现形式（如Gif、WebP、缩略图、Video） * 生命周期集成（根据Activity或者Fragment的生命周期管理图片加载请求） * 高效处理Bitmap（bitmap的复用和主动回收，减少系统回收压力） * 高效的缓存策略，灵活（Picasso只会缓存原始尺寸的图片，Glide缓存的是多种规格），加载速度快且内存开销小（默认Bitmap格式的不同，使得内存开销是Picasso的一半）。   \n缺点：方法较多较复杂，因为相当于在Picasso上的改进，包较大（500k），影响不是很大。 \n\n\nFresco     \n优点：最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统，Fresco将图片放到一个特别的内存区域(Ashmem区) * 大大减少OOM（在更底层的Native层对OOM进行处理，图片将不再占用App的内存） * 适用于需要高性能加载大量图片的场景。    \n缺点：包较大（2~3M） * 用法复杂 * 底层涉及c++领域\n\n5，主线程中的Looper.loop()一直无限循环为什么不会造成ANR？ \nActivityThread.java 是主线程入口的类，ActivityThread.java 的main函数的内容如下。\n\n显然，ActivityThread的main方法主要就是做消息循环，一旦退出消息循环，那么你的应用也就退出了。那么这个死循环不会造成ANR异常呢？ \n\n说明：因为Android 的是由事件驱动的，looper.loop() 不断地接收事件、处理事件，每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下，如果它停止了，应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop()，而不是 Looper.loop() 阻塞它。也就说我们的代码其实就是在这个循环里面去执行的，当然不会阻塞了。来看一下handleMessage的源码：\n\n可以看见Activity的生命周期都是依靠主线程的Looper.loop，当收到不同Message时则采用相应措施。 \n\n如果某个消息处理时间过长，比如你在onCreate(),onResume()里面处理耗时操作，那么下一次的消息比如用户的点击事件不能处理了，整个循环就会产生卡顿，时间一长就成了ANR。\n\n总结：Looer.loop()方法可能会引起主线程的阻塞，但只要它的消息循环没有被阻塞，能一直处理事件就不会产生ANR异常。\n\n6，图片框架的一些原理知识\n\n7，其他的一些Android的模块化开发，热更新，组件化等知识。\n\nAndroid面试之主流框架\n在Android面试的时候，经常会被问到一些Android开发中用到的一些开发框架，如常见的网络请求框架Retrofit/OkHttp，组件通信框架EventBus/Dagger2，异步编程RxJava/RxAndroid等。本文给大家整理下上面的几个框架，以备面试用。\n\nEventBus\nEventBus是一个Android发布/订阅事件总线，简化了组件间的通信，让代码更加简介，但是如果滥用EventBus，也会让代码变得更加辅助。面试EventBus的时候一般会谈到如下几点：\n\n（1）EventBus是通过注解+反射来进行方法的获取的\n\n注解的使用：@Retention(RetentionPolicy.RUNTIME)表示此注解在运行期可知，否则使用CLASS或者SOURCE在运行期间会被丢弃。 \n通过反射来获取类和方法：因为映射关系实际上是类映射到所有此类的对象的方法上的，所以应该通过反射来获取类以及被注解过的方法，并且将方法和对象保存为一个调用实体。\n\n（2）使用ConcurrentHashMap来保存映射关系\n\n调用实体的构建：调用实体中对于Object，也就是实际执行方法的对象不应该使用强引用而是应该使用弱引用，因为Map的static的，生命周期有可能长于被调用的对象，如果使用强引用就会出现内存泄漏的问题。\n\n说明：并发编程实践中，ConcurrentHashMap是一个经常被使用的数据结构，相比于Hashtable以及Collections.synchronizedMap()，ConcurrentHashMap在线程安全的基础上提供了更好的写并发能力，但同时降低了对读一致性的要求。详情可以查看下面的文章： \nhttp://www.importnew.com/22007.html。\n\n（3）方法的执行\n\n使用Dispatcher进行方法的分派，异步则使用线程池来处理，同步就直接执行，而UI线程则使用MainLooper创建一个Handler，投递到主线程中去执行。\n\nRetrofit\n首先要明确EventBus中最核心的就是动态代理技术。\n\nJava中的动态代理：\n\n首先动态代理是区别于静态代理的，代理模式中需要代理类和实际执行类同时实现一个相同的接口，并且在每个接口定义的方法前后都要加入相同的代码，这样有可能很多方法代理类都需要重复。而动态代理就是将这个步骤放入运行时的过程，一个代理类只需要实现InvocationHandler接口中的invoke方法，当需要动态代理时只需要根据接口和一个实现了InvocationHandler的代理对象A生成一个最终的自动生成的代理对象A*。这样最终的代理对象A*无论调用什么方法，都会执行InvocationHandler的代理对象A的invoke函数，你就可以在这个invoke函数中实现真正的代理逻辑。\n\n动态代理的实现机制实际上就是使用Proxy.newProxyInstance函数为动态代理对象A生成一个代理对象A*的类的字节码从而生成具体A*对象过程，这个A*类具有几个特点，一是它需要实现传入的接口，第二就是所有接口的实现中都会调用A的invoke方法，并且传入相应的调用实际方法（即接口中的方法）。\n\nRetrofit中的动态代理\nRetrofit中使用了动态代理是不错，但是并不是为了真正的代理才使用的，它只是为了动态代理一个非常重要的功能，就是“拦截”功能。我们知道动态代理中自动生成的A*对象的所有方法执行都会调用实际代理类A中的invoke方法，再由我们在invoke中实现真正代理的逻辑，实际上也就是A*的所有方法都被A对象给拦截了。 \n而Retrofit的功能就是将代理变成像方法调用那么简单。\n\n再用这个retrofit对象创建一个ServiceApi对象，并通过getAuthor函数来调用函数。\n\n也就是一个网络调用你只需要在你创建的接口里面通过注解进行设置，然后通过retrofit创建一个api然后调用，就可以自动完成一个Okhttp的Call的创建。Retrofit的create()函数的代码如下：\n\n我们可以看出怎么从接口类创建成一个API对象？就是使用了动态代理中的拦截技术，通过创建一个符合此接口的动态代理对象A*，那A呢？就是这其中创建的这个匿名类了，它在内部实现了invoke函数，这样A*调用的就是A中的invoke函数，也就是被拦截了，实际运行invoke。而invoke就是根据调用的method的注解（，从而生成一个符合条件的Okhttp的Call对象，并进行真正的请求。\n\nRetrofit作用\nRetrofit实际上是为了更方便的使用Okhttp，因为Okhttp的使用就是构建一个Call，而构建Call的大部分过程都是相似的，而Retrofit正是利用了代理机制带我们动态的创建Call，而Call的创建信息就来自于你的注解。\n\nOkHttp3\n关于OkHttp3的内容大家可以访问下面的博客链接：OkHttp3源码分析。该文章主要从以下几个方面来讲解OkHttps相关的内容： \nOkHttp3源码分析[综述] \nOkHttp3源码分析[复用连接池] \nOkHttp3源码分析[缓存策略] \nOkHttp3源码分析[DiskLruCache] \nOkHttp3源码分析[任务队列]\n\n请求任务队列\nOkhttp使用了一个线程池来进行异步网络任务的真正执行，而对于任务的管理采用了任务队列的模型来对任务执行进行相应的管理，有点类似服务器的反向代理模型。Okhttp使用分发器Dispatcher来维护一个正在运行任务队列和一个等待队列。如果当前并发任务数量小于64，就放入执行队列中并且放入线程池中执行。而如果当前并发数量大于64就放入等待队列中，在每次有任务执行完成之后就在finally块中调用分发器的finish函数，在等待队列中查看是否有空余任务，如果有就进行入队执行。Okhttp就是使用任务队列的模型来进行任务的执行和调度的。\n\n复用连接池\nHttp使用的TCP连接有长连接和短连接之分，对于访问某个服务器的频繁通信，使用短连接势必会造成在建立连接上大量的时间消耗；而长连接的长时间无用保持又会造成资源你的浪费。Okhttp底层是采用Socket建立流连接，而连接如果不手动close掉，就会造成内存泄漏，那我们使用Okhttp时也没有做close操作，其实是Okhttp自己来进行连接池的维护的。在Okhttp中，它使用类似引用计数的方式来进行连接的管理，这里的计数对象是StreamAllocation，它被反复执行aquire与release操作，这两个函数其实是在改变Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的数量也就是物理socket被引用的计数（Refference Count），如果计数为0的话，说明此连接没有被使用，是空闲的，需要通过淘汰算法实现回收。\n\n在连接池内部维护了一个线程池，这个线程池运行的cleanupRunnable实际上是一个阻塞的runnable，内部有一个无限循环，在清理完成之后调用wait进行等待，等待的时间由cleanup的返回值决定，在等待时间到了之后再进行清理任务。相关代码如下：\n\n其中，Cleanup函数的执行过程如下：\n\n遍历Deque中所有的RealConnection，标记泄漏的连接；\n\n如果被标记的连接满足(空闲socket连接超过5个&&keepalive时间大于5分钟)，就将此连接从Deque中移除，并关闭连接，返回0，也就是将要执行wait(0)，提醒立刻再次扫描；\n\n如果(目前还可以塞得下5个连接，但是有可能泄漏的连接(即空闲时间即将达到5分钟))，就返回此连接即将到期的剩余时间，供下次清理；\n\n如果(全部都是活跃的连接)，就返回默认的keep-alive时间，也就是5分钟后再执行清理；\n\n如果(没有任何连接)，就返回-1,跳出清理的死循环。\n\n说明：“并发”==(“空闲”＋“活跃”)==5，而不是说并发连接就一定是活跃的连接。\n\n如何标记空闲的连接呢？我们前面也说了，如果一个连接身上的引用为0，那么就说明它是空闲的，那么就要使用pruneAndGetAllocationCount来计算它身上的引用数，如同引用计数过程。 \n其实标记引用为0的算法很简单，就是遍历它的List<Reference<StreamAllocation>>，删除所有已经为null的弱引用，剩下的数量就是现在它的引用数量，pruneAndGetAllocationCount函数的源码如下：\n\n\n\nRxJava\n\n从15年开始，前端掀起了一股异步编程的热潮，在移动Android编程过程中，经常会听到观察者与被观察者等概念。\n\n观察者与被观察者通信\nObservable的通过create函数创建一个观察者对象。\n\n\n\nObservable的构造函数如下：\n\n\n\n创建了一个Observable我们记为Observable1，保存了传入的OnSubscribe对象为onSubscribe，这个很重要，后面会说到。\n\nonSubscribe方法\n\n\n\nRxjava的变换过程\n\n在RxJava中经常会数据转换，如map函数，filtmap函数和lift函数。 \n\n\nlift函数\n\n\n\n我们可以看到这里我们又创建了一个新的Observable对象，我们记为Observable2，也就是说当我们执行map时，实际上返回了一个新的Observable对象，我们之后的subscribe函数实际上执行再我们新创建的Observable2上，这时他调用的就是我们新的call函数，也就是Observable2的call函数（加粗部分），我们来看一下这个operator的call的实现。这里call传入的就是我们的Subscriber1对象，也就是调用最终的subscribe的处理对象。\n\ncall函数\n\n\n\n这里的transformer就是我们在map调用是传进去的func函数，也就是变换的具体过程。那看之后的onSubscribe.call（回到call中），这里的onSubscribe是谁呢？就是我们Observable1保存的onSubscribe对象，也就是我们前面说很重要的那个对象。而这个o（又回来了）就是我们的Subscriber1，这里可以看出，在调用了转换函数之后我们还是调用了一开始的Subscriber1的onNext，最终事件经过转换传给了我们的结果。\n\n线程切换过程（Scheduler）\nRxJava最好用的特点就是提供了方便的线程切换，但它的原理归根结底还是lift，使用subscribeOn()的原理就是创建一个新的Observable，把它的call过程开始的执行投递到需要的线程中；而 observeOn() 则是把线程切换的逻辑放在自己创建的Subscriber中来执行。把对于最终的Subscriber1的执行过程投递到需要的线程中来进行。 \n\n\n\n从图中可以看出，subscribeOn() 和 observeOn() 都做了线程切换的工作（图中的 “schedule…” 部位）。不同的是， subscribeOn()的线程切换发生在 OnSubscribe 中，即在它通知上一级 OnSubscribe 时，这时事件还没有开始发送，因此 subscribeOn() 的线程控制可以从事件发出的开端就造成影响；而 observeOn() 的线程切换则发生在它内建的 Subscriber 中，即发生在它即将给下一级 Subscriber 发送事件时，因此 observeOn() 控制的是它后面的线程。\n\n为什么subscribeOn()只有第一个有效？ \n因为它是从通知开始将后面的执行全部投递到需要的线程来执行，但是之后的投递会受到在它的上级的（但是执行在它之后）的影响，如果上面还有subscribeOn() ，又会投递到不同的线程中去，这样就不受到它的控制了。\n\n"
  },
  {
    "path": "笔试面试/初级工程师.md",
    "content": "﻿# Java 面试清单\n\n## 1、Java 基础\n\n- [x] 九种基本数据类型的大小，以及他们的封装类；[参考](java/九种基本数据类型的大小以及他们的封装类.md)\n- [x] switch能否用String做参数；[参考](java/Switch能否用string做参数.md)\n- [x] `equals`与`==`的区别；[参考](java/equals与==的区别.md)\n- [x] Object有哪些公用方法；[参考](java/Object有哪些公用方法.md)\n- [x] Java的四种引用，强弱软虚，用到的场景；[参考](java/Java的四种引用，强弱软虚，用到的场景.md)\n- [x] Hashcode的作用；[参考](java/Hashcode的作用.md)\n- [x] ArrayList、LinkedList、Vector的区别；[参考](java/ArrayList、LinkedList、Vector.md)\n- [x] String、StringBuffer与StringBuilder的区别；[参考](java/String、StringBuffer与StringBuilder的区别.md)\n- [x] Map、Set、List、Queue、Stack的特点与用法；[参考](java/Map、Set、List、Queue、Stack的特点与用法.md)\n- [x] HashMap和HashTable的区别；[参考](java/HashMap和HashTable的区别.md)\n- [x] HashMap和ConcurrentHashMap的区别，HashMap的底层源码；[参考](java/HashMap和ConcurrentHashMap的区别，HashMap的底层源码.md)\n- [x] TreeMap、HashMap、LindedHashMap的区别；[参考](java/TreeMap、HashMap、LindedHashMap.md)\n- [x] Collection包结构，与Collections的区别；[参考](java/Collection包结构，与Collections的区别.md)\n- [x] try catch finally，try里有return，finally还执行么；\n- [x] Excption与Error包结构。OOM你遇到过哪些情况，SOF你遇到过哪些情况；\n- [ ] Java面向对象的三个特征与含义：封装、继承、多态；\n- [ ] Override和Overload的含义与区别；\n- [ ] Interface与abstract类的区别；\n- [ ] 静态类与非静态类的区别；\n- [ ] java多态的实现原理；\n- [ ] 实现多线程的两种方法：Thread与Runable；\n- [ ] 线程同步的方法：sychronized、lock、reentrantLock等；\n- [ ] 锁的等级：方法锁、对象锁、类锁；\n- [ ] 写出生产者消费者模式；\n- [ ] ThreadLocal的设计理念与作用；\n- [ ] ThreadPool用法与优势；\n- [ ] Concurrent包里的其他东西：ArrayBlockingQueue、CountDownLatch等等\n- [ ] wait()和sleep()的区别；\n- [ ] foreach与正常for循环效率对比；\n- [ ] 反射的作用与原理；\n- [ ] Java IO与NIO；\n- [ ] 泛型常用特点，`List<String>`能否转为`List<Object>`，不行；\n- [ ] Java1.7与1.8新特性；\n- [ ] JNI的使用；\n- [ ] 简述在异常当中，throw和throws有什么区别；\n- [ ] 死锁的必要条件，怎么处理死锁\n\n## 2、Java 虚拟机\n\n- [ ] 内存溢出和内存泄露的区别；\n- [ ] 内存模型以及分区，需要详细到每个区放什么；\n- [ ] 堆里面的分区：Eden，survival from to，老年代，各自的特点；\n- [ ] 对象创建方法，对象的内存分配，对象的访问定位；\n- [ ] GC的两种判定方法：引用计数与引用链；\n- [ ] GC的三种收集方法：标记清除、标记整理、复制算法的原理与特点，分别用在什么地方，如果让你优化收集方法，有什么思路；\n- [ ] GC收集器有哪些？CMS收集器与G1收集器的特点；\n- [ ] Minor GC与Full GC分别在什么时候发生；\n- [ ] 几种常用的内存调试工具：jmap、jstack、jconsole；\n- [ ] 类加载的五个过程：加载、验证、准备、解析、初始化；\n- [ ] 双亲委派模型：Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader；\n- [ ] 分派：静态分派与动态分派；\n\n## 3、操作系统\n\n- [ ] 进程和线程的区别；\n- [ ] Window内存管理方式：段存储，页存储，段页存储；\n- [ ] 进程的几种状态；\n- [ ] IPC几种通信方式；\n- [ ] 什么是虚拟内存；\n- [ ] 虚拟地址、逻辑地址、线性地址、物理地址的区别；\n\n## 4、TCP/IP\n  \n- [ ] OSI与TCP/IP各层的结构与功能，都有哪些协议；\n- [ ] TCP与UDP的区别；\n- [ ] TCP报文结构；\n- [ ] TCP的三次握手与四次挥手过程，各个状态名称与含义，TIMEWAIT的作用；\n- [ ] TCP拥塞控制；\n- [ ] TCP滑动窗口与回退N针协议；\n- [ ] Http的报文结构；\n- [ ] Http的状态码含义；\n- [ ] Http request的几种类型；\n- [ ] Http1.1和Http1.0的区别；\n- [ ] Http怎么处理长连接；\n- [ ] Cookie与Session的作用于原理；\n- [ ] 电脑上访问一个网页，整个过程是怎么样的：DNS、HTTP、TCP、OSPF、IP、ARP；\n- [ ] Ping的整个过程。ICMP报文是什么；\n- [ ] C/S模式下使用socket通信，几个关键函数；\n- [ ] IP地址分类；\n- [ ] 路由器与交换机区别；\n\n## 5、数据结构与算法\n  \n1. 链表与数组。\n2. 队列和栈，出栈与入栈。\n3. 链表的删除、插入、反向。\n4. 字符串操作。\n5. Hash表的hash函数，冲突解决方法有哪些。\n6. 各种排序：冒泡、选择、插入、希尔、归并、快排、堆排、桶排、基数的原理、平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定。\n7. 快排的partition函数与归并的Merge函数。\n8. 对冒泡与快排的改进。\n9. 二分查找，与变种二分查找。\n10. 二叉树、B+树、AVL树、红黑树、哈夫曼树。\n11. 二叉树的前中后续遍历：递归与非递归写法，层序遍历算法。\n12. 图的BFS与DFS算法，最小生成树prim算法与最短路径Dijkstra算法。\n13. KMP算法。\n14. 排列组合问题。\n15. 动态规划、贪心算法、分治算法。（一般不会问到）\n16. 大数据处理：类似10亿条数据找出最大的1000个数.........等等\n\n## 6、Android\n  \n1. Activity与Fragment的生命周期。\n2. Acitivty的四中启动模式与特点。\n3. Activity缓存方法。\n4. Service的生命周期，两种启动方法，有什么区别。\n5. 怎么保证service不被杀死。\n6. 广播的两种注册方法，有什么区别。\n7. Intent的使用方法，可以传递哪些数据类型。\n8. ContentProvider使用方法。\n9. Thread、AsycTask、IntentService的使用场景与特点。\n10. 五种布局：FrameLayout、LinearLayout、AbsoluteLayout、RelativeLayout、TableLayout各自特点及绘制效率对比。\n11. Android的数据存储形式。\n12. Sqlite的基本操作。\n13. Android中的MVC模式。\n14. Merge、ViewStub的作用。\n15. Json有什么优劣势。\n16. 动画有哪两类，各有什么特点？\n17. Handler、Loop消息队列模型，各部分的作用。\n18. 怎样退出终止App。\n19. Asset目录与res目录的区别。\n20. Android怎么加速启动Activity。\n21. Android内存优化方法：ListView优化，及时关闭资源，图片缓存等等。\n22. Android中弱引用与软引用的应用场景。\n23. Bitmap的四中属性，与每种属性队形的大小。\n24. View与View Group分类。自定义View过程：onMeasure()、onLayout()、onDraw()。\n25. Touch事件分发机制。\n26. Android长连接，怎么处理心跳机制。\n27. Zygote的启动过程。\n28. Android IPC:Binder原理。\n29. 你用过什么框架，是否看过源码，是否知道底层原理。\n30. Android 5.0、6.0 新特性。\n"
  },
  {
    "path": "系统架构/Android应用启动过程.md",
    "content": "# Android 应用启动过程\n\n在之前的文中，我们已经了解过了 Android 系统启动的过程。系统启动之后会由 PMS 安装系统应用，并启动 Launcher，也就是桌面程序。然后，我们安装的程序的图标将会显示到桌面上面。\n\n所谓应用启动过程分成两种情形，一个是应用进程已经建立，一种是应用进程没有建立的情况下。后者需要先创建应用进程，然后再执行启动的过程。\n\n安卓系统中的应用在源码中的位置是 `platform/packages/apps`。这里我们以 Launcher3 为例，它的 Launcher 类也就是我们通常所说的 Main Activity. 当系统启动的时候会由它来展示我们安装的各种应用。\n\n当我们点击应用的图标的时候将会启动应用，它先以 `Intent.FLAG_ACTIVITY_NEW_TASK` 构建一个 Intent 来启动 Activity. 随后的过程就与启动一个普通的 Activity 差不多（调用 Activity 的 `startActivity()` 方法），只是当应用进程不存在的情况下，需要先创建应用进程。\n\n## 1、应用进程启动的过程\n\n从之前的文中，我们知道系统启动的时候会创建一个 Server 端的 Socket 等待 AMS 请求 Zygote 通过 fork 来创建应用进程。当 AMS 需要启动应用进程的时候，它将会调用下面的方法，\n\n```java\n    // platform\\framework\\base\\services\\core\\java\\com\\android\\server\\am\\ActivityManagerService.java\n    private ProcessStartResult startProcess(String hostingType, String entryPoint,\n            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,\n            String seInfo, String requiredAbi, String instructionSet, String invokeWith,\n            long startTime) {\n        try {\n            final ProcessStartResult startResult;\n            if (hostingType.equals(\"webview_service\")) {\n                startResult = startWebView(entryPoint,\n                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,\n                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,\n                        app.info.dataDir, null,\n                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});\n            } else {\n                startResult = Process.start(entryPoint,\n                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,\n                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,\n                        app.info.dataDir, invokeWith,\n                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});\n            }\n            return startResult;\n        }\n    }\n```\n\n当然，方法名称和调用的位置可能因为源码的版本不同而不同。但它们本质上都是通过调用 `Process` 的 `start()` 方法来最终启动应用进程的。\n\n方法的调用会通过 `Process` 的 `start()` 方法直到 ZygoteProcess 的 `startViaZygote()`. 因为调用链比较简单，所以我们直接给出下面的方法即可，\n\n```java\n    // platform\\framework\\base\\core\\java\\android\\os\\ZygoteProcess.java\n    private Process.ProcessStartResult startViaZygote(/* 各种参数 */)\n            throws ZygoteStartFailedEx {\n        ArrayList<String> argsForZygote = new ArrayList<String>();\n\n        // --runtime-args, --setuid=, --setgid=,\n        // and --setgroups= must go first\n        argsForZygote.add(\"--runtime-args\");\n        argsForZygote.add(\"--setuid=\" + uid);\n        argsForZygote.add(\"--setgid=\" + gid);\n        argsForZygote.add(\"--runtime-flags=\" + runtimeFlags);\n        if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {\n            argsForZygote.add(\"--mount-external-default\");\n        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {\n            argsForZygote.add(\"--mount-external-read\");\n        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {\n            argsForZygote.add(\"--mount-external-write\");\n        }\n        argsForZygote.add(\"--target-sdk-version=\" + targetSdkVersion);\n\n        // ... 准备各种参数\n\n        synchronized(mLock) {\n            return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);\n        }\n    }\n```\n\n可以看到，这里准备了一些用于 Zygote 的参数，然后调用了 `zygoteSendArgsAndGetResult()` 方法来向 Zygote 发送请求并获取返回结果。这个方法中要求输入一个 ZygoteState 类型的参数。这个类主要封装了一些与 Zygote 进程通信的状态。这个变量是通过 `openZygoteSocketIfNeeded()` 方法得到的，它用来建立与 Zygote 进程之间的 Socket 连接。\n\n```java\n    private static Process.ProcessStartResult zygoteSendArgsAndGetResult(\n            ZygoteState zygoteState, ArrayList<String> args)\n            throws ZygoteStartFailedEx {\n        try {\n            // ...\n\n            // 获取 Zygote 的读写流\n            final BufferedWriter writer = zygoteState.writer;\n            final DataInputStream inputStream = zygoteState.inputStream;\n    \n            // 通过流向 Zygote 写命令\n            writer.write(Integer.toString(args.size()));\n            writer.newLine();\n\n            for (int i = 0; i < sz; i++) {\n                String arg = args.get(i);\n                writer.write(arg);\n                writer.newLine();\n            }\n\n            writer.flush();\n\n            // 读取返回结果\n            Process.ProcessStartResult result = new Process.ProcessStartResult();\n            result.pid = inputStream.readInt();\n            result.usingWrapper = inputStream.readBoolean();\n\n            if (result.pid < 0) {\n                throw new ZygoteStartFailedEx(\"fork() failed\");\n            }\n            return result;\n        } catch (IOException ex) {\n            zygoteState.close();\n            throw new ZygoteStartFailedEx(ex);\n        }\n    }\n```\n\n上面我们也看到了，这里会先从 ZygoteState 中获取输入流和输出流，然后使用流来进行读写。实际上呢，在获取流之前先使用 ZygoteState 的 `connect()` 方法与 Zygote 建立了 Socket 连接。\n\n这里是发送 Socket 给 Zygote，那么远程的 Zygote 是如何对连接进行处理的呢？如果你阅读过我们上一篇文章就会知道，ZygoteServer 启动的时候会执行 `runSelectLoop()` 方法不断对 Socket 进行监听，当收到 AMS 的创建应用进程的请求之后，会调用 Zygote 类的静态方法 `forkAndSpecialize()` 来创建子进程。读者可以参考下面的文章来了解，\n\n[《系统源码-1：Android 系统启动流程源码分析》](https://blog.csdn.net/github_35186068/article/details/86563397)\n\n创建子进程完毕之后会将创建的结果返回给调用者，然后 Zygote 需要对 fork 的子进程的结果进行后续处理，比如启动进程中的方法等。这些将交给 `handleChildProc()` 方法来完成，\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/ZygoteConnection.java\n    private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,\n            FileDescriptor pipeFd, boolean isZygote) {\n        // ...\n        if (parsedArgs.invokeWith != null) {\n            throw new IllegalStateException(\"WrapperInit.execApplication unexpectedly returned\");\n        } else {\n            if (!isZygote) {\n                return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,\n                        null /* classLoader */);\n            } else {\n                return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,\n                        parsedArgs.remainingArgs, null /* classLoader */);\n            }\n        }\n    }\n```\n\n这里的 `zygoteInit()` 用来对 Zygote 的子进程进行处理。\n\n```java\n    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {\n        RuntimeInit.redirectLogStreams();\n        RuntimeInit.commonInit();\n        ZygoteInit.nativeZygoteInit(); // 启动 Binder 线程池\n        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader); // 调用到 ActivityThread 的 main 方法\n    }\n```\n\n它主要做了两件事情：1).启动 Binder 线程池；2).调用到 ActivityThread 的 main 方法，这样程序就进入到了我们的 ActivityThread 中。启动 Binder 线程池的时候，将会通过 JNI 调用进入 AndroidRuntime.cpp 中，\n\n```c++\nstatic void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)\n{\n    gCurRuntime->onZygoteInit();\n}\n```\n\n这里的 gCurRuntime 就是 AppRuntime 了，它定义在 app_main.cpp 文件中。它会调用下面的方法来启动 Binder 线程池，\n\n\n```c++\n    virtual void onZygoteInit()\n    {\n        sp<ProcessState> proc = ProcessState::self();\n        proc->startThreadPool();\n    }\n```\n\n## 2、已存在应用进程的时候的启动过程\n\n上面是创建应用进程的过程，下面我们再来看下当应用进程创建之后，应用将如何启动。当我们从 Launcher 页面启动 Activity 的时候会通过 Activity 的 `startActivity()` 启动 Activity. 最终所有的启动 Activity 的操作都将经过 `startActivityForResult()` 方法处理。它将调用 Instrumentation 的 `execStartActivity()` 方法来执行启动操作。\n\n```java\n    public ActivityResult execStartActivity(\n            Context who, IBinder contextThread, IBinder token, Activity target,\n            Intent intent, int requestCode, Bundle options) {\n        // 当前应用的 Binder\n        IApplicationThread whoThread = (IApplicationThread) contextThread;\n        Uri referrer = target != null ? target.onProvideReferrer() : null;\n        if (referrer != null) {\n            intent.putExtra(Intent.EXTRA_REFERRER, referrer);\n        }\n        if (mActivityMonitors != null) {\n            // ...\n        }\n        try {\n            intent.migrateExtraStreamToClipData();\n            intent.prepareToLeaveProcess(who);\n            // 获取 AMS 启动 Activity\n            int result = ActivityManager.getService()\n                .startActivity(whoThread, who.getBasePackageName(), intent,\n                        intent.resolveTypeIfNeeded(who.getContentResolver()),\n                        token, target != null ? target.mEmbeddedID : null,\n                        requestCode, 0, null, options);\n            checkStartActivityResult(result, intent);\n        } catch (RemoteException e) {\n            throw new RuntimeException(\"Failure from system\", e);\n        }\n        return null;\n    }\n```\n\n在这个方法中有两个比较关键的地方，一个是 IApplicationThread，它被用来交给 AMS 继续执行启动操作。AMS 通过 `ActivityManager.getService()`，它用来获取远程的 AMS 的 Binder 来调用。这里的 IApplicationThread 则是当前应用进程的代表，也是一个 Binder. 这样我们可以在当前应用进程中通过 AMS 执行启动操作（实际是在另一个进程完成的）。当启动操作完成了之后，AMS 可以通过当前进程的代表 IApplicationThread 调用本进程的方法来完成启动的后续任务，比如回调各个生命周期方法。\n\n我们先来从整体的角度看一下。如下面的图所示，IApplicationThread 和 AMS 作为两个代表在两个进程之间进行通信。\n\n![Activity 的启动流程](https://user-gold-cdn.xitu.io/2019/1/23/1687abe036d33a83?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)\n\n在上面的一节中，我们已经说明了在应用进程没有创建的时候是如何创建应用进程的。在创建应用进程的时候，会调用 `ActivityThread` 的 main 方法来，这个方法中会启动主线程的 Looper 来创建主线程的消息循环，这个 Looper 对应的消息处理的 Handler 就是 H. 以下面的程序为例，当 AMS 或者其他服务需要回调当前进程的方法的时候，可以直接调用下面的方法。其中的 `scheduleLowMemory()` 方法通过向 H 发送消息来在主线程中执行任务。这里的 `scheduleTransaction()` 是用来执行 Activity 等生命周期回调的。这里的 ClientTransaction 封装了回调的信息。Activity 的生命周期方法就是通过它来回调的。\n\n```java\n    private class ApplicationThread extends IApplicationThread.Stub {\n \n        @Override\n        public void scheduleLowMemory() {\n            sendMessage(H.LOW_MEMORY, null);\n        }\n \n        // ...\n\n        @Override\n        public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {\n            ActivityThread.this.scheduleTransaction(transaction);\n        }\n    }\n\n    class H extends Handler {\n        // ...\n        public static final int LOW_MEMORY              = 124;\n        // ...\n\n        public void handleMessage(Message msg) {\n            switch (msg.what) {\n                // ...\n                case LOW_MEMORY:\n                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, \"lowMemory\");\n                    handleLowMemory();\n                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);\n                    break;\n                // ...\n            }\n        }\n    }\n```\n\nOK，上面我们分析了 Activity 启动过程中的主要的交互逻辑。下面我们就看下 AMS 在启动的过程中做了上面操作。\n\n当启动过程进入到 AMS 之后，它会进行如下的处理，\n\n```java\n    public final int startActivityAsUser(/*各种参数*/) {\n        enforceNotIsolatedCaller(\"startActivity\");\n\n        userId = mActivityStartController.checkTargetUser(userId, validateIncomingUser,\n                Binder.getCallingPid(), Binder.getCallingUid(), \"startActivityAsUser\");\n\n        return mActivityStartController.obtainStarter(intent, \"startActivityAsUser\")\n                .setCaller(caller)\n                // ... 基于构建者模式进行各种赋值\n                .execute();\n\n    }\n```\n\n它会先通过 `UserHandle.getCallingUserId()` 获取启动的进程的用户信息，然后用 `Binder.getCallingPid()` 和 `Binder.getCallingUid()` 分别获取调用方的进程 ID 和用户 ID. 然后使用 mActivityStartController 检查用户信息是否合法。它内部实际上使用的是 UserController 来进行检查的。当发现用户信息不合法的时候将会抛出一个异常。\n\n然后，它通过 ActivityStartController 的 `obtainStarter()` 方法获取一个 ActivityStarter，使用构建者模式将启动信息传入之后，调用 `execute()` 方法执行启动逻辑。然后程序进入 ActivityStarter 的 `startActivityMayWait()` 方法。该方法中会先对传入的 Intent 的信息进行分析，比如传入的 ACTION_VIEW 等，然后调用 `startActivity()` 方法继续执行，从该方法中返回结果之后再对结果进行处理。随后，程序进入 `startActivityUnchecked()` 方法，这个方法主要负责与 Activity 栈相关的逻辑。Activity 的栈在 AMS 中使用 ActivityStack 类来表示，Activity 实例的信息则使用 ActivityRecord 来表示。\n\n```java\n    private int startActivityUnchecked(/*各种参数*/) {\n        // ...\n        int result = START_SUCCESS;\n        if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask\n                && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {\n            newTask = true;\n            // 在新的任务栈中执行任务\n            result = setTaskFromReuseOrCreateNewTask(taskToAffiliate, topStack);\n        } else if (mSourceRecord != null) {\n            result = setTaskFromSourceRecord();\n        } else if (mInTask != null) {\n            result = setTaskFromInTask();\n        } else {\n            setTaskToCurrentTopOrCreateNewTask();\n        }\n\n        // ...\n        if (mDoResume) {\n            final ActivityRecord topTaskActivity =\n                    mStartActivity.getTask().topRunningActivityLocked();\n            if (!mTargetStack.isFocusable()\n                    || (topTaskActivity != null && topTaskActivity.mTaskOverlay\n                    && mStartActivity != topTaskActivity)) {\n                // ...\n            } else {\n                if (mTargetStack.isFocusable() && !mSupervisor.isFocusedStack(mTargetStack)) {\n                    mTargetStack.moveToFront(\"startActivityUnchecked\");\n                }\n                // 将新的任务栈移动到前台（聚焦）\n                mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity, mOptions);\n            }\n        } else if (mStartActivity != null) {\n            mSupervisor.mRecentTasks.add(mStartActivity.getTask());\n        }\n        // ...\n        return START_SUCCESS;\n    }\n```\n\n `startActivityUnchecked()` 方法会根据 Activity 启动时指定的栈信息来决定创建新的栈还是在启动它的 Activity 所在的栈中执行。因为我们默认的启动类型时 NEW_TASK，因此我们将进入到 `setTaskFromReuseOrCreateNewTask()`。然后，新创建的栈将会被 focus，也就相当于移动到前台。这里调用了 ActivityStackSupervisor 的 `resumeFocusedStackTopActivityLocked()` 方法实现。在该方法中将根据当前的 ActivityRecord 是否已经进入了 RESUMED 状态来进行后续处理，它将调用当前栈的 `resumeTopActivityUncheckedLocked()` 方法。该方法的主要逻辑是对栈的 Activity 进行处理，因为一个新的 Activity 要加入，那么之前的 Activity 需要调用生命周期的方法，比如 `onStop()` 等，还要通知 WMS 进行处理。然后程序进入到 ActivityStackSupervisor 的 `startSpecificActivityLocked()` 方法中执行启动 Activity 真实的罗辑。在新版本的 Android 源码中，它采用如下的方式进行 Activity 的生命周期的回调，\n\n ```java\n     final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,\n            boolean andResume, boolean checkConfig) throws RemoteException {\n            // ...\n                final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread, r.appToken);\n                clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),\n                        System.identityHashCode(r), r.info,\n                        mergedConfiguration.getGlobalConfiguration(),\n                        mergedConfiguration.getOverrideConfiguration(), r.compat,\n                        r.launchedFromPackage, task.voiceInteractor, app.repProcState, r.icicle,\n                        r.persistentState, results, newIntents, mService.isNextTransitionForward(),\n                        profilerInfo));\n                // ...\n                mService.getLifecycleManager().scheduleTransaction(clientTransaction);\n        // ...\n    }\n ```\n\n最后当调用了 ClientTransaction 的 `schedule()` 方法的时候，它通过 IApplicationThread 的 `scheduleTransaction()` 方法将自身传递给当前应用的进程。当传递到当前进程之后，按照上面我们说的那样回调 Activity 的生命周期即可。\n\n## 总结\n\n在本文中我们分析了 Android 应用启动的源码。其分成两种情形，一个是应用的进程没有创建的时候，此时要通过 Socket 与服务端的 Socket 建立通信，通过 Zygote 创建当前进程的实例。另一个情形是应用已经启动的过程，此时我们的应用会通过 AMS 调用远程的服务，然后将 IApplicationThread 作为信使传递给 AMS，AMS 通过 IApplicationThread 调用当前应用的方法来回调 Activity 等的生命周期。\n\n以上就是 Android 应用启动过程的源码分析，如有疑问，欢迎评论区交流！\n"
  },
  {
    "path": "系统架构/Android应用安装过程.md",
    "content": "# Android 系统源码源码-应用安装过程\n\nAndroid 中应用安装的过程就是解析 AndroidManifest.xml 的过程，系统可以从 Manifest 中得到应用程序的相关信息，比如 Activity、Service、Broadcast Receiver 和 ContentProvider 等。这些工作都是由 PackageManageService 负责的，也就是所谓的 PMS. 它跟 AMS 一样都是一种远程的服务，并且都是在系统启动 SystemServer 的时候启动的。下面我们通过源代码来分析下这个过程。\n\n## 1、启动 PMS 的过程\n\n系统在启动 SystemServer 的过程会启动 PMS，系统的启动过程可以参考下面这篇文章学习，\n\n[Android 系统源码-1：Android 系统启动流程源码分析](https://juejin.im/post/5c4471e56fb9a04a027aa8ac)\n\n在启动 SystemServer 的时候会调用 `startBootstrapServices()` 方法启动引导服务。PMS 就是在这个方法中启动的，\n\n```java\n    private void startBootstrapServices() {\n        // ...\n        mPackageManagerService = PackageManagerService.main(mSystemContext, installer,\n                mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);\n        mFirstBoot = mPackageManagerService.isFirstBoot();\n        mPackageManager = mSystemContext.getPackageManager();\n        // ...\n    }\n```\n\n可以看出，系统是通过调用 PMS 的 main 方法来将其启动起来的。其 main 方法会先实例化一个 PMS 对象，然后调用 ServiceManager 的静态方法将其注册到 ServiceManager 中进行管理。\n\n```java\n    public static PackageManagerService main(Context context, Installer installer,\n            boolean factoryTest, boolean onlyCore) {\n        PackageManagerServiceCompilerMapping.checkProperties();\n\n        PackageManagerService m = new PackageManagerService(context, installer,\n                factoryTest, onlyCore);\n        m.enableSystemUserPackages();\n        ServiceManager.addService(\"package\", m);\n        final PackageManagerNative pmn = m.new PackageManagerNative();\n        ServiceManager.addService(\"package_native\", pmn);\n        return m;\n    }\n```\n\n当我们需要使用 PMS 解析 APK 的时候就会从 ServiceManager 中获取。\n\n在 PMS 的构造方法中有许多工作要完成。一个 APK 安装的主要分成下面几个步骤，\n\n1. **拷贝文件到指定的目录**：默认情况下，用户安装的 APK 首先会被拷贝到 `/data/app` 目录下，`/data/app` 目录是用户有权限访问的目录，在安装 APK 的时候会自动选择该目录存放用户安装的文件，而系统的 APK 文件则被放到了 `/system` 分区下，包括 `/system/app`，`/system/vendor/app`，以及 `/system/priv-app` 等等，该分区只有 ROOT 权限的用户才能访问，这也就是为什么在没有 Root 手机之前，我们没法删除系统出场的 APP 的原因了。\n2. **解压缩 APK，拷贝文件，创建应用的数据目录**：为了加快 APP 的启动速度，APK 在安装的时候，会首先将 APP 的可执行文件 dex 拷贝到 `/data/dalvik-cache` 目录，缓存起来。然后，在 `/data/data/` 目录下创建应用程序的数据目录 (以应用的包名命名)，存放在应用的相关数据，如数据库、XML 文件、Cache、二进制的 so 动态库等。\n3. 解析 APK 的 AndroidManifest.xml 文件。\n\n```java\n    public PackageManagerService(Context context, Installer installer,\n            boolean factoryTest, boolean onlyCore) {\n        // ....\n\n        synchronized (mInstallLock) {\n        synchronized (mPackages) {\n            // Expose private service for system components to use.\n            LocalServices.addService(\n                    PackageManagerInternal.class, new PackageManagerInternalImpl());\n            sUserManager = new UserManagerService(context, this,\n                    new UserDataPreparer(mInsstaller, mInstallLock, mContext, mOnlyCore), mPackages);\n            mPermissionManager = PermissionManagerService.create(context,\n                    new DefaultPermissionGrantedCallback() {\n                        @Override\n                        public void onDefaultRuntimePermissionsGranted(int userId) {\n                            synchronized(mPackages) {\n                                mSettings.onDefaultRuntimePermissionsGrantedLPr(userId);\n                            }\n                        }\n                    }, mPackages /*externalLock*/);\n            mDefaultPermissionPolicy = mPermissionManager.getDefaultPermissionGrantPolicy();\n            mSettings = new Settings(mPermissionManager.getPermissionSettings(), mPackages);\n        }\n        }\n        // ...\n\n        mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context, \"*dexopt*\");\n        DexManager.Listener dexManagerListener = DexLogger.getListener(this, installer, mInstallLock);\n        mDexManager = new DexManager(mContext, this, mPackageDexOptimizer, installer, mInstallLock, dexManagerListener);\n        mArtManagerService = new ArtManagerService(mContext, this, installer, mInstallLock);\n\n        // ...\n\n        synchronized (mInstallLock) {\n        synchronized (mPackages) {\n            // 创建消息\n            mHandlerThread = new ServiceThread(TAG,\n                    Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);\n            mHandlerThread.start();\n            mHandler = new PackageHandler(mHandlerThread.getLooper());\n            // ...\n\n            // 扫描各个目录获取 APK 文件：VENDOR_OVERLAY_DIR           \n            // framework 文件夹：frameworkDir\n            // 系统文件夹：privilegedAppDir systemAppDir\n            // 供应商的包：Environment.getVendorDirectory()\n            // 原始设备制造商的包 ：Environment.getOdmDirectory()\n            // 原始设计商的包：Environment.getOdmDirectory()\n            // 原始产品的包：\n\n            // ....\n\n            mInstallerService = new PackageInstallerService(context, this);\n            final Pair<ComponentName, String> instantAppResolverComponent = getInstantAppResolverLPr();\n            if (instantAppResolverComponent != null) {\n                mInstantAppResolverConnection = new InstantAppResolverConnection(\n                        mContext, instantAppResolverComponent.first,\n                        instantAppResolverComponent.second);\n                mInstantAppResolverSettingsComponent =\n                        getInstantAppResolverSettingsLPr(instantAppResolverComponent.first);\n            } else {\n                mInstantAppResolverConnection = null;\n                mInstantAppResolverSettingsComponent = null;\n            }\n            updateInstantAppInstallerLocked(null);\n\n            final Map<Integer, List<PackageInfo>> userPackages = new HashMap<>();\n            final int[] currentUserIds = UserManagerService.getInstance().getUserIds();\n            for (int userId : currentUserIds) {\n                userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList());\n            }\n            mDexManager.load(userPackages);\n        } // synchronized (mPackages)\n        } // synchronized (mInstallLock)\n\n        // ....\n    }\n```\n\n在构造方法中会扫描多个目录来获取 APK 文件，上述注释中我们已经给出了这些目录，及其获取的方式。当扫描一个路径的时候会使用 `scanDirLI()` 方法来完成扫描工作。\n\n```java\n    private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {\n        final File[] files = scanDir.listFiles();\n        if (ArrayUtils.isEmpty(files)) {\n            return;\n        }\n\n        try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(\n                mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir, mParallelPackageParserCallback)) {\n            int fileCount = 0;\n            for (File file : files) {\n                final boolean isPackage = (isApkFile(file) || file.isDirectory())\n                        && !PackageInstallerService.isStageName(file.getName());\n                if (!isPackage) {\n                    continue;\n                }\n                // 提交文件用来解析\n                parallelPackageParser.submit(file, parseFlags);\n                fileCount++;\n            }\n\n            for (; fileCount > 0; fileCount--) {\n                // 获取解析的结果，即从队列阻塞队列中获取解析的结果\n                ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();\n                // ...\n                if (throwable == null) {\n                    // TODO(toddke): move lower in the scan chain\n                    // Static shared libraries have synthetic package names\n                    if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {\n                        renameStaticSharedLibraryPackage(parseResult.pkg);\n                    }\n                    try {\n                        if (errorCode == PackageManager.INSTALL_SUCCEEDED) {\n                            scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags, currentTime, null);\n                        }\n                    } catch (PackageManagerException e) {\n                        errorCode = e.error;\n                    }\n                }\n                // 。。。\n            }\n        }\n    }\n```\n\n从上面的代码中可以看出，提交文件来解析以及获取解析都是通过 ParallelPackageParser 来完成的。它使用 `submit()` 方法来提交文件用来解析，使用 `take()` 方法获取解析的结果。这两个方法的定义如下，\n\n```java\n    public void submit(File scanFile, int parseFlags) {\n        mService.submit(() -> {\n            ParseResult pr = new ParseResult();\n            try {\n                PackageParser pp = new PackageParser();\n                pp.setSeparateProcesses(mSeparateProcesses);\n                pp.setOnlyCoreApps(mOnlyCore);\n                pp.setDisplayMetrics(mMetrics);\n                pp.setCacheDir(mCacheDir);\n                pp.setCallback(mPackageParserCallback);\n                pr.scanFile = scanFile;\n                pr.pkg = parsePackage(pp, scanFile, parseFlags);\n            } catch (Throwable e) {\n                pr.throwable = e;\n            }\n            try {\n                mQueue.put(pr);\n            } catch (InterruptedException e) {\n                Thread.currentThread().interrupt();\n                mInterruptedInThread = Thread.currentThread().getName();\n            }\n        });\n    }\n\n    public ParseResult take() {\n        try {\n            if (mInterruptedInThread != null) {\n                throw new InterruptedException(\"Interrupted in \" + mInterruptedInThread);\n            }\n            return mQueue.take();\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n            throw new IllegalStateException(e);\n        }\n    }\n```\n\n`submit()` 方法使用一个线程池来执行任务，也就是上面的 mService。它会将要解析的信息封装成 PackageParser 对象，然后把解析的结果信息封装成 ParseResult 放进一个阻塞队列中。当调用 `take()` 方法的时候会从该阻塞队列中获取解析的结果。\n\n包信息的解析最终是通过 PackageParser 的 `parsePackage()` 方法来完成的。其定义如下，\n\n```java\n    public Package parsePackage(File packageFile, int flags, boolean useCaches)\n            throws PackageParserException {\n        Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;\n        if (parsed != null) {\n            return parsed;\n        }\n\n        long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;\n        if (packageFile.isDirectory()) {\n            parsed = parseClusterPackage(packageFile, flags);\n        } else {\n            // 是文件，所以走这条路线\n            parsed = parseMonolithicPackage(packageFile, flags);\n        }\n\n        long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;\n        cacheResult(packageFile, flags, parsed);\n        return parsed;\n    }\n\n```\n\n我们会在这方法中进入到 `parseMonolithicPackage()` 来对文件进行解析。\n\n```java\n    public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {\n        final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);\n        final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags);\n        try {\n            // 解析\n            final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags);\n            pkg.setCodePath(apkFile.getCanonicalPath());\n            pkg.setUse32bitAbi(lite.use32bitAbi);\n            return pkg;\n        } catch (IOException e) {\n            throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION);\n        } finally {\n            IoUtils.closeQuietly(assetLoader);\n        }\n    }\n```\n\n在这个方法中会使用 `parseBaseApk()` 来对 APK 文件进行解析，\n\n```java\n    private Package parseBaseApk(File apkFile, AssetManager assets, int flags)\n            throws PackageParserException {\n        final String apkPath = apkFile.getAbsolutePath();\n\n        String volumeUuid = null;\n        if (apkPath.startsWith(MNT_EXPAND)) {\n            final int end = apkPath.indexOf('/', MNT_EXPAND.length());\n            volumeUuid = apkPath.substring(MNT_EXPAND.length(), end);\n        }\n\n        mParseError = PackageManager.INSTALL_SUCCEEDED;\n        mArchiveSourcePath = apkFile.getAbsolutePath();\n\n        XmlResourceParser parser = null;\n        try {\n            final int cookie = assets.findCookieForPath(apkPath);\n            // 读取 AndroidManifest.xml\n            parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);\n            final Resources res = new Resources(assets, mMetrics, null);\n\n            final String[] outError = new String[1];\n            // 在这里进一步解析 Manifest 的各种信息\n            final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);\n   \n            pkg.setVolumeUuid(volumeUuid);\n            pkg.setApplicationVolumeUuid(volumeUuid);\n            pkg.setBaseCodePath(apkPath);\n            pkg.setSigningDetails(SigningDetails.UNKNOWN);\n            return pkg;\n        } catch (PackageParserException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION);\n        } finally {\n            IoUtils.closeQuietly(parser);\n        }\n    }\n```\n\n这里的 ANDROID_MANIFEST_FILENAME 是一个字符串，这个字符串的定义是 AndroidManifest.xml，所以，我们找到了解析 Manifest 的地方。\n\n然后方法会进入到 `parseBaseApk()` 方法中进一步对 Manifest 进行解析。其读取操作就是基本的 XML 解析的过程。它会使用内部定义的字符串常量从 Manifest 中获取应用的版本还有四大组件等信息。\n\n解析完了 APK 之后会一路经过 return 语句返回到 `scanDirLI()` 方法中，当从阻塞队列中取出 Package 之后将会调用 `scanPackageChildLI()` 在该方法中会将解析的出的 APK 信息缓存到 PMS 中。\n\n这样，在系统启动之后 PMS 就解析了全部的 APK 文件，并将其缓存到了 PMS 中。这样这些应用程序还无法展示给用户，所以需要 Launcher 桌面程序从 PMS 中获取安装包信息并展示到桌面上。\n\n## 2、应用安装的过程\n\n虽然 PMS 用来负责应用的安装和卸载，但是真实的工作却是交给 installd 来实现的。 installd 是在系统启动的时候，由 init 进程解析 init.rc 文件创建的。在早期版本的 Android 中，它使用 Socket 与 Java 层的 Installer 进行通信。在 9.0 的代码中，它使用 Binder 与 Java 层的 Installer 进行通信。当启动 Installd 的时候，将会调用其 main 方法，\n\n```c++\nint main(const int argc, char *argv[]) {\n    return android::installd::installd_main(argc, argv);\n}\n\nstatic int installd_main(const int argc ATTRIBUTE_UNUSED, char *argv[]) {\n    int ret;\n    int selinux_enabled = (is_selinux_enabled() > 0);\n\n    setenv(\"ANDROID_LOG_TAGS\", \"*:v\", 1);\n    android::base::InitLogging(argv);\n\n    SLOGI(\"installd firing up\");\n\n    union selinux_callback cb;\n    cb.func_log = log_callback;\n    selinux_set_callback(SELINUX_CB_LOG, cb);\n\n    // 初始化全局信息\n    if (!initialize_globals()) {\n        exit(1);\n    }\n\n    // 初始化相关目录\n    if (initialize_directories() < 0) {\n        exit(1);\n    }\n\n    if (selinux_enabled && selinux_status_open(true) < 0) {\n        exit(1);\n    }\n\n    if ((ret = InstalldNativeService::start()) != android::OK) {\n        exit(1);\n    }\n\n    // 加入到 Binder 线程池当中\n    IPCThreadState::self()->joinThreadPool();\n\n    LOG(INFO) << \"installd shutting down\";\n\n    return 0;\n}\n```\n\n在启动 Installd 的时候会初始化各种相关的目录，这部分内容就不展开了。然后，它会调用 `IPCThreadState::self()->joinThreadPool()` 一行来将当前线程池加入到 Binder 线程池当中等待通信。\n\n当 Java 层的 Installer 需要与之通信的时候，会调用 `connect()` 方法与之建立联系。其源码如下，这里会通过 ServiceManager 获取 installd 服务，然后将其转换成本地的服务进行 IPC 的调用。\n\n```java\n    private void connect() {\n        // 获取远程服务 \n        IBinder binder = ServiceManager.getService(\"installd\");\n        if (binder != null) {\n            try {\n                binder.linkToDeath(new DeathRecipient() {\n                    @Override\n                    public void binderDied() {\n                        connect();\n                    }\n                }, 0);\n            } catch (RemoteException e) {\n                binder = null;\n            }\n        }\n\n        if (binder != null) {\n            // 转成本地服务进行 IPC 调用\n            mInstalld = IInstalld.Stub.asInterface(binder);\n            try {\n                invalidateMounts();\n            } catch (InstallerException ignored) {\n            }\n        } else {\n            // 重连\n            BackgroundThread.getHandler().postDelayed(() -> {\n                connect();\n            }, DateUtils.SECOND_IN_MILLIS);\n        }\n    }\n```\n\nInstaller 与 PMC 类似，也是一种系统服务，它的启动的时刻与 PMS 基本一致，位于同一个方法中，并且其启动时刻位于 PMS 之前。\n\n## 2、从 ADB 安装的过程\n\n\n\n\n"
  },
  {
    "path": "系统架构/Android打包过程.md",
    "content": "# Android 打包过程\n\n![打包过程](https://upload-images.jianshu.io/upload_images/1441907-8a2c24bbb71c2cbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/536/format/webp)\n\n## 编译资源\n\naapt 类目录：`base\\tools\\aapt` 和 `base\\tools\\aapt2`\n\naapt 工具目录：`sdk/build-tools/27.0.3/aapt.exe` 和 `sdk/build-tools/27.0.3/aapt2.exe`\n\n## AIDL\n\n工具目录：`sdk/build-tools/27.0.3/aidl.exe`\n\n## DX\n\n工具目录：`sdk/build-tools/27.0.3/lib/dx.jar`\n工具目录：`sdk/build-tools/27.0.3/dx.bat`\n\n## 签名\n\n工具目录：`sdk/build-tools/27.0.3/lib/apksigner.jar`\n工具目录：`sdk/build-tools/27.0.3/apksigner.bat`\n\n遍历 apk 中所有文件，对非文件夹非签名文件的文件逐个生成 SHA1 数字签名信息，再 base64 编码 然后再写入 MANIFEST.MF 文件中\n\nSHA1 生成的摘要信息，如果你修改了某个文件，apk 安装校验时，取到的该文件的摘要与 MANIFEST.MF 中对应的摘要不同，则安装不成功\n\n接下来对之前生成的 manifest 使用 SHA1withRSA 算法， 用私钥签名，writeSignatureFile 这个函数，最后生成 CERT.SF 文件\n\nhttps://blog.csdn.net/tencent_bugly/article/details/51424209\n\n## 对齐\n\n它对apk中未压缩的数据进行4字节对齐，对齐后就可以使用mmap函数读取文件，可以像读取内存一样对普通文件进行操作。如果没有4字节对齐，就必须显式的读取，这样比较缓慢并且会耗费额外的内存。\n\n工具位于 `sdk\\build-tools\\27.0.3`\n\nhttps://www.jianshu.com/p/7c288a17cda8"
  },
  {
    "path": "系统架构/Android系统启动过程.md",
    "content": "# Android 系统启动过程\n\n现在我们来梳理下 Android 系统的启动过程。Android 启动过程还是比较重要的，因为在这个过程中除了要完成 Linux 系统的初始化工作还要完成 Android 的基础服务和启动界面的初始化工作。\n\n在这篇文章中，我们不打算过多深入源码。因为 Android 中任何一个功能模块在 Framework 层都涉及大量的代码调用。过多深入源码只会让我们迷失在一层层的调用栈中。相比之下，我更倾向于只出一些核心代码，另外梳理下调用栈的流程。当我们需要深入研究这方面的内容的时候，知道去哪里找答案就够了。\n\n## 1、系统启动\n\n按下电源之后，首先加载引导程序 BootLoader 到 RAM；然后，执行引导程序 BootLoader 以把系统 OS 拉起来；接着，启动 Linux 内核；内核中启动的第一个用户进程是 init 进程，init 进程会通过解析 init.rc 来启动 zygote 服务；Zygote 又会进一步的启动 SystemServer；在 SystemServer 中，Android 会启动一系列的系统服务供用户调用。\n\nAndroid 系统中 init 程序对应的 `Android.mk` 位于 `system/core/init/Android.mk`，是一种 Makefile 文件，用来向编译系统描述我们的源代码。我们可以使用 make 工具来执行该文件。所以，mk 文件就像是 Shell 脚本一样。\n\n### 1.1 执行 init 程序\n\nLinux 内核加载完成后，首先启动 init 进程。在 8.0 的源码中系统启动的第一个阶段是创建启动所需的各种目录。而在最新的源码中，这部分代码被包含在了 `init_first_stage` 中：\n\n```C++\n    // platform/system/core/init/init_first_stage.cpp\n    int main(int argc, char** argv) {\n        if (REBOOT_BOOTLOADER_ON_PANIC) {\n            InstallRebootSignalHandlers();\n        }\n        boot_clock::time_point start_time = boot_clock::now();\n        std::vector<std::pair<std::string, int>> errors;\n    #define CHECKCALL(x) \\\n        if (x != 0) errors.emplace_back(#x \" failed\", errno);\n        umask(0);\n        CHECKCALL(clearenv());\n        CHECKCALL(setenv(\"PATH\", _PATH_DEFPATH, 1));\n        // 创建目录\n        CHECKCALL(mount(\"tmpfs\", \"/dev\", \"tmpfs\", MS_NOSUID, \"mode=0755\"));\n        CHECKCALL(mkdir(\"/dev/pts\", 0755));\n        CHECKCALL(mkdir(\"/dev/socket\", 0755));\n        // ...\n    #undef CHECKCALL\n        auto reboot_bootloader = [](const char*) { RebootSystem(ANDROID_RB_RESTART2, \"bootloader\"); };\n        InitKernelLogging(argv, reboot_bootloader);\n        // ...\n        const char* path = \"/system/bin/init\";\n        const char* args[] = {path, nullptr};\n        execv(path, const_cast<char**>(args));\n        return 1;\n    }\n```\n\n在系统启动过程中会多次调用 `execv()`，每次调用该方法时会重新执行 main() 方法。该方法如下：\n\n```\nint execv(const char *progname, char *const argv[]);   //#include <unistd.h>\n```\n\nexecv() 会停止执行当前的进程，并且以 progname 应用进程替换被停止执行的进程，进程 ID 不会改变。\n\n然后是 `init.cpp` 进程的入口函数 main:\n\n```C++\n    // platform/system/core/init/init.cpp\n    int main(int argc, char** argv) {\n        if (!strcmp(basename(argv[0]), \"ueventd\")) {\n            return ueventd_main(argc, argv);\n        }\n        if (argc > 1 && !strcmp(argv[1], \"subcontext\")) {\n            android::base::InitLogging(argv, &android::base::KernelLogger);\n            const BuiltinFunctionMap function_map;\n            return SubcontextMain(argc, argv, &function_map);\n        }\n        if (REBOOT_BOOTLOADER_ON_PANIC) {\n            // 初始化重启系统的处理信号\n            InstallRebootSignalHandlers();\n        }\n        // ...\n        property_init(); // 初始化属性服务\n        // ...\n        Epoll epoll; // 创建 epoll 句柄\n        if (auto result = epoll.Open(); !result) {\n            PLOG(FATAL) << result.error();\n        }\n\n        InstallSignalFdHandler(&epoll);\n        // ...\n        StartPropertyService(&epoll); // 启动属性服务\n        // ...\n\n        ActionManager& am = ActionManager::GetInstance();\n        ServiceList& sm = ServiceList::GetInstance();\n\n        LoadBootScripts(am, sm); // 加载启动脚本\n        // ...\n        // 充电模式不启动系统，否则启动系统\n        std::string bootmode = GetProperty(\"ro.bootmode\", \"\");\n        if (bootmode == \"charger\") {\n            am.QueueEventTrigger(\"charger\");\n        } else {\n            am.QueueEventTrigger(\"late-init\");\n        }\n        // ...\n        return 0;\n    }\n```\n\n这里会在 `LoadBootScripts()` 方法中解析 `init.rc` 文件。关于该文件指令的含义可以参考 AOSP 中的文档：[《Android Init Language》](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/master/init/). 完成解析相关的类是 `ActionManager`、`Parser` 和 `XXParser`，均位于 `system/core/init` 目录下面。除此之外，还有 `Action` 和 `Service` 等类。它们的作用是，各种 `Parser` 用来解析 rc 文件中的指令。解析出的指令会被封装成 `Action` 和 `Service` 等对象。\n\n打开该文件我们可以看到其中包含了下面两行代码，这里使用了占位符，也就是说，它会根据当前的环境变量加载当前目录下对应的文件。并且，我们可以看到在 `system/core/rootdir` 目录下面确实存在着 `init.zygote64.rc` 和 `init.zygote32.rc` 等文件。\n\n```c\nimport /init.${ro.hardware}.rc\nimport /init.${ro.zygote}.rc\n```\n\n以 `rinit.zygote64.rc` 为例，它表示通知 init 进程创建名为 zygote 的进程。执行路径是 `/system/bin/app_process64`，\n\n```c\n// platform/system/core/rootdir/init.zygote64.rc\nservice zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server\n    class main\n    // ...\n```\n\n我们可以看出它使用了 service 指令，所以它将被解析成 Service. \n\n注意到在 `init.cpp` 的 main() 方法的最后，如果非充电模式将触发 `late-init`. 在 `rc` 中配置了对 `late-init` 事件的监听，通过 `on` 来实现的。同时，它又使用 `trigger` 触发了其他的命令。这些命令也都是通过 `on` 来监听的。（当然，rc 只是一种配置文件，而实际的逻辑是被解析之后在程序中完成的。）\n\n在 `late-init` 事件触发的事件当中包含了 `zygote-start` 事件. 而 `zygote-start` 监听实现又根据监听条件又多种。不过，它们都会调用 `start zygote` 方法。这里的 start 会被映射到 builtins 类的 `do_start()` 方法。该方法会调用 Service 的 `start()` 方法。该方法主要是调用 clone 或 fork 创建子进程，然后调用 execve 执行配置的二进制文件，另外根据之前在 rc 文件中的配置，去执行这些配置。因此程序将开始执行 app_process64. \n\n```c++\n// platform/system/core/init/service.cpp\nResult<Success> Service::Start() {\n    // ...\n    pid_t pid = -1;\n    if (namespace_flags_) {\n        pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr);\n    } else {\n        pid = fork();\n    }\n\n    if (pid == 0) {\n        umask(077);\n        // ...\n        // 内部调用 execv() 来执行\n        if (!ExpandArgsAndExecv(args_, sigstop_)) {\n            PLOG(ERROR) << \"cannot execve('\" << args_[0] << \"')\";\n        }\n        _exit(127);\n    }\n    // ...\n    return Success();\n}\n```\n\n> 映射关系参考源码：system/core/init/builtins.cpp    \n> 关于 rc 文件的命令的解析，可以参考[《Android 8.0 系统启动流程之init.rc解析与service流程(七)》](https://blog.csdn.net/marshal_zsx/article/details/80600622)\n\n上述 rc 文件的 `/system/bin/app_process64` 对应的 mk 文件位于 `/base/cmds/app_process/Android.mk` 目录下面。从该文件中我们可以看出，不论 app_process、app_process32 还是 app_process64，对应的源文件都是 `app_main.cpp`. 于是程序将进入 `app_main.cpp` 的 main() 方法。\n\n进入 main() 方法之后先要进行指令的参数的解析，\n\n```c++\n// platform/frameworks/base/cmds/app_process/app_main.cpp\nint main(int argc, char* const argv[])\n{\n    // ...\n    bool zygote = false;\n    bool startSystemServer = false;\n    bool application = false;\n    String8 niceName;\n    String8 className;\n\n    ++i;  // Skip unused \"parent dir\" argument.\n    while (i < argc) {\n        const char* arg = argv[i++];\n        if (strcmp(arg, \"--zygote\") == 0) {\n            zygote = true;\n            niceName = ZYGOTE_NICE_NAME;\n        } else if (strcmp(arg, \"--start-system-server\") == 0) {\n            startSystemServer = true;\n        } else if (strcmp(arg, \"--application\") == 0) {\n            application = true;\n        } else if (strncmp(arg, \"--nice-name=\", 12) == 0) {\n            niceName.setTo(arg + 12);\n        } else if (strncmp(arg, \"--\", 2) != 0) {\n            className.setTo(arg);\n            break;\n        } else {\n            --i;\n            break;\n        }\n    }\n    // ...\n    if (zygote) {\n        runtime.start(\"com.android.internal.os.ZygoteInit\", args, zygote);\n    } else if (className) {\n        runtime.start(\"com.android.internal.os.RuntimeInit\", args, zygote);\n    } else {\n        app_usage();\n    }\n}\n```\n\n我们从之前的 rc 文件中可以看出，参数为 `--zygote`，因此将调用 `ZygoteInit` 的 main() 方法继续执行。**这里的 runtime 是 `AndroidRuntime`，这里的 `start()` 方法是一种 JNI 调用。这里将会调用 Java 中的静态 main() 方法继续执行。** 这种调用方式还是比较重要的，我们经常在 Java 中调用 C++ 的方法，而这里是在 C++ 中调用 Java 的方法。它的源码位于 `base\\core\\jni\\AndroidRuntime.cpp`. \n\n```c++\n// platform/frameworks/base/core/jni/AndroidRuntime.cpp\nvoid AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)\n{\n    // ...\n\n    // 获取ANDROID_ROOT环境变量\n    const char* rootDir = getenv(\"ANDROID_ROOT\");\n    if (rootDir == NULL) {\n        rootDir = \"/system\";\n        if (!hasDir(\"/system\")) {\n            return;\n        }\n        setenv(\"ANDROID_ROOT\", rootDir, 1);\n    }\n\n    // 启动虚拟机\n    JniInvocation jni_invocation;\n    jni_invocation.Init(NULL);\n    JNIEnv* env;\n    if (startVm(&mJavaVM, &env, zygote) != 0) {\n        return;\n    }\n    onVmCreated(env);\n\n    // ... 解析 main 函数以在下面进行触发\n\n    // 启动线程，当前线程将会变成虚拟机的主线程，并且直到虚拟机退出的时候才结束。\n    char* slashClassName = toSlashClassName(className != NULL ? className : \"\");\n    jclass startClass = env->FindClass(slashClassName);\n    if (startClass == NULL) {\n        ALOGE(\"JavaVM unable to locate class '%s'\\n\", slashClassName);\n    } else {\n        jmethodID startMeth = env->GetStaticMethodID(startClass, \"main\",\n            \"([Ljava/lang/String;)V\");\n        if (startMeth == NULL) {\n            ALOGE(\"JavaVM unable to find main() in '%s'\\n\", className);\n        } else {\n            env->CallStaticVoidMethod(startClass, startMeth, strArray);\n        }\n    }\n    // ...\n}\n```\n\n在上面的方法中，我们可以看出启动虚拟机的时候需要调用 `startVM()` 方法来启动。当虚拟机启动完成之后使用句柄函数 env 来执行 ZygoteInit 的静态 `main()` 方法。\n\n### 1.2 启动 Zygote\n\n根据上面的分析，系统已经启动了虚拟机。并且在虚拟机启动完成之后，程序进入了 `ZygoteInit` 中 `main()` 方法中，\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/ZygoteInit.java\n    public static void main(String argv[]) {\n        // ...\n        try {\n            // ...\n            boolean startSystemServer = false;\n            String socketName = \"zygote\";\n            String abiList = null;\n            boolean enableLazyPreload = false;\n            for (int i = 1; i < argv.length; i++) {\n                if (\"start-system-server\".equals(argv[i])) {\n                    startSystemServer = true;\n                } else if (\"--enable-lazy-preload\".equals(argv[i])) {\n                    enableLazyPreload = true;\n                } else if (argv[i].startsWith(ABI_LIST_ARG)) {\n                    abiList = argv[i].substring(ABI_LIST_ARG.length());\n                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {\n                    socketName = argv[i].substring(SOCKET_NAME_ARG.length());\n                } else {\n                    throw new RuntimeException(\"Unknown command line argument: \" + argv[i]);\n                }\n            }\n\n            // 注册名为 zygote 的 Socket\n            zygoteServer.registerServerSocketFromEnv(socketName);\n            // 决定是否进行资源的预加载\n            if (!enableLazyPreload) {\n                // ... 记录日志信息\n                preload(bootTimingsTraceLog);\n                // ... 记录日志信息\n            } else {\n                Zygote.resetNicePriority();\n            }\n\n            gcAndFinalize(); // 进行 GC 清理空间\n\n            // ...\n\n            if (startSystemServer) {\n                // 启动 SystemServer 进程，如果 r 为 null 则处于父进程，否则是子进程\n                Runnable r = forkSystemServer(abiList, socketName, zygoteServer);\n                if (r != null) {\n                    r.run();\n                    return;\n                }\n            }\n\n            // 等待 AMS 连接请求\n            caller = zygoteServer.runSelectLoop(abiList);\n        } catch (Throwable ex) {\n            throw ex;\n        } finally {\n            zygoteServer.closeServerSocket();\n        }\n\n        if (caller != null) {\n            caller.run();\n        }\n    }\n```\n这里主要做了几件事情：\n\n首先，创建 Server 端的 Socket. 这里创建的是 ZygoteServer 对象。它提供了等待 UNIX 套接字的命令，并且提供了 fork 虚拟机的方法。\n\n然后，进行资源预加载。\n\n接着，启动 SystemServer. 这里通过调用 forkSystemServer() 来进行。这里先会构建一个命令参数，然后调用 Zygote 的静态方法来 Fork 一个子进程。该方法内部又会调用 JNI 层的 `nativeForkSystemServer` 方法最终完成 Fork 操作。\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/Zygote.java\n    private static Runnable forkSystemServer(String abiList, String socketName, ZygoteServer zygoteServer) {\n        // ...\n\n        /* 硬编码的命令行来启动 System Server */\n        String args[] = {\n            \"--setuid=1000\",\n            \"--setgid=1000\",\n            \"--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1024,1032,1065,3001,3002,3003,3006,3007,3009,3010\",\n            \"--capabilities=\" + capabilities + \",\" + capabilities,\n            \"--nice-name=system_server\",\n            \"--runtime-args\",\n            \"--target-sdk-version=\" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT,\n            \"com.android.server.SystemServer\",\n        };\n\n        ZygoteConnection.Arguments parsedArgs = null;\n        int pid;\n        try {\n            parsedArgs = new ZygoteConnection.Arguments(args);\n            ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);\n            ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);\n\n            boolean profileSystemServer = SystemProperties.getBoolean(\n                    \"dalvik.vm.profilesystemserver\", false);\n            if (profileSystemServer) {\n                parsedArgs.runtimeFlags |= Zygote.PROFILE_SYSTEM_SERVER;\n            }\n\n            /* 请求 fork System Server 进程 */\n            pid = Zygote.forkSystemServer(\n                    parsedArgs.uid, parsedArgs.gid,\n                    parsedArgs.gids,\n                    parsedArgs.runtimeFlags,\n                    null,\n                    parsedArgs.permittedCapabilities,\n                    parsedArgs.effectiveCapabilities);\n        } catch (IllegalArgumentException ex) {\n            throw new RuntimeException(ex);\n        }\n\n        /* 对于子进行进行处理 */\n        if (pid == 0) {\n            if (hasSecondZygote(abiList)) {\n                waitForSecondaryZygote(socketName);\n            }\n\n            zygoteServer.closeServerSocket();\n            // 为新 fork 的 system server 进程停止剩下的工作\n            return handleSystemServerProcess(parsedArgs);\n        }\n\n        return null;\n    }\n```\n\n最后启动 select 循环，等待新的连接。下面是这个方法的定义，代码中的注释已经比较全了，我们就不多解释了。\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/ZygoteServer.java\n    Runnable runSelectLoop(String abiList) {\n        // ...\n        while (true) { // 使用无限循环进行监听\n            // ...\n            for (int i = pollFds.length - 1; i >= 0; --i) {\n                if ((pollFds[i].revents & POLLIN) == 0) {\n                    continue;\n                }\n                if (i == 0) { // 遍历到最后一个\n                    ZygoteConnection newPeer = acceptCommandPeer(abiList);\n                    peers.add(newPeer);\n                    fds.add(newPeer.getFileDesciptor());\n                } else { // 正在等待连接\n                    try {\n                        ZygoteConnection connection = peers.get(i);\n                         // processOneCommand() 从命令 socket 中读取一个命令，如果读取成功，将会fork子进程，并返回子进程的 main 方法. 如果是父进程，那么应该始终返回 null\n                        final Runnable command = connection.processOneCommand(this);\n                        if (mIsForkChild) {\n                            // 子进程，需要至少一个命令\n                            if (command == null) {\n                                throw new IllegalStateException(\"command == null\");\n                            }\n                            return command;\n                        } else {\n                            // server 进程，不应该存在要执行的命令\n                            if (command != null) {\n                                throw new IllegalStateException(\"command != null\");\n                            }\n                            if (connection.isClosedByPeer()) { // 关闭请求\n                                connection.closeSocket();\n                                peers.remove(i);\n                                fds.remove(i);\n                            }\n                        }\n                    } catch (Exception e) {\n                        if (!mIsForkChild) {\n                            // 中间发生错误，关闭请求，告知请求端请求结束\n                            ZygoteConnection conn = peers.remove(i);\n                            conn.closeSocket();\n                            fds.remove(i);\n                        } else {\n                            throw e;\n                        }\n                    } finally {\n                        mIsForkChild = false;\n                    }\n                }\n            }\n        }\n    }\n```\n\n当使用 `acceptCommandPeer()` 从 socket 中读取到了命令之后，会 fork 子进程并返回一个 Runnable，用来启动子进程的 main() 方法。这部分逻辑在 `acceptCommandPeer()` 方法中。它会调用 Zygote 类的静态方法 `forkAndSpecialize()` 来创建子进程。（与 SystemServer 进程创建时的静态方法不同）然后将调用 `handleChildProc()` 方法返回用来启动子进程的 main() 方法。其定义如下，\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/ZygoteConnection.java\n    private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,\n            FileDescriptor pipeFd, boolean isZygote) {\n        // ...\n        if (parsedArgs.invokeWith != null) {\n            throw new IllegalStateException(\"WrapperInit.execApplication unexpectedly returned\");\n        } else {\n            if (!isZygote) {\n                return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,\n                        null /* classLoader */);\n            } else {\n                return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,\n                        parsedArgs.remainingArgs, null /* classLoader */);\n            }\n        }\n    }\n```\n\n这里的 isZygote 的含义是，是否以当前进程的子进程的形式来启动一个进程，使用 `--start-child-zygote` 参数来指定。因为当前我们启动的进程是父 Zygote 进程，所以将会调用 `ZygoteInit.zygoteInit()` 方法继续处理。该方法的核心代码只有两行，\n\n```java\n    // platform/framework/base/core/java/com/android/internal/os/ZygoteInit.java\n    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {\n        // ...\n        ZygoteInit.nativeZygoteInit();\n        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);\n    }\n```\n\n`nativeZygoteInit()` 是一个 native 方法，用来启动 Binder 线程池。它对应的 native 方法定义在 `AndroidRuntime.cpp` 中。这里的 `gCurRuntime` 是 AppRumtime，定义在 `app_main.cpp` 中。\n\n```c++\n// platform/frameworks/base/base/core/jni/AndroidRuntime.cppp\nstatic void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)\n{\n    gCurRuntime->onZygoteInit();\n}\n\n// latform/frameworks/base/cmds/app_process/app_main.cpp\nclass AppRuntime : public AndroidRuntime\n{\n    virtual void onZygoteInit()\n    {\n        sp<ProcessState> proc = ProcessState::self();\n        proc->startThreadPool();\n    }\n}\n```\n\n`applicationInit()` 方法主要用来触发 SystemServer 的 main() 方法。在最新的代码中，会将要触发的方法和参数封装到一个 Runnable 中，并在它的 `run()` 方法中调用反射触发方法。所以，我们将进入 SystemServer 的 `main()` 方法。该类位于 `base\\services\\java\\com\\android\\server` 下面。其方法定义如下，\n\n```java\n    // platform/frameworks/base/service/java/com/android/server/SystemServer.java\n    public static void main(String[] args) {\n        new SystemServer().run();\n    }\n\n    // platform/frameworks/base/service/java/com/android/server/SystemServer.java\n    private void run() {\n        try {\n            // ...\n\n            Looper.prepareMainLooper(); // 创建主线程消息循环\n            System.loadLibrary(\"android_servers\"); // 加载 so 库\n            performPendingShutdown();\n            // 创建系统的 context\n            createSystemContext();\n            // ServiceManager!!! 用来管理系统服务中的服务的创建、启动等生命周期\n            mSystemServiceManager = new SystemServiceManager(mSystemContext);\n            mSystemServiceManager.setStartInfo(mRuntimeRestart,\n                    mRuntimeStartElapsedTime, mRuntimeStartUptime);\n            LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);\n            SystemServerInitThreadPool.get();\n        } finally {\n            traceEnd();  // InitBeforeStartServices\n        }\n\n        // 启动服务\n        try {\n            traceBeginAndSlog(\"StartServices\");\n            startBootstrapServices(); // 启动引导服务\n            startCoreServices(); // 启动核心服务\n            startOtherServices(); // 启动其他服务\n            SystemServerInitThreadPool.shutdown();\n        } catch (Throwable ex) {\n            throw ex;\n        } finally {\n            traceEnd();\n        }\n        // ...\n        Looper.loop();\n        throw new RuntimeException(\"Main thread loop unexpectedly exited\");\n    }\n```\n\n从上面可以看出，这个方法中的主要逻辑是对系统中各种服务进行管理。创建了 `SystemServiceManager` 之后，借助它来实现对各种服务的创建、启动等生命周期进行管理。比如在 `startBootstrapServices()` 中会启动大名鼎鼎的 PMS 和 AMS 等. 启动服务的操作是通过调用 `SystemServiceManager` 的 `startService()` 方法完成的。该方法有 3 个重载的方法。但是，不论调用哪个方法，最终都会调用到下面的方法。\n\n```java\n    // platform/frameworks/base/services/core/com/android/server/SystemServiceManager.java\n    public void startService(@NonNull final SystemService service) {\n        mServices.add(service);\n        long time = SystemClock.elapsedRealtime();\n        try {\n            service.onStart();\n        } catch (RuntimeException ex) {\n            throw new RuntimeException(\"Failed to start service \" + service.getClass().getName()\n                    + \": onStart threw an exception\", ex);\n        }\n    }\n```\n\n在该方法中除了回调 service 的 `onStart()` 之外，还要将其注册到 `mServices` 中，它是 `ArrayList<SystemService>` 类型的变量，用来存储启动的服务。\n\n此外，我们还注意到在 `run()` 方法中启动了一个 Looper 循环。这表明该系统服务主线程将会一直运行下去。关于 Looper 的内容可以参考我的另一篇文章：\n\n[《Android 消息机制：Handler、MessageQueue 和 Looper》](https://blog.csdn.net/github_35186068/article/details/83718379)\n\n### 1.3 启动 Launcher\n\n系统启动过程中必不可少的一个环节就是启动 Launcher，就是所谓的 Android 桌面程序。在上面的方法中，系统会启动所需的各种服务，在其中的 `startOtherServices()` 方法中，会调用启动的服务的 `systemReady()` 方法来做系统启动准备就绪之后的逻辑。这其中就包括 AMS.  `startOtherServices()` 方法比较长，我们就不贴代码了。我们直接看下 AMS 的  `systemReady()` 方法。这个方法也比较长，我们只截取其中的一部分方法，\n\n```java\n    // platform/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java\n    public void systemReady(final Runnable goingCallback, TimingsTraceLog traceLog) {\n        // ...\n        synchronized (this) {\n            // ...\n            startHomeActivityLocked(currentUserId, \"`\");\n            // ...\n        }\n    }\n```\n\n这里会调用 `startHomeActivityLocked()` 方法来继续操作以完成桌面的启动，\n\n```java\n    // platform/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java\n    boolean startHomeActivityLocked(int userId, String reason) {\n        // ...\n        // 构建一个用于启动桌面程序的 Intent，这个 Intent 包含一个 Category android.intent.category.HOME 类型的 Cateogry\n        Intent intent = getHomeIntent();\n        // 遍历安装包检查是否存在 Cateogry 为 android.intent.category.HOME 的 Activity\n        ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);\n        if (aInfo != null) {\n            // 将上述得到的应用信息传递给 Intent\n            intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));\n            aInfo = new ActivityInfo(aInfo);\n            aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);\n            ProcessRecord app = getProcessRecordLocked(aInfo.processName,\n                    aInfo.applicationInfo.uid, true);\n            if (app == null || app.instr == null) {\n                intent.setFlags(intent.getFlags() | FLAG_ACTIVITY_NEW_TASK);\n                final int resolvedUserId = UserHandle.getUserId(aInfo.applicationInfo.uid);\n\n                final String myReason = reason + \":\" + userId + \":\" + resolvedUserId;\n                // 继续启动 Launcher 的进程\n                mActivityStartController.startHomeActivity(intent, aInfo, myReason);\n            }\n        }\n        return true;\n    }\n```\n\n然后方法将进入 ActivityStartController 的 `startHomeActivity()` 方法继续进行，\n\n```java\n    // platform/frameworks/base/services/core/java/com/android/server/am/ActivityStartController.java\n    void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason) {\n        // 把 Launcher 的堆栈移到顶部\n        mSupervisor.moveHomeStackTaskToTop(reason);\n        // obtainStarter() 将返回一个 ActivityStarter，然后调用它的 execute() 继续处理\n        mLastHomeActivityStartResult = obtainStarter(intent, \"startHomeActivity: \" + reason)\n                .setOutActivity(tmpOutRecord)\n                .setCallingUid(0)\n                .setActivityInfo(aInfo)\n                .execute();\n        mLastHomeActivityStartRecord = tmpOutRecord[0];\n        if (mSupervisor.inResumeTopActivity) {\n            mSupervisor.scheduleResumeTopActivities();\n        }\n    }\n```\n\n这里通过 `obtainStarter()` 将返回一个 ActivityStarter，然后调用它的 execute() 继续处理，显然这里使用的是构建者设计模式。剩下的流程就是 Activity 的启动流程。我们不做更多说明了，可以在随后介绍 Activity 启动的时候来继续梳理。\n\n## 2、总结\n\n上面我们梳理了 Android 系统启动的主流程，这里我们总结一下。\n\n![系统启动流程](res/launcher.png)\n\n### 推荐资料：\n\n1. [Android 8.0 系统启动流程之zygote进程(八)](https://blog.csdn.net/marshal_zsx/article/details/80547780)\n2. [Android 8.0 系统启动流程之init.rc解析与service流程(七)](https://blog.csdn.net/marshal_zsx/article/details/80600622)"
  },
  {
    "path": "系统架构/Android系统架构.md",
    "content": "# Android 系统架构\n\n如下所示的时Andriod系统的架构图：\n\n![Andriod系统架构图](res/Andriod系统架构图.jpg)\n\n总结：\n\n*Android 架构 = 应用部分 + 核心部分 + 底层部分*\n\n1. *应用部分 = 系统应用 + 第三方应用*\n\n2. *核心部分 = 框架层 + 核心类库 + 运行时*\n    1. 框架层：Java 编写，可以称为 Java 框架层，提供应用开发所需要的API；\n    2. 核心类库：Android 的库文件，如数据库、浏览器和 OpenGL 等；\n    3. 运行时：由 Java 核心库和 Dalvik 虚拟机组成。\n\n3. *底层部分 = 硬件抽象层 + Linux内核*\n    1. 硬件抽象层：位于操作系统内核与硬件电路之间的接口层，目的在于将硬件抽象化；\n    2. Linux 内核：在 Linux 内核之上增加了 Android 驱动，系统的安全、内存、进程和网络等的管理都依赖于它。\n\n"
  },
  {
    "path": "系统架构/SurefaceView_and_TextureView.md",
    "content": "# Android：解析 SurefaceView & TextureView\n\n## 1、关于 SurefaceView 和 TextureView\n\n### 1.1 基础\n\n[SurfaceView](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/SurfaceView.java) 以及 [TextureView](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/TextureView.java) 均继承于 `android.view.View`，属于 Android 提供的控件体系的一部分。与普通 View 不同，它们都在独立的线程中绘制和渲染。所以，相比于普通的 ImageView 它们的性能更高，因此常被用在对绘制的速率要求比较高的应用场景中，用来解决普通 View 因为绘制的时间延迟而带来的掉帧的问题，比如用作相机预览、视频播放的媒介等。\n\n相比于普通的 View，SurfaceView 有以下几点优势：\n\n1. SurfaceView 适用于主动刷新，普通的 View 无法进行主动刷新；\n2. SurfaceView 通过子线程来更新画面，而普通的 View 需要在主线程中更新画面；\n3. 最后就是缓冲的问题，普通的 View 不存在缓存机制，而 SurfaceView 存在缓冲机制。\n\n### 1.2 两种控件的基础使用\n\n#### 1.2.1 TextureView 的使用\n\nTextureView 在 `API 14` 中引入，用来展示流，比如视频和 OpenGL 等的流。这些流可以来自应用进程或者是跨进程的。它只能用在开启了硬件加速的窗口，否则无法绘制任何内容。与 SurefaceView 不同，TextureView 不会创建一个独立的窗口，而是像一个普通的 View 一样。这种区别使得 TextureView 可以移动、转换和做动画等，比如你可以使用 TextureView 的 setAlpha() 方法将其设置成半透明的。\n\nTextureView 的使用非常简单，你只需要获取到它的 SurfaceTexture. 然后就可以使用它来渲染。下面的示例说明了如何使用 TextureView 作为相机的预览控件，\n\n```kotlin\nclass TextureViewActivity : CommonActivity<ActivityTextureViewBinding>(), TextureView.SurfaceTextureListener {\n\n    private lateinit var camera: Camera\n    private lateinit var textureView: TextureView\n\n    override fun getLayoutResId(): Int = R.layout.activity_texture_view\n\n    override fun doCreateView(savedInstanceState: Bundle?) {\n        textureView = TextureView(this)\n        // Add callback to listen the lifecycle callback for TextureView\n        textureView.surfaceTextureListener = this\n        binding.cl.addView(textureView)\n    }\n\n    override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {\n        camera = Camera.open()\n        // Add the surface texture to camera\n        camera.setPreviewTexture(surface)\n        camera.startPreview()\n    }\n\n    override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {\n        // Ignored, Camera does all the work for us\n    }\n\n    override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {\n        // Invoked every time there's a new Camera preview frame\n    }\n\n    override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {\n        // Release everything when texture destroyed\n        camera.stopPreview()\n        camera.release()\n        return true\n    }\n}\n```\n\nTextureView 的 SurfaceTexture 可以通过 `getSurfaceTexture()` 方法或者通过 SurfaceTextureListener 获取到。还有一点很重要的是，SurfaceTexture 只在 TextureView 关联到窗口并且 `onAttachedToWindow()` 被触发的之后可用。因此，强烈建议使用监听的方式来获取 SurfaceTexture 可用的通知。\n\n最后还有一个重要的就是，在同一时刻只能由一个生产者可以使用 TextureView，就是说，当你使用 TextureView 作为相机预览的时候是无法使用 `lockCanvas()` 同时在 TextureView 上面进行绘制的。\n\n除了只能在开启了硬件加速的窗口中使用，TextureView 消费的内存要比 SurfaceView 要多，并伴随着 1-3 帧的延迟。\n\n#### 1.2.2 SurefaceView 的使用\n\nSurfaceView 也用来解决页面刷新频繁的问题，它提供了嵌入在视图层次结构内的专用绘图图层 (Surface)。我们可以用它来控制曲面的格式和大小以及在屏幕上的位置。图层 (Surface) 处于 Z 轴，位于持有 SurfaceView 的窗口之后。SurfaceView 在窗口上开了一个透明的 “洞” 以展示图面。Surface 的排版显示受到视图层级关系的影响，它的兄弟视图结点会在顶端显示。这意味者 Surface 的内容会被它的兄弟视图遮挡，这一特性可以用来放置遮盖物(overlays)(例如，文本和按钮等控件)。注意，如果 Surface 上面有透明控件，那么每次 Surface 变化都会引起框架重新计算它和顶层控件的透明效果，这会影响性能。\n\n可以通过 SurfaceView 的 `getHolder()` 方法获取图层 (Surface)，它通过接口 SurfaceHolder 提供。当 SurfaceView 所在的窗口可见的时候，图层 (Surface) 会被创建。你可以通过实现 `SurfaceHolder.Callback.surfaceCreated(SurfaceHolder) ` 和 ` SurfaceHolder.Callback.surfaceDestroyed(SurfaceHolder)` 监听 Surface 的创建和销毁事件，并且只能在这两个方法之间对图层 (Surface) 进行操作。 SurfaceView 和 SurfaceHolder.Callback 的所有方法都会被主线程调用，所以当在子线程中进行绘制的时候，必须妥善进行线程的同步。\n\n```kotlin\nclass SurfaceViewActivity : CommonActivity<ActivitySurfaceViewBinding>(), SurfaceHolder.Callback {\n\n    private lateinit var camera: Camera\n    private lateinit var surfaceView: SurfaceView\n    private lateinit var holder: SurfaceHolder\n\n    override fun getLayoutResId(): Int = R.layout.activity_surface_view\n\n    override fun doCreateView(savedInstanceState: Bundle?) {\n        surfaceView = SurfaceView(this)\n        // Add callback to listen the lifecycle callback for SurfaceView\n        holder = surfaceView.holder\n        holder.addCallback(this)\n        binding.cl.addView(surfaceView)\n    }\n\n    override fun surfaceCreated(holder: SurfaceHolder?) {\n        camera = Camera.open()\n        camera.setPreviewDisplay(holder)\n        camera.startPreview()\n    }\n\n    override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {\n        // Ignored, Camera does all the work for us\n    }\n\n    override fun surfaceDestroyed(holder: SurfaceHolder?) {\n        camera.stopPreview()\n        camera.release()\n    }\n}\n```\n\n就像 TextureView 一样，SurfaceView 也提供了生命周期的回调接口。当我们只需要从 SurfaceView 上面得到一个 SurfaceHolder 实例然后向其中添加回调即可。\n\n上面我们以两种控件在相机中的使用为例，除此之外，你也可以自定义两个控件的字类，然后覆写 `onDraw()` 方法进行简单绘图。然后尝试在线程当中对绘图进行更新，以观察它们在非主线程当中更新页面的表现。\n\nSurfaceView 和 View 一大不同就是 SurfaceView 是被动刷新的，但我们可以控制刷新的帧率，而 View 并且通过`invalidate()` 方法通知系统来主动刷新界面的，但是 View 的刷新是依赖于系统的 VSYSC 信号的，其帧率并不受控制，而且因为 UI 线程中的其他一些操作会导致掉帧卡顿。而对于 SurfaceView 而言，它是在子线程中绘制图形，根据这一特性即可控制其显示帧率，通过简单地设置休眠时间，即可，并且由于在子线程中，一般不会引起 UI 卡顿。\n\nSurfaceView 是通过双缓冲来实现的：对于每一个 SurfaceView 而言，有两个独立的 graphic buffer. 在 Buffer A 中绘制内容，然后让屏幕显示 Buffer A；在下一个循环中，在 Buffer B 中绘制内容，然后让屏幕显示Buffer B，如此往复。而由于这个双缓冲机制的存在，可能会引起闪屏现象。解决办法是：不 `post` 空 buffer 到屏幕：当准备更新内容时，先判断内容是否为空，只有非空时才启动 `lockCanvas()-drawCanvas()-unlockCanvasAndPost()` 这个流程。\n\n### 1.3 区别\n\nTextureView 和 SurfaceView 都继承自 View 类，但是 TextureView 在 Andriod 4.0 之后的 API 中才能使用。SurfaceView 可以通过 `SurfaceHolder.addCallback()` 方法在子线程中更新 UI；TextureView 则可以通过 `TextureView.setSurfaceTextureListener()` 在子线程中更新 UI，能够在子线程中更新 UI 是上述两控件相比于 View 的最大优势。\n\n两者更新画面的方式也有些不同，由于 SurfaceView 的双缓冲功能，可以是画面更加流畅的运行。SurfaceView 自带一个 Surface，这个 Surface 在 WMS 中有自己对应的WindowState，在 SurfaceFlinger 中也会有自己的 Layer。这样的好处是对这个Surface的渲染可以放到单独线程去做，渲染时可以有自己的GL context。但是由于其 Surface 的存在导致画面更新会存在间隔。并且，因为这个 Surface 不在 View hierachy 中，它的显示也不受 View 的属性控制，所以不能进行平移，缩放等变换，也不能放在其它 ViewGroup 中，一些 View 中的特性也无法使用。\n\nTextureView 和 SurfaceView 不同，它不会在 WMS 中单独创建窗口，而是作为 View hierachy 中的一个普通 View，因此可以和其它普通 View 一样进行移动，旋转，缩放，动画等变化。值得注意的是 TextureView 必须在硬件加速的窗口中。它显示的内容流数据可以来自 App 进程或是远端进程。从类图中可以看到，TextureView 继承自 View，它与其它的 View 一样在 View hierachy 中管理与绘制。TextureView 重载了 `draw()` 方法，其中主要 SurfaceTexture 中收到的图像数据作为纹理更新到对应的 HardwareLayer 中。它占用内存比 SurfaceView 高，在 5.0 以前在主线程渲染，5.0 以后有单独的渲染线程。\n\n\n\n"
  },
  {
    "path": "系统架构/控件体系/RV.md",
    "content": "# RecyclerView 源码分析\n\nRV 和 ListView：\n\n1. 不同的地方\n2. 高效的原因\n3. 使用不便的地方\n\n要分析的内容：\n\n1. 基本的流程；\n2. 一些效果的实现的逻辑是如何解耦出来的；\n3. 缓存实现的原理。\n\nRV 一个类的代码有超过 1W 行代码，首先我们要明确分析它的角度：\n\n1. 布局发生变化的时候通过 `requestLayout()` 请求进行界面重绘，然后重走 `onMeasure()`, `onLayout()`, `onDraw()` 三个流程\n2. 任何控件的绘制无外乎 `onMeasure()`, `onLayout()`, `onDraw()` 三个流程\n\n## notifyXXX() 系列方法的更新原理\n\n先来看下布局发生变化的时候的更新逻辑，当我们调用 Adapter 的 `notifyXXX()` 方法的时候，内部会调用一个 mObservable 对应的 `notifyXXX()` 方法：\n\n```java\n    public abstract static class Adapter<VH extends ViewHolder> {\n        private final AdapterDataObservable mObservable = new AdapterDataObservable();\n\n        // ...\n\n        // 注册观察者\n        public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {\n            mObservable.registerObserver(observer);\n        }\n\n        // 通知观察者\n        public final void notifyItemChanged(int position, @Nullable Object payload) {\n            mObservable.notifyItemRangeChanged(position, 1, payload);\n        }\n    }\n\n    // 内部通过 ArrayList 维护了一个观察者列表\n    static class AdapterDataObservable extends Observable<AdapterDataObserver> {\n        // ...\n    }\n```\n\n这里的 AdapterDataObservable 也是 Adapter 的内部类，继承了 `android.database.Observable`，这是典型的观察者模式的封装。`android.database.Observable` 内部维护了一个观察者列表，我们可以通过它提供的方法注册和取消注册观察者。当我们调用了 RV 的 `setAdapter()` 方法的时候，内部会调用 Adapter 的 `registerAdapterDataObserver()` 方法注册一个默认的观察者。这个观察者也是 RV 的内部类，即 RecyclerViewDataObserver：\n\n```java\n    private class RecyclerViewDataObserver extends AdapterDataObserver {\n        // ...\n        // 当数据发生变化的时候，这个方法被通知到，从而触发数据更新\n        @Override\n        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {\n            assertNotInLayoutOrScroll(null);\n            if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {\n                triggerUpdateProcessor();\n            }\n        }\n\n        void triggerUpdateProcessor() {\n            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {\n                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);\n            } else {\n                mAdapterUpdateDuringMeasure = true;\n                // 请求布局进行更新\n                requestLayout();\n            }\n        }\n    }\n```\n\n因此，当我们调用 Adapter 的 `notifyXXX()` 方法的时候，它会通知 Adapter 中注册的所有的观察者，其中就包括这个默认的观察者。当观察者收到数据变化的逻辑之后，就通过 `requestLayout()` 请求 RV 进行重绘，然后重走 `onMeasure()`, `onLayout()`, `onDraw()` 三个流程。\n\n## 测量，布局和绘制的流程\n\n首先，在 RV 的布局和测量等过程中，涉及到的一个数据结构是 State，它包含了当前 RV 的状态信息。State 通常被用来在 RV 中的各个组件之间传递信息。这个数据结构中定义了三个常量，用来表示 RV 当前处于哪个阶段；此外，它还定义了一个稀疏数组，用来让用户通过 remove/put/get 等操作存取一些信息：\n\n```java\n    public static class State {\n        // 三个常量，表示三个不同的布局阶段\n        static final int STEP_START = 1;\n        static final int STEP_LAYOUT = 1 << 1;\n        static final int STEP_ANIMATIONS = 1 << 2;\n\n        // 稀疏数组，用来存储自定义数据信息\n        private SparseArray<Object> mData;\n\n        // 当前所处的布局的阶段\n        int mLayoutStep = STEP_START;\n\n        // ... 其他\n    }\n```\n\n### onMeasure()\n\n```java\n    @Override\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        if (mLayout == null) {\n            // LM 为空，使用默认的测量结果（类似于普通的 View）\n            defaultOnMeasure(widthSpec, heightSpec);\n            return;\n        }\n        // isAutoMeasureEnabled() 返回 true 表示使用 RV 自动测量的结果\n        if (mLayout.isAutoMeasureEnabled()) {\n            // ...\n\n            // 第一步：更新 adpater，决定执行的动画，保存控件信息，预布局并保存其信息\n            if (mState.mLayoutStep == State.STEP_START) {\n                dispatchLayoutStep1();\n            }\n\n            // ...\n            // 第二步：根据最终的状态进行布局的地方\n            dispatchLayoutStep2();\n\n            // ...\n        } else {\n            if (mHasFixedSize) {\n                mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);\n                return; \n            }\n            // ...\n            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);\n            mState.mInPreLayout = false; // clear\n        }\n    }\n```\n\n`startInterceptRequestLayout()` 和 `stopInterceptRequestLayout(false)` 方法的调用应该成对存在。它们分别通过对一个整数进行递增和递减。然后在 RecyclerView 的 `requestLayout()` 方法中，通过判断该整数值是否为 0 来防止多次调用 RecyclerView 的 `requestLayout()` 方法。通常这两个方法的调用应该是成对存在的。第一方法应该在 RV 的 `requestLayout()` 被调用之前调用，第二个应该在之后被调用。\n\n```java\n    private void dispatchLayoutStep1() {\n        mState.mIsMeasuring = false;\n        mViewInfoStore.clear();\n        // 进行 adapter 的更新，设置 animation\n        processAdapterUpdatesAndSetAnimationFlags();\n        saveFocusInfo();\n        mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;\n        mItemsAddedOrRemoved = mItemsChanged = false;\n        mState.mInPreLayout = mState.mRunPredictiveAnimations;\n        mState.mItemCount = mAdapter.getItemCount();\n        findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);\n\n        if (mState.mRunSimpleAnimations) {\n            // Step 0: 找出所有没有被移除的条目，进行预布局\n            int count = mChildHelper.getChildCount();\n            for (int i = 0; i < count; ++i) {\n                final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));\n                if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {\n                    continue;\n                }\n                final ItemHolderInfo animationInfo = mItemAnimator\n                        .recordPreLayoutInformation(mState, holder,\n                                ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),\n                                holder.getUnmodifiedPayloads());\n                mViewInfoStore.addToPreLayout(holder, animationInfo);\n                if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved()\n                        && !holder.shouldIgnore() && !holder.isInvalid()) {\n                    long key = getChangedHolderKey(holder);\n                    mViewInfoStore.addToOldChangeHolders(key, holder);\n                }\n            }\n        }\n        if (mState.mRunPredictiveAnimations) {\n            saveOldPositions(); // 保存旧的位置信息，以便 LM 可以进行映射\n            final boolean didStructureChange = mState.mStructureChanged;\n            mState.mStructureChanged = false;\n            // temporarily disable flag because we are asking for previous layout\n            mLayout.onLayoutChildren(mRecycler, mState);\n            mState.mStructureChanged = didStructureChange;\n\n            for (int i = 0; i < mChildHelper.getChildCount(); ++i) {\n                final View child = mChildHelper.getChildAt(i);\n                final ViewHolder viewHolder = getChildViewHolderInt(child);\n                if (viewHolder.shouldIgnore()) {\n                    continue;\n                }\n                if (!mViewInfoStore.isInPreLayout(viewHolder)) {\n                    int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);\n                    boolean wasHidden = viewHolder\n                            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);\n                    if (!wasHidden) {\n                        flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;\n                    }\n                    final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(\n                            mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());\n                    if (wasHidden) {\n                        recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);\n                    } else {\n                        mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);\n                    }\n                }\n            }\n            // 清除旧的位置信息\n            clearOldPositions();\n        } else {\n            clearOldPositions();\n        }\n        mState.mLayoutStep = State.STEP_LAYOUT;\n    }\n```\n\n布局 2：\n\n```java\n    private void dispatchLayoutStep2() {\n        mAdapterHelper.consumeUpdatesInOnePass();\n        mState.mItemCount = mAdapter.getItemCount();\n        mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;\n\n        // Step 2: Run layout\n        mState.mInPreLayout = false;\n        mLayout.onLayoutChildren(mRecycler, mState);\n\n        mState.mStructureChanged = false;\n        mPendingSavedState = null;\n\n        // onLayoutChildren may have caused client code to disable item animations; re-check\n        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;\n        mState.mLayoutStep = State.STEP_ANIMATIONS;\n    }\n```\n\nLinearLayoutManager#onLayoutChildren()\n\n```java\n    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {\n        // ...\n\n        final View focused = getFocusedChild();\n        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION\n                || mPendingSavedState != null) {\n            mAnchorInfo.reset();\n            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;\n            // calculate anchor position and coordinate\n            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);\n            mAnchorInfo.mValid = true;\n        } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)\n                        >= mOrientationHelper.getEndAfterPadding()\n                || mOrientationHelper.getDecoratedEnd(focused)\n                <= mOrientationHelper.getStartAfterPadding())) {\n            mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));\n        }\n\n        // LLM may decide to layout items for \"extra\" pixels to account for scrolling target,\n        // caching or predictive animations.\n        int extraForStart;\n        int extraForEnd;\n        final int extra = getExtraLayoutSpace(state);\n        // If the previous scroll delta was less than zero, the extra space should be laid out\n        // at the start. Otherwise, it should be at the end.\n        if (mLayoutState.mLastScrollDelta >= 0) {\n            extraForEnd = extra;\n            extraForStart = 0;\n        } else {\n            extraForStart = extra;\n            extraForEnd = 0;\n        }\n        extraForStart += mOrientationHelper.getStartAfterPadding();\n        extraForEnd += mOrientationHelper.getEndPadding();\n        if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION\n                && mPendingScrollPositionOffset != INVALID_OFFSET) {\n            // if the child is visible and we are going to move it around, we should layout\n            // extra items in the opposite direction to make sure new items animate nicely\n            // instead of just fading in\n            final View existing = findViewByPosition(mPendingScrollPosition);\n            if (existing != null) {\n                final int current;\n                final int upcomingOffset;\n                if (mShouldReverseLayout) {\n                    current = mOrientationHelper.getEndAfterPadding()\n                            - mOrientationHelper.getDecoratedEnd(existing);\n                    upcomingOffset = current - mPendingScrollPositionOffset;\n                } else {\n                    current = mOrientationHelper.getDecoratedStart(existing)\n                            - mOrientationHelper.getStartAfterPadding();\n                    upcomingOffset = mPendingScrollPositionOffset - current;\n                }\n                if (upcomingOffset > 0) {\n                    extraForStart += upcomingOffset;\n                } else {\n                    extraForEnd -= upcomingOffset;\n                }\n            }\n        }\n        int startOffset;\n        int endOffset;\n        final int firstLayoutDirection;\n        if (mAnchorInfo.mLayoutFromEnd) {\n            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL\n                    : LayoutState.ITEM_DIRECTION_HEAD;\n        } else {\n            firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD\n                    : LayoutState.ITEM_DIRECTION_TAIL;\n        }\n\n        onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);\n        detachAndScrapAttachedViews(recycler);\n        mLayoutState.mInfinite = resolveIsInfinite();\n        mLayoutState.mIsPreLayout = state.isPreLayout();\n        if (mAnchorInfo.mLayoutFromEnd) {\n            // fill towards start\n            updateLayoutStateToFillStart(mAnchorInfo);\n            mLayoutState.mExtra = extraForStart;\n            fill(recycler, mLayoutState, state, false);\n            startOffset = mLayoutState.mOffset;\n            final int firstElement = mLayoutState.mCurrentPosition;\n            if (mLayoutState.mAvailable > 0) {\n                extraForEnd += mLayoutState.mAvailable;\n            }\n            // fill towards end\n            updateLayoutStateToFillEnd(mAnchorInfo);\n            mLayoutState.mExtra = extraForEnd;\n            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;\n            fill(recycler, mLayoutState, state, false);\n            endOffset = mLayoutState.mOffset;\n\n            if (mLayoutState.mAvailable > 0) {\n                // end could not consume all. add more items towards start\n                extraForStart = mLayoutState.mAvailable;\n                updateLayoutStateToFillStart(firstElement, startOffset);\n                mLayoutState.mExtra = extraForStart;\n                fill(recycler, mLayoutState, state, false);\n                startOffset = mLayoutState.mOffset;\n            }\n        } else {\n            // fill towards end\n            updateLayoutStateToFillEnd(mAnchorInfo);\n            mLayoutState.mExtra = extraForEnd;\n            fill(recycler, mLayoutState, state, false);\n            endOffset = mLayoutState.mOffset;\n            final int lastElement = mLayoutState.mCurrentPosition;\n            if (mLayoutState.mAvailable > 0) {\n                extraForStart += mLayoutState.mAvailable;\n            }\n            // fill towards start\n            updateLayoutStateToFillStart(mAnchorInfo);\n            mLayoutState.mExtra = extraForStart;\n            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;\n            fill(recycler, mLayoutState, state, false);\n            startOffset = mLayoutState.mOffset;\n\n            if (mLayoutState.mAvailable > 0) {\n                extraForEnd = mLayoutState.mAvailable;\n                // start could not consume all it should. add more items towards end\n                updateLayoutStateToFillEnd(lastElement, endOffset);\n                mLayoutState.mExtra = extraForEnd;\n                fill(recycler, mLayoutState, state, false);\n                endOffset = mLayoutState.mOffset;\n            }\n        }\n\n        // changes may cause gaps on the UI, try to fix them.\n        // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have\n        // changed\n        if (getChildCount() > 0) {\n            // because layout from end may be changed by scroll to position\n            // we re-calculate it.\n            // find which side we should check for gaps.\n            if (mShouldReverseLayout ^ mStackFromEnd) {\n                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);\n                startOffset += fixOffset;\n                endOffset += fixOffset;\n                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);\n                startOffset += fixOffset;\n                endOffset += fixOffset;\n            } else {\n                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);\n                startOffset += fixOffset;\n                endOffset += fixOffset;\n                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);\n                startOffset += fixOffset;\n                endOffset += fixOffset;\n            }\n        }\n        layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);\n        if (!state.isPreLayout()) {\n            mOrientationHelper.onLayoutComplete();\n        } else {\n            mAnchorInfo.reset();\n        }\n        mLastStackFromEnd = mStackFromEnd;\n    }\n```\n\n布局 3：\n\n```java\n    private void dispatchLayoutStep3() {\n        mState.mLayoutStep = State.STEP_START;\n        if (mState.mRunSimpleAnimations) {\n            // 步骤 3：执行更新动画\n            for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {\n                ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));\n                if (holder.shouldIgnore()) {\n                    continue;\n                }\n                long key = getChangedHolderKey(holder);\n                final ItemHolderInfo animationInfo = mItemAnimator\n                        .recordPostLayoutInformation(mState, holder);\n                ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);\n                if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {\n                    // 执行动画\n                    final boolean oldDisappearing = mViewInfoStore.isDisappearing(oldChangeViewHolder);\n                    final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);\n                    if (oldDisappearing && oldChangeViewHolder == holder) {\n                        // run disappear animation instead of change\n                        mViewInfoStore.addToPostLayout(holder, animationInfo);\n                    } else {\n                        final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(\n                                oldChangeViewHolder);\n                        // we add and remove so that any post info is merged.\n                        mViewInfoStore.addToPostLayout(holder, animationInfo);\n                        ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);\n                        if (preInfo == null) {\n                            handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);\n                        } else {\n                            animateChange(oldChangeViewHolder, holder, preInfo, postInfo,\n                                    oldDisappearing, newDisappearing);\n                        }\n                    }\n                } else {\n                    mViewInfoStore.addToPostLayout(holder, animationInfo);\n                }\n            }\n\n            // 步骤 4: 处理控件信息，触发动画\n            mViewInfoStore.process(mViewInfoProcessCallback);\n        }\n\n        mLayout.removeAndRecycleScrapInt(mRecycler);\n        mState.mPreviousLayoutItemCount = mState.mItemCount;\n        mDataSetHasChangedAfterLayout = false;\n        mDispatchItemsChangedEvent = false;\n        mState.mRunSimpleAnimations = false;\n\n        mState.mRunPredictiveAnimations = false;\n        mLayout.mRequestedSimpleAnimations = false;\n        if (mRecycler.mChangedScrap != null) {\n            mRecycler.mChangedScrap.clear();\n        }\n        if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {\n            mLayout.mPrefetchMaxCountObserved = 0;\n            mLayout.mPrefetchMaxObservedInInitialPrefetch = false;\n            mRecycler.updateViewCacheSize();\n        }\n\n        mLayout.onLayoutCompleted(mState);\n        mViewInfoStore.clear();\n        if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {\n            dispatchOnScrolled(0, 0);\n        }\n        recoverFocusFromState();\n        resetFocusInfo();\n    }\n```\n\n### onLayout()\n\n```java\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        dispatchLayout();\n        mFirstLayoutComplete = true;\n    }\n\n    void dispatchLayout() {\n        mState.mIsMeasuring = false;\n        if (mState.mLayoutStep == State.STEP_START) {\n            dispatchLayoutStep1();\n            mLayout.setExactMeasureSpecsFrom(this);\n            dispatchLayoutStep2();\n        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()\n                || mLayout.getHeight() != getHeight()) {\n            mLayout.setExactMeasureSpecsFrom(this);\n            dispatchLayoutStep2();\n        } else {\n            mLayout.setExactMeasureSpecsFrom(this);\n        }\n        dispatchLayoutStep3();\n    }\n```\n\n### onDraw()\n\n```java\n    public void draw(Canvas c) {\n        super.draw(c);\n\n        // ItemDecoration 被调用的地方 1\n        final int count = mItemDecorations.size();\n        for (int i = 0; i < count; i++) {\n            mItemDecorations.get(i).onDrawOver(c, this, mState);\n        }\n\n        boolean needsInvalidate = false;\n        if (mLeftGlow != null && !mLeftGlow.isFinished()) {\n            final int restore = c.save();\n            final int padding = mClipToPadding ? getPaddingBottom() : 0;\n            c.rotate(270);\n            c.translate(-getHeight() + padding, 0);\n            needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);\n            c.restoreToCount(restore);\n        }\n        if (mTopGlow != null && !mTopGlow.isFinished()) {\n            final int restore = c.save();\n            if (mClipToPadding) {\n                c.translate(getPaddingLeft(), getPaddingTop());\n            }\n            needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);\n            c.restoreToCount(restore);\n        }\n        if (mRightGlow != null && !mRightGlow.isFinished()) {\n            final int restore = c.save();\n            final int width = getWidth();\n            final int padding = mClipToPadding ? getPaddingTop() : 0;\n            c.rotate(90);\n            c.translate(-padding, -width);\n            needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);\n            c.restoreToCount(restore);\n        }\n        if (mBottomGlow != null && !mBottomGlow.isFinished()) {\n            final int restore = c.save();\n            c.rotate(180);\n            if (mClipToPadding) {\n                c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom());\n            } else {\n                c.translate(-getWidth(), -getHeight());\n            }\n            needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);\n            c.restoreToCount(restore);\n        }\n\n        if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0\n                && mItemAnimator.isRunning()) {\n            needsInvalidate = true;\n        }\n\n        if (needsInvalidate) {\n            ViewCompat.postInvalidateOnAnimation(this);\n        }\n    }\n```\n\n\n```java\n    public void onDraw(Canvas c) {\n        super.onDraw(c);\n\n        // ItemDecoration 被调用的地方 2\n        final int count = mItemDecorations.size();\n        for (int i = 0; i < count; i++) {\n            mItemDecorations.get(i).onDraw(c, this, mState);\n        }\n    }\n```\n\n### 再认识 ViewHolder \n\n定义了一些 FLAG 常量。然后内部定义了整型的 flag 字段，包含了当前的 flag 信息：\n\n```java\n    public abstract static class ViewHolder {\n\n        static final int FLAG_BOUND = 1 << 0;\n\n        // ... 其他 FLAG\n\n        int mFlags; // 当前的 flag 信息\n\n        // ... 其他方法和字段\n    }\n```\n\n### ItemAnimator\n\n这个类定义了当 item 发生变化的时候应用到 item 上面的动画。\n\n它还有一个内部类 ItemHolderInfo，它是印个包含了 item 的边界信息的数据结构，用在 item 的动画的计算中。\n\nRV 中默认使用 DefaultItemAnimator 的实现。\n\nDefaultItemAnimator 中提供了一些列表。它们包含了要作动画的 ViewHolder 信息：\n\n```java\n    private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();\n    private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();\n    private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();\n    private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();\n```\n\nRV 中的 `animateAppearance()` `animateDisappearance()` 和 `animateChange()` 三个方法用来向 DefaultItemAnimator 中添加 ViewHolder 信息。然后，当需要进行动画的时候，执行下下面的 Runnable：\n\n```java\n    private Runnable mItemAnimatorRunner = new Runnable() {\n        @Override\n        public void run() {\n            if (mItemAnimator != null) {\n                mItemAnimator.runPendingAnimations();\n            }\n            mPostedAnimatorRunner = false;\n        }\n    };\n```\n\n然后，DefaultItemAnimator 就会从上述的 ArrayList 中取出 ViewHolder 并对它们执行动画。\n\n那么问题来了，上面的 RV 中的三个方法在什么时候调用呢？\n\n```java\n    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =\n            new ViewInfoStore.ProcessCallback() {\n                @Override\n                public void processDisappeared(ViewHolder viewHolder, ItemHolderInfo info, ItemHolderInfo postInfo) {\n                    mRecycler.unscrapView(viewHolder);\n                    // 被调用 1\n                    animateDisappearance(viewHolder, info, postInfo);\n                }\n                @Override\n                public void processAppeared(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo info) {\n                    // 被调用 2 \n                    animateAppearance(viewHolder, preInfo, info);\n                }\n\n                @Override\n                public void processPersistent(ViewHolder viewHolder, ItemHolderInfo preInfo, ItemHolderInfo postInfo) {\n                    viewHolder.setIsRecyclable(false);\n                    if (mDataSetHasChangedAfterLayout) {\n                        if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, postInfo)) {\n                            postAnimationRunner();\n                        }\n                        // 被调用 3\n                    } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {\n                        postAnimationRunner();\n                    }\n                }\n\n                @Override\n                public void unused(ViewHolder viewHolder) {\n                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);\n                }\n            };\n```\n\n然后 `mViewInfoProcessCallback` 在 `dispatchLayoutStep3()` 中被传递到 ViewHolderStore 中：\n\n```java\nmViewInfoStore.process(mViewInfoProcessCallback);\n```\n\n### ItemDecoration\n\n`onDraw()` 和 `draw()` 两个方法，使用到的位置等\n\n### 触摸事件\n\nItemTouchHelper 继承了 `RecyclerView.ItemDecoration` 并实现了 `RecyclerView.OnChildAttachStateChangeListener` 接口。\n\n```java\n    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {\n        if (mRecyclerView == recyclerView) {\n            return; // nothing to do\n        }\n        if (mRecyclerView != null) {\n            destroyCallbacks();\n        }\n        mRecyclerView = recyclerView;\n        if (recyclerView != null) {\n            // ...\n            setupCallbacks();\n        }\n    }\n\n    private void setupCallbacks() {\n        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());\n        mSlop = vc.getScaledTouchSlop();\n        mRecyclerView.addItemDecoration(this);\n        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);\n        mRecyclerView.addOnChildAttachStateChangeListener(this);\n        startGestureDetection();\n    }\n````\n\n这里的 mOnItemTouchListener 是 OnItemTouchListener 的实例，实现了该接口中的三个方法。\n\n\n\n\n\n\n"
  },
  {
    "path": "系统架构/控件体系/RV各种效果实现.md",
    "content": "##  RV 的各种效果实现\n\n### 触摸\n\n```kotlin\n// 定义 ItemTouchHelper.Callback\nclass SimpleItemTouchCallback<T, K : BaseViewHolder>(private var adapter: SimpleTouchAdapter<T, K>) : ItemTouchHelper.Callback() {\n\n    // 这里指定长按和拖拽允许的方向\n    override fun getMovementFlags(p0: RecyclerView, p1: RecyclerView.ViewHolder): Int {\n        val upFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN\n        val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END\n        return makeMovementFlags(upFlags, swipeFlags)\n    }\n\n    // 长按拖拽的时候会回调这个方法\n    override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean {\n        adapter.onItemMove(p1.adapterPosition, p2.adapterPosition)\n        return true\n    }\n\n    // 非长按状态拖拽的时候回调这个方法\n    override fun onSwiped(p0: RecyclerView.ViewHolder, p1: Int) {\n        adapter.onSwiped(p0.adapterPosition, p1)\n    }\n}\n\n// 一个接口，用来回调通知 Adapter\nabstract class SimpleTouchAdapter<T, K : BaseViewHolder>(layoutResId: Int) : BaseQuickAdapter<T, K>(layoutResId) {\n\n    abstract fun onSwiped(position: Int, direction: Int)\n\n    abstract fun onItemMove(from: Int, to: Int)\n}\n\n// Adapter\nclass ColorfulAdapter : SimpleTouchAdapter<ColorModel, BaseViewHolder>(R.layout.item_colorful) {\n\n    override fun onSwiped(position: Int, direction: Int) {\n        data.removeAt(position)\n        notifyItemRemoved(position)\n    }\n\n    // 本质上就是数据位置交换的逻辑，上面的触摸事件不会真正地导致数据交换，因此我们在回调接口中自己实现这些逻辑\n    override fun onItemMove(from: Int, to: Int) {\n        if (from < to) {\n            for (i in from until to) {\n                Collections.swap(data, i, i + 1)\n            }\n        } else {\n            for (i in to until from) {\n                Collections.swap(data, i, i + 1)\n            }\n        }\n        notifyItemMoved(from, to)\n    }\n\n    override fun convert(helper: BaseViewHolder?, item: ColorModel?) {\n        helper?.setBackgroundColor(R.id.ll, item?.color!!)\n        helper?.setText(R.id.tv, item?.name)\n    }\n}\n```\n\n最终的应用方式：\n\n```kotlin\n    // 构建 Adapter\n    val adapter = ColorfulAdapter()\n    adapter.setNewData(colorModels)\n    binding.rv.adapter = adapter\n    // 构建一个 ItemTouchHelper，并将其关联到 RV\n    callback = SimpleItemTouchCallback(true, true, adapter)\n    val touchHelper = ItemTouchHelper(callback)\n    touchHelper.attachToRecyclerView(binding.rv)\n```\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "系统架构/控件体系/View体系详解：View的工作流程.md",
    "content": "# View 的绘制流程\n\n## 1、View 树的加载流程\n\n当我们调用 `startActivity()` 方法的时候，会调用到 `ActivityThread` 中的 `performLaunchActivity()` 获取一个 Activity 实例， 并在 `Instrumentation` 的 `callActivityOnCreate()`  方法中调用 Activity 的 `onCreate()` 完成 DecorView 的创建。这样我们就获取了一个 Activity 的实例，然后我们调用 `handleResumeActivity()` 来回调 Activity 的 `onResume()`：\n\n```java\n    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {\n        // ....\n        WindowManagerGlobal.initialize();\n        // 创建 Activity 的实例，在这里完成对 Activity 的 onCreate() 方法的回调\n        Activity a = performLaunchActivity(r, customIntent);\n        if (a != null) {\n            // ...\n            // 在这里回调 Activity 的 onResume() 方法\n            handleResumeActivity(r.token, false, r.isForward,\n                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);\n            if (!r.activity.mFinished && r.startsNotResumed) {\n                // 在这里完成对 Activity 的 onPause() 方法的回调\n                performPauseActivityIfNeeded(r, reason);\n                // ...\n            }\n        }\n        // ...\n    }\n```\n\n然后，在 `handleResumeActivity()` 方法中的 `performResumeActivity()` 会回调 Activity 的 `onResume()` 方法。在该方法中，我们会从 Window 中获取之前添加进去的 DecorView，然后将其添加到 WindowManager 中：\n\n```java\n    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {\n        // 在这里会回调 Activity 的 onResume()\n        r = performResumeActivity(token, clearHide, reason);\n        if (r != null) {\n            final Activity a = r.activity;\n            // ...\n            if (r.window == null && !a.mFinished && willBeVisible) {\n                r.window = r.activity.getWindow();\n                // 在这里获取 DecorView\n                View decor = r.window.getDecorView();\n                decor.setVisibility(View.INVISIBLE);\n                // 获取 WindowManager 实例，实际是 WindowManagerImpl \n                ViewManager wm = a.getWindowManager();\n                WindowManager.LayoutParams l = r.window.getAttributes();\n                a.mDecor = decor;\n                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;\n                l.softInputMode |= forwardBit;\n                if (r.mPreserveWindow) {\n                    a.mWindowAdded = true;\n                    r.mPreserveWindow = false;\n                    // Activity 被重建，复用 DecorView，通知子元素\n                    ViewRootImpl impl = decor.getViewRootImpl();\n                    if (impl != null) {\n                        impl.notifyChildRebuilt();\n                    }\n                }\n                if (a.mVisibleFromClient) {\n                    if (!a.mWindowAdded) {\n                        a.mWindowAdded = true;\n                        // 将 DecorView 添加到 WindowManager 中\n                        wm.addView(decor, l);\n                    } else {\n                        a.onWindowAttributesChanged(l);\n                    }\n                }\n            }\n        }\n    }\n```\n\n这里的 `WindowManager` 是 `WindowManagerImpl` 的实例，而调用它的 `addView()` 方法的时候会使用 `WindowManagerGlobal` 的 `addView()` 方法。在该方法中会 new 出来一个 `ViewRootImpl`，然后调用它的 `setView()` 把传进来的 `DecorView` 添加到 `Window` 里。同时，会调用 `requestLayout()` 方法进行布局，然后，并最终调用 `performTraversals()` 完成对整个 View 树进行遍历：\n\n```java\n    private void performTraversals() {\n        // ...\n        if (!mStopped || mReportNextDraw) {\n            // ...\n            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);\n        }\n        // ...\n        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);\n        boolean triggerGlobalLayoutListener = didLayout\n                || mAttachInfo.mRecomputeGlobalAttributes;\n        if (didLayout) {\n            performLayout(lp, mWidth, mHeight);\n            // ...\n        }\n        // ...\n        if (!cancelDraw && !newSurface) {\n            // ...\n            performDraw();\n        }\n    }\n```\n\n在该方法中会调用 `performMeasure()`、`performLayout()` 和 `performDraw()` 三个方法，它们分别会调用 DecorView 的 `measure()`、`layout()` 和 `draw()` 完成对**整个 View 树的测量、布局和绘制**，一个界面也就呈现给用户了。如果您做过自定义 View 的话，那么您对 `onMeasure()`、`onLayout()` 和 `onDraw()`三个方法一定不会陌生，前面的三个方法与后面的三个方法之间的关系就是：后面的三个方法会被前面的三个方法调用，本质上就是提供给用户用来自定义的方法。下面我们就看下这三个方法究竟各自做了什么操作，当然，我们尽可能从自定义控件的角度来分析，因为这对一个开发者可能帮助更大。\n\n## 2、measure()\n\nView 的大小不仅由自身所决定，同时也会受到父控件的影响，为了我们的控件能更好的适应各种情况，一般会自己进行测量。在上面我们提到了 `measure()` 方法，它是用来测量 View 的大小的，但实际上测量的主要工作是交给 `onMeasure()` 方法的。在 View 中，`onMeasure()` 是一个 `protected` 的方法，显然它设计的目的就是：提供给子 View 按照父容器提供的限制条件，控制自身的大小，实现自己大小的测量逻辑。所以，当我们自定义一个控件的时候，只会去覆写 `onMeasure()` 而不去覆写 `measure()` 方法。\n\n在 Android 中，我们的控件分成 View 和 ViewGroup 两种类型。根据上面的分析，对 View 的测量，我们可以得出如下结论：在 Android 中，ViewGroup 会根据其自身的布局特点，把限制条件封装成 `widthMeasureSpec` 和 `heightMeasureSpec` 两个参数传递给子元素；然后，在子元素中根据这两个参数来调整自身的大小。所以，ViewGroup 的 `measure()` 方法会根据其布局特性的不同而不同；而 View 的 `measure()`，不论其父容器是哪种类型，只根据 `widthMeasureSpec` 和 `heightMeasureSpec` 决定。\n\n下面我们来看一下 `onMeasure()` 在 View 和 ViewGroup 中的不同表现形式。\n\n### 2.1 View 的 onMeasure()\n\n下面是 View 类中的 `onMeasure()` 方法。这是一个默认的实现，调用了 `setMeasuredDimension()` 方法来存储测量之后的宽度和高度。**当我们自定义 View 的时候，也需要调用 setMeasuredDimension() 方法把最终的测量结果存储起来**：\n\n```java\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        setMeasuredDimension(\n            getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),\n            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));\n    }\n```\n\n显然，我们的测量依据就是 `widthMeasureSpec` 和 `heightMeasureSpec` 两个参数。它们是整型的、32位变量，包含了测量模式和测量数值的信息（按位存储到整型变量上，包装成整型的目的是为了节约存储空间）。一般我们会像下面这样来分别获取高度和宽度的测量模式和测量数值（实际就是按位截取）：\n\n```java    \n    int widthsize = MeasureSpec.getSize(widthMeasureSpec);      // 测量数值\n    int widthmode = MeasureSpec.getMode(widthMeasureSpec);      // 测量模式    \n    int heightsize = MeasureSpec.getSize(heightMeasureSpec);    // 测量数值\n    int heightmode = MeasureSpec.getMode(heightMeasureSpec);    // 测量模式\n```\n\n测量模式共有 `MeasureSpec.UNSPECIFIED`、`MeasureSpec.AT_MOST` 和 `MeasureSpec.EXACTLY` 三种，分别对应二进制数值 `00`、`01` 和 `10`，它们各自的含义如下：\n\n1. `UNSPECIFIED`：默认值，父控件没有给子 View 任何限制，子 View 可以设置为任意大小；\n2. `EXACTLY`：表示父控件已经确切的指定了子 View 的大小；\n3. `AT_MOST`：表示子 View 具体大小没有尺寸限制，但是存在上限，上限一般为父 View 大小。\n\n这里，我不打算详细介绍 View 中默认测量逻辑的具体实现。它的大致逻辑是这样的：首先我们会用 `getDefaultSize()` 获取默认的宽度或者高度，这个方法接收两个参数，一个是默认的尺寸，一个测量模式。如果父控件没有给它任何限制，它就使用默认的尺寸，否则使用测量数值。这里的默认的尺寸通过 `getSuggestedMinimumHeight()`/`getSuggestedMinimumWidth()` 方法得到，它会根据背景图片高度/宽度和 `mMinHeight`/`mMinWidth` 的值，取一个最大的值作为控件的高度/宽度。\n\n所以，View 的默认的测量逻辑的实际效果是：首先 View 的大小受父容器的影响，如果父容器没有给它限制的话，它会取背景图片和最小的高度或者宽度中取一个最大的值作为自己的大小。\n\n### 2.2 ViewGroup 的 onMeasure()\n\n#### 2.2.1 ViewGroup 中的方法\n\n由于 ViewGroup 本身没有布局的特点，所以它没有覆写 `onMeasure()`。有自身布局特点的，比如 `LinearLayout` 和 `RelativeLayout` 等都覆写并实现了这个方法。尽管如此，ViewGroup 提供了一些方法帮助我们进行测量，首先是 `measureChildren()` 方法：\n\n```java\n    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {\n        final int size = mChildrenCount;\n        final View[] children = mChildren;\n        for (int i = 0; i < size; ++i) {\n            final View child = children[i];\n            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {\n                measureChild(child, widthMeasureSpec, heightMeasureSpec);\n            }\n        }\n    }\n```\n\n这里的逻辑比较简单，就是对子元素进行遍历并判断如果指定的 View 是否位 `GONE` 的状态，如果不是就调用 `measureChild()` 方法：\n\n```java\n    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {\n        final LayoutParams lp = child.getLayoutParams();\n        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,\n                mPaddingLeft + mPaddingRight, lp.width);\n        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,\n                mPaddingTop + mPaddingBottom, lp.height);\n        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);\n    }\n```\n\n该方法也比较容易理解，就是将子元素的布局参数 `LayoutParams` 取出，获取它的宽度和高度之后，将所有信息传递给 `getChildMeasureSpec()`。这样就得到了用于子元素布局的 `childWidthMeasureSpec` 和 `childHeightMeasureSpec` 参数。然后，再调用子元素的 `measure()` 方法，从而依次完成对整个 View 树的遍历。下面我们看下 `getChildMeasureSpec()` 方法做了什么操作：\n\n```java\n    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {\n        // 首先从 spec 中取出父控件的测量模式和测量数值\n        int specMode = MeasureSpec.getMode(spec);\n        int specSize = MeasureSpec.getSize(spec);\n        // 这里需要保证 size 不能为负数，也就是预留给子元素的最大空间，由父元素的测量数值减去填充得到\n        int size = Math.max(0, specSize - padding);\n        // 用于返回的值\n        int resultSize = 0;\n        int resultMode = 0;\n        // 根据父空间的测量模式\n        switch (specMode) {\n            // 父控件的大小是固定的\n            case MeasureSpec.EXACTLY:\n                if (childDimension >= 0) {\n                    // 子 View 指定了大小\n                    resultSize = childDimension;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childDimension == LayoutParams.MATCH_PARENT) {\n                    // 子元素希望大小与父控件相同（填满整个父控件）\n                    resultSize = size;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childDimension == LayoutParams.WRAP_CONTENT) {\n                    // 子元素希望有自己决定大小，但是不能比父控件大\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                }\n                break;\n            // 父控件的具体大小没有尺寸限制，但是存在上限\n            case MeasureSpec.AT_MOST:\n                if (childDimension >= 0) {\n                    // 子 View 指定了大小\n                    resultSize = childDimension;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childDimension == LayoutParams.MATCH_PARENT) {\n                    // 子控件希望与父控件大小一致，但是父控件的大小也是不确定的，故让子控件不要比父控件大\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                } else if (childDimension == LayoutParams.WRAP_CONTENT) {\n                    // 子控件希望自己决定大小，限制其不要比父控件大\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                }\n                break;\n            // 父控件没有任何限制，可以设置为任意大小\n            case MeasureSpec.UNSPECIFIED:\n                if (childDimension >= 0) {\n                    // 子元素设置了大小\n                    resultSize = childDimension;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childDimension == LayoutParams.MATCH_PARENT) {\n                    // 子控件希望和父控件一样大，但是父控件多大都不确定；系统23以下返回true，以上返回size\n                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;\n                    resultMode = MeasureSpec.UNSPECIFIED;\n                } else if (childDimension == LayoutParams.WRAP_CONTENT) {\n                    resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;\n                    resultMode = MeasureSpec.UNSPECIFIED;\n                }\n                break;\n        }\n        // 返回一个封装好的测量结果，就是把测量数值和测量模式封装成一个32位的整数\n        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);\n    }\n```\n\n上面我们已经为这段代码作了非常详细的注释。只需要注意，这里在获取子元素的测量结果的时候是基于父控件的测量结果来的，需要根据父元素的测量模式和测量数值结合自身的布局特点分成上面九种情况。或者可以按照下面的写法将其划分成下面几种情况：\n\n```java\n    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {\n        int specMode = MeasureSpec.getMode(spec), specSize = MeasureSpec.getSize(spec);\n        int size = Math.max(0, specSize - padding);\n        int resultSize = 0, resultMode = 0;\n        if (childDimension >= 0) {\n            // 子元素指定了具体的大小，就用子元素的大小\n            resultSize = childDimension;\n            resultMode = MeasureSpec.EXACTLY;\n        } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {\n            // 子元素希望和父控件一样大，需要设置其上限，然后测量模式与父控件一致即可\n            if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {\n                resultSize = size;\n                resultMode = specMode;\n            } else if (specMode == MeasureSpec.UNSPECIFIED) {\n                // API23一下就是0，父控件没有指定大小的时候，子控件只能是0；以上是size\n                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;\n                resultMode = MeasureSpec.UNSPECIFIED;\n            }\n        } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {\n            // 子元素希望自己决定大小，设置其大小的上限是父控件的大小即可\n            if (specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST) {\n                resultSize = size;\n                resultMode = MeasureSpec.AT_MOST;\n            } else if (specMode == MeasureSpec.UNSPECIFIED) {\n                // API23一下就是0，父控件没有指定大小的时候，子控件只能是0；以上是size\n                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;\n                resultMode = MeasureSpec.UNSPECIFIED;\n            }\n        }\n        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);\n    }\n```\n\n这两种方式只是划分的角度不一样，后面的这种方法是从子元素的布局参数上面来考虑的。另外，这里有个 `sUseZeroUnspecifiedMeasureSpec` 布尔参数需要提及一下，会根据系统的版本来进行赋值：\n\n```java\n    sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;\n```\n\n也就是当系统是 API23 以下的时候的为 `true`. 加入这个参数的原因是，API23 之后，当父控件的测量模式是 `UNSPECIFIED` 的时候，子元素可以给父控件提供一个可能的大小。下面是注释的原话 ;-)\n\n```java\n    // In M and newer, our widgets can pass a \"hint\" value in the size\n    // for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers\n    // know what the expected parent size is going to be, so e.g. list items can size\n    // themselves at 1/3 the size of their container. It breaks older apps though,\n    // specifically apps that use some popular open source libraries.\n```\n\n#### 2.2.2 LinearLayout 的 onMeasure()\n\n上面我们分析的是 ViewGroup 中提供的一些方法，下面我们以 LinearLayout 为例，看一下一个标准的容器类型的控件是如何实现其测量的逻辑的。\n\n下面是其 `onMeasure()` 方法，显然在进行测量的时候会根据其布局的方向分别实现测量的逻辑：\n\n```java\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        if (mOrientation == VERTICAL) {\n            measureVertical(widthMeasureSpec, heightMeasureSpec);\n        } else {\n            measureHorizontal(widthMeasureSpec, heightMeasureSpec);\n        }\n    }\n```\n\n然后，我们以 `measureVertical()` 为例，来看一下 LinearLayout 在垂直方向上面是如何进行测量的。这段代码比较长，我们只截取其中的一部分来进行分析：\n\n```java\n    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {\n        // ...\n        // 获取LinearLayout的测量模式\n        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n        // ...\n        mTotalLength += mPaddingTop + mPaddingBottom;\n        int heightSize = mTotalLength;\n        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());\n        // ...\n            for (int i = 0; i < count; ++i) {\n                final View child = getVirtualChildAt(i);\n                if (child == null || child.getVisibility() == View.GONE) {\n                    continue;\n                }\n                final LayoutParams lp = (LayoutParams) child.getLayoutParams();\n                final float childWeight = lp.weight;\n                if (childWeight > 0) {\n                    // ...\n                    // 获取一个测量的数值和测量模式\n                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(\n                            Math.max(0, childHeight), MeasureSpec.EXACTLY);\n                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,\n                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,\n                            lp.width);\n                    // 调用子元素进行测量\n                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);\n                    childState = combineMeasuredStates(childState, child.getMeasuredState()\n                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));\n                }\n                \n                final int margin =  lp.leftMargin + lp.rightMargin;\n                final int measuredWidth = child.getMeasuredWidth() + margin;\n                maxWidth = Math.max(maxWidth, measuredWidth);\n\n                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&\n                        lp.width == LayoutParams.MATCH_PARENT;\n\n                alternativeMaxWidth = Math.max(alternativeMaxWidth,\n                        matchWidthLocally ? margin : measuredWidth);\n\n                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;\n\n                final int totalLength = mTotalLength;\n                // 将宽度增加到 mTotalLength 上\n                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +\n                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));\n            }\n            mTotalLength += mPaddingTop + mPaddingBottom;\n        // ...\n        maxWidth += mPaddingLeft + mPaddingRight;\n        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());\n        // 最终确定测量的大小\n        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),\n                heightSizeAndState);\n        // ...\n    }\n```\n\n上面是 LinearLayout 在垂直方向上面的测量的过程，在测量的时候会根据子元素的布局将子元素的测量高度添加到 `mTotalLength` 上，然后再加上填充的大小，作为最终的测量结果。\n\n## 3、layout()\n\n `layout()` 用于确定控件的位置，它提供了 `onLayout()` 来交给字类实现，同样我们在自定义控件的时候只要实现 `onLayout()` 方法即可。在我们自定义 View 的时候，如果定义的是非 ViewGroup 类型的控件，一般是不需要覆写 `onLayout()` 方法的。\n\n下面我们先看一下 `layout()` 方法在 View 中的实现：\n\n```java\n    public void layout(int l, int t, int r, int b) {\n        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {\n            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);\n            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;\n        }\n\n        int oldL = mLeft;\n        int oldT = mTop;\n        int oldB = mBottom;\n        int oldR = mRight;\n\n        boolean changed = isLayoutModeOptical(mParent) ?\n                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);\n\n        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {\n            onLayout(changed, l, t, r, b);\n            // ...\n        }\n\n        // ...\n    }\n```\n\n这里会调用 `setFrame()` 方法，它的主要作用是根据新的布局参数和老的布局参数做一个对比，以判断控件的大小是否发生了变化，如果变化了的话就调用 `invalidate()` 方法并传入参数 `true`，以表明绘图的缓存也发生了变化。这里就不给出这个方法的具体实现了。然后注意到，在 `layout()` 方法中会回调 `onLayout()` 方法来完成各个控件的位置的确定。\n\n对于 ViewGroup，它重写了 `layout()` 并在其中调用了 View 中的 `layout()` 方法，不过整体并没有做太多的逻辑。与测量过程类似，ViewGroup 并没有实现 `onLayout` 方法。同样，对于 ViewGroup 类型的控件，我们还是以 LinearLayout 为例说明一下 `onLayout()` 的实现逻辑：\n\n与测量过程类似，LinearLayout 在 layout 的时候也根据布局的方向分成两种情形：\n\n```java\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        if (mOrientation == VERTICAL) {\n            layoutVertical(l, t, r, b);\n        } else {\n            layoutHorizontal(l, t, r, b);\n        }\n    }\n```\n\n这里我们仍以垂直方向的方法为例。与测量的过程相比，layout 的过程的显得简单、清晰得多：\n\n```java\n    void layoutVertical(int left, int top, int right, int bottom) {\n        // ...\n        // 根据控件的 gravity 特点得到顶部的位置\n        switch (majorGravity) {\n           case Gravity.BOTTOM:\n               childTop = mPaddingTop + bottom - top - mTotalLength;\n               break;\n           case Gravity.CENTER_VERTICAL:\n               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;\n               break;\n           case Gravity.TOP:\n           default:\n               childTop = mPaddingTop;\n               break;\n        }\n\n        // 遍历子控件\n        for (int i = 0; i < count; i++) {\n            final View child = getVirtualChildAt(i);\n            if (child == null) {\n                childTop += measureNullChild(i);\n            } else if (child.getVisibility() != GONE) {\n                final int childWidth = child.getMeasuredWidth();\n                final int childHeight = child.getMeasuredHeight();\n\n                final LinearLayout.LayoutParams lp =\n                        (LinearLayout.LayoutParams) child.getLayoutParams();\n\n                int gravity = lp.gravity;\n                if (gravity < 0) {\n                    gravity = minorGravity;\n                }\n                final int layoutDirection = getLayoutDirection();\n                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);\n                // 得到子控件的左边的位置\n                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {\n                    case Gravity.CENTER_HORIZONTAL:\n                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)\n                                + lp.leftMargin - lp.rightMargin;\n                        break;\n                    case Gravity.RIGHT:\n                        childLeft = childRight - childWidth - lp.rightMargin;\n                        break;\n                    case Gravity.LEFT:\n                    default:\n                        childLeft = paddingLeft + lp.leftMargin;\n                        break;\n                }\n\n                if (hasDividerBeforeChildAt(i)) {\n                    childTop += mDividerHeight;\n                }\n\n                childTop += lp.topMargin;\n                // 本质上调用子控件的 layout() 方法\n                setChildFrame(child, childLeft, childTop + getLocationOffset(child),\n                        childWidth, childHeight);\n                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);\n                i += getChildrenSkipCount(child, i);\n            }\n        }\n    }\n```\n\n因为布局方向是垂直方向的，所以在对子元素进行遍历之前，先对自身的顶部的位置进行计算，然后再依次遍历子元素，并对顶部的高度不断叠加，最后调用 `setChildFrame()` 方法:\n\n```java\n    private void setChildFrame(View child, int left, int top, int width, int height) {\n        child.layout(left, top, left + width, top + height);\n    }\n```\n\n这样就完成了对整个 View 树的 `layout()` 方法的调用。\n\n## 4、draw()\n\nView 的 `draw()` 方法实现的逻辑也很清晰。在绘制的过程会按照如下的步骤进行：\n\n1. 绘制背景\n2. 保存 canvas\n3. 绘制自身的内容\n4. 绘制子控件\n5. 绘制 View 的褪色边缘，比如阴影效果之类的\n6. 绘制装饰，比如滚动条之类的\n\nView 中提供了 `onDraw()` 方法用来完成对自身的内容的绘制，所以，我们自定义 View 的时候只要重写这个方法就可以了。当我们要自定义一个 ViewGroup 类型的控件的时候，一般是不需要重写 `onDraw()` 方法的，因为它只需要遍历子控件并依次调用它们的 `draw()` 方法就可以了。（当然，如果非要实现的话，也是可以的。）\n\n下面是这部分代码，代码的注释中也详细注释了每个步骤的逻辑：\n\n```java\n    public void draw(Canvas canvas) {\n        final int privateFlags = mPrivateFlags;\n        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&\n                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);\n        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;\n\n        // Step 1, draw the background, if needed\n        int saveCount;\n\n        if (!dirtyOpaque) {\n            drawBackground(canvas);\n        }\n\n        // skip step 2 & 5 if possible (common case)\n        final int viewFlags = mViewFlags;\n        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;\n        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;\n        if (!verticalEdges && !horizontalEdges) {\n            // Step 3, draw the content\n            if (!dirtyOpaque) onDraw(canvas);\n\n            // Step 4, draw the children\n            dispatchDraw(canvas);\n\n            drawAutofilledHighlight(canvas);\n\n            // Overlay is part of the content and draws beneath Foreground\n            if (mOverlay != null && !mOverlay.isEmpty()) {\n                mOverlay.getOverlayView().dispatchDraw(canvas);\n            }\n\n            // Step 6, draw decorations (foreground, scrollbars)\n            onDrawForeground(canvas);\n\n            // Step 7, draw the default focus highlight\n            drawDefaultFocusHighlight(canvas);\n\n            if (debugDraw()) {\n                debugDrawFocus(canvas);\n            }\n\n            // we're done...\n            return;\n        }\n\n        // ...\n    }\n```\n\n注意到在上面的方法中会调用 `dispatchDraw(canvas)` 方法来分发绘制事件给子控件来完成整个 View 树的绘制。在 View 中，这是一个空的方法，ViewGroup 覆写了这个方法，并在其中调用 `drawChild()` 来完成对指定的 View 的 `draw()` 方法的调用：\n\n```java\n    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {\n        return child.draw(canvas, this, drawingTime);\n    }\n```\n\n而对于 LinearLayout 这样本身没有绘制需求的控件，没有覆写 `onDraw()` 和  `dispatchDraw(canvas)`  等方法，因为 View 和 ViewGroup 中提供的功能已经足够使用。\n\n## 总结：\n\n上文中，我们介绍了在 Android 系统中整个 View 树的工作的流程，从 DecorView 被加载到窗口中，到测量、布局和绘制三个方法的实现。本质上整个工作的流程就是对 View 树的一个深度优先的遍历过程。\n\n"
  },
  {
    "path": "系统架构/控件体系/View体系详解：坐标系、滑动事件和分发机制.md",
    "content": "# View 体系详解：坐标系、滑动、手势和事件分发机制\n\n## 1、位置\n\n### 1.1 坐标系\n\n下面是 Android 中的 View 坐标系的基本图。要获得一个 View 的位置，我们可以借助两个对象，一个是 View ，一个是 MotionEvent。以下是它们的一些方法的位置的含义：\n\n![Android View 坐标系](./resources/坐标系.png)\n\n在 View 中共有 `mLeft`, `mRight`, `mTop` 和 `mBottom` 四个变量包含 View 的坐标信息，你可以在源码中获取它们的含义：\n\n1. `mLeft`：指定控件的左边缘距离其父控件左边缘的位置，单位：像素；\n2. `mRight`：指定控件的右边缘距离其父控件左边缘的位置，单位：像素；\n3. `mTop`：指定控件的上边缘距离其父控件上边缘的位置，单位：像素；\n4. `mBottom`：指定控件的下边缘距离其父控件上边缘的位置，单位：像素。\n\n此外，View 中还有几个方法用来获取控件的位置等信息，实际上就是上面四个变量的 getter 方法：\n\n1. `getLeft()`：即 `mLeft`；\n2. `getRight()`：即 `mRight`；\n3. `getTop()`：即 `mTop`；\n4. `getBottom()`：即 `mBottom`；\n\n所以，我们可以得到两个获取 View 高度和宽度信息的方法：\n\n1. `getHeight()`：即 `mBottom - mTop`；\n2. `getWidth()`：即 `mRight - mLeft`；\n\n另外，就是 View 中的 `getX()` 和 `getY()` 两个方法，你需要注意将其与 MotionEvent 中的同名方法进行区分。在没有对控件进行平移的时候，`getX()` 与 `getLeft()` 返回结果相同，只是前者会在后者的基础上加上平移的距离：\n\n1. `getX()`：即 `mLeft + getTranslationX()`，即控件的左边缘加上 X 方向平移的距离；\n2. `getY()`：即 `mTop + getTranslationY()`，即控件的上边缘加上 Y 方向平移的距离；\n\n以上是我们对 View 中获取控件位置的方法的梳理，你可以到源码中查看它们更加相详尽的定义，那更有助于自己的理解。\n\n### 1.2 MotionEvent\n\n通常当你对控件进行触摸监听的时候会用到 MotionEvent ，它封住了触摸的位置等信息。下面我们对 MotionEvent 中的获取点击事件的位置的方法进行梳理，它主要涉及下面四个方法：\n\n1. `MotionEvent.getX()`：获取点击事件距离控件左边缘的距离，单位：像素；\n2. `MotionEvent.getY()`：获取点击事件距离控件上边缘的距离，单位：像素；\n3. `MotionEvent.getRawX()`：获取点击事件距离屏幕左边缘的距离，单位：像素；\n4. `MotionEvent.getRawY()`：获取点击事件距离屏幕上边缘的距离，单位：像素。\n\n另外是触摸事件中的三种典型的行为，按下、移动和抬起。接下来的代码示例中我们会用到它们来判断手指的行为，并对其做响应的处理：\n\n1. `MotionEvent.ACTION_DOWN`：按下的行为；\n2. `MotionEvent.ACTION_MOVE`：手指在屏幕上移动的行为；\n3. `MotionEvent.ACTION_UP`：手指抬起的行为。\n\n## 2、滑动\n\n我们有几种方式实现 View 的滑动：\n\n### 2.1 layout() 方法\n\n调用控件的 `layout()` 方法进行滑动，下面是该方法的定义：\n\n```java\npublic void layout(int l, int t, int r, int b) { /*...*/ }\n```\n\n其中的四个参数 `l`, `t`, `r`, `b`分别表示控件相对于父控件的左、上、右、下的距离，分别对应于上面的 `mLeft`, `mTop`, `mRight` 和 `mBottom`。所以，调用该方法同时可以改变控件的高度和宽度，但有时候我们不需要改变控件的高度和宽度，只要移动其位置即可。所以，我们又有方法 `offsetLeftAndRight()` 和 `offsetTopAndBottom()` 可以使用，后者只会对控件的位置进行平移。因此，我们可以进行如下的代码测试：\n\n```java\n    private int lastX, lastY;\n\n    private void layoutMove(MotionEvent event) {\n        int x = (int) event.getX(), y = (int) event.getY();\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                lastX = x;\n                lastY = y;\n                break;\n            case MotionEvent.ACTION_MOVE:\n                int offsetX = x - lastX, offsetY = y - lastY;\n                getBinding().v.layout(getBinding().v.getLeft() + offsetX,\n                        getBinding().v.getTop() + offsetY,\n                        getBinding().v.getRight() + offsetX,\n                        getBinding().v.getBottom() + offsetY);\n                break;\n            case MotionEvent.ACTION_UP:\n                break;\n        }\n    }\n```\n\n上面的代码的效果是指定的控件会随着手指的移动而移动。这里我们先记录下按下的位置，然后手指移动的时候记录下平移的位置，最后调用 `layout()` 即可。\n\n### 2.2 offsetLeftAndRight() 和 offsetTopAndBottom()\n\n上面已经提到过这两个方法，它们只改变控件的位置，无法改变大小。我们只需要对上述代码做少量修改就可以实现同样的效果：\n\n```java\n    getBinding().v.offsetLeftAndRight(offsetX);\n    getBinding().v.offsetTopAndBottom(offsetY);\n```\n\n### 2.3 改变布局参数\n\n通过获取并修改控件的 `LayoutParams`，我们一样可以达到修改控件的位置的目的。毕竟，本身这个对象就代表着控件的布局：\n\n```java\n    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getBinding().v.getLayoutParams();\n    lp.leftMargin = getBinding().v.getLeft() + offsetX;\n    lp.topMargin = getBinding().v.getTop() + offsetY;\n    getBinding().v.setLayoutParams(lp);\n```\n\n### 2.4 动画\n\n使用动画我们也可以实现控件移动的效果，这里所谓的动画主要是操作 View 的 `transitionX` 和 `transitionY` 属性：\n\n```java\n    getBinding().v.animate().translationX(5f);\n    getBinding().v.animate().translationY(5f);\n```\n\n关于动画的内容，我们会在后面详细介绍。\n\n### 2.5 scrollTo() 和 scrollBy()\n\n`scrollBy()` 方法内部调用了 `scrollTo()`，以下是这部分的源码。`scrollBy()` 表示在当前的位置上面进行平移，而 `scrollTo()` 表示平移到指定的位置：\n\n```java\n    public void scrollBy(int x, int y) {\n        scrollTo(mScrollX + x, mScrollY + y);\n    }\n```\n\n同样对上述代码进行修改，我们也可以实现之前的效果：\n\n```java\n    ((View) getBinding().v.getParent()).scrollBy(-offsetX, -offsetY);\n```\n\n或者\n\n```java\n    View parent = ((View) getBinding().v.getParent());\n    parent.scrollTo(parent.getScrollX()-offsetX, parent.getScrollY()-offsetY);\n```\n\n此外，还有一个需要注意的地方是：与上面的 `offsetLeftAndRight()` 和 `offsetTopAndBottom()` 不同的是，这里我们用了平移的值的相反数。原因很简单，因为我们要使用这两个方法的时候需要对指定的控件所在的父容器进行调用（正如上面是先获取父控件）。当我们希望控件相对于之前的位置向右下方向移动，就应该让父容器相对于之前的位置向左上方向移动。因为实际上该控件相对于父控件的位置没有发生变化，变化的是父控件的位置。（参考的坐标系不同）\n\n### 2.6 Scroller\n\n上面，我们的测试代码是让指定的控件随着手指移动，但是假如我们希望控件从一个位置移动到另一个位置呢？当然，它们也可以实现，但是这几乎就是在瞬间完成了整个操作，实际的UI效果肯定不会好。所以，为了让滑动的过程看起来更加流畅，我们可以借助 `Scroller` 来实现。\n\n在使用 `Scroller` 之前，我们需要先实例化一个 `Scroller` ：\n\n```java\n    private Scroller scroller = new Scroller(getContext());\n```\n\n然后，我们需要覆写自定义控件的 `computeScroll()` 方法，这个方法会在绘制 View 的时候被调用。所以，这里的含义就是，当 View 重绘的时候会调用 `computeScroll()` 方法，而 `computeScroll()` 方法会判断是否需要继续滚动，如果需要继续滚动的时候就调用 `invalidate()` 方法，该方法会导致 View 进一步重绘。所以，也就是靠着这种不断进行重绘的方式实现了滚动的效果。\n\n滑动效果最终结束的判断是通过 `Scroller` 的 `computeScrollOffset()` 方法实现的，当滚动停止的时候，该方法就会返回 `false`，这样不会继续调用 `invalidate()` 方法，因而也就不会继续绘制了。下面是该方法典型的覆写方式：\n\n```java    \n    @Override\n    public void computeScroll() {\n        super.computeScroll();\n        if (scroller.computeScrollOffset()) {\n            ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY());\n            invalidate();\n        }\n    }\n```\n\n然后，我们再加入一个滚动到指定位置的方法，在该方法内部我们使用了 2000ms 来指定完成整个滑动所需要的时间：\n\n```java\n    public void smoothScrollTo(int descX, int descY) {\n        scroller.startScroll(getScrollX(), getScrollY(), descX - getScrollX(), descY - getScrollY(), 2000);\n        invalidate();\n    }\n```\n\n这样定义了之后，我们只需要在需要滚动的时候调用自定义 View 的 `smoothScrollTo()` 方法即可。\n\n## 3、手势\n\n### 3.1 ViewConfiguration\n\n在类 `ViewConfiguration` 中定义了一些列的常量用来标志指定的行为，比如，`TouchSlop` 就是滑动的最小的距离。你可以通过 `ViewConfiguration.get(context)` 来获取 `ViewConfiguration` 实例，然后通过它的 getter 方法来获取这些常量的定义。\n\n### 3.2 VelocityTracker\n\n`VelocityTracker` 用来检测手指滑动的速率，它的使用非常简单。在使用之前，我们先使用它的静态方法 `obtain()` 获取一个实例，然后在 `onTouch()` 方法中调用它的 `addMovement(MotionEvent)` 方法：\n\n```java\n    velocityTracker = VelocityTracker.obtain();\n```\n\n随后，当我们想要获得速率的时候，先调用 `computeCurrentVelocity(int)` 传入一个时间片段，单位是毫秒，然后调用 `getXVelocity()` 和 `getYVelocity()` 分别获得在水平和竖直方向上的速率即可：\n\n```java\n    velocityTracker.computeCurrentVelocity((int) duration);\n    getBinding().tvVelocity.setText(\"X:\" + velocityTracker.getXVelocity() + \"\\n\"\n            + \"Y:\" + velocityTracker.getYVelocity());\n```\n\n本质上，计算速率的时候是用指定时间的长度变化除以我们传入的时间片。当我们使用完了 `VelocityTracker` 之后，需要回收资源：\n\n```java\n    velocityTracker.clear();\n    velocityTracker.recycle();\n```\n\n### 3.3 GestureDectector\n\n`GestureDectector` 用来检测手指的手势。在使用它之前我们需要先获取一个 `GestureDetector` 的实例：\n\n```java\n    mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener());\n```\n\n这里我们用了 `GestureDetector` 的构造方法，需要传入一个 `OnGestureListener` 对象。这里我们用了 `MyOnGestureListener` 实例。 `MyOnGestureListener` 是一个自定义的类，实现了 `OnGestureListener` 接口：\n\n```java\n    private class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener {\n\n        @Override\n        public boolean onSingleTapUp(MotionEvent e) {\n            ToastUtils.makeToast(\"Click detected\");\n            return false;\n        }\n\n        @Override\n        public void onLongPress(MotionEvent e) {\n            LogUtils.d(\"Long press detected\");\n        }\n\n        @Override\n        public boolean onDoubleTap(MotionEvent e) {\n            LogUtils.d(\"Double tab detected\");\n            return true;\n        }\n\n        @Override\n        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {\n            LogUtils.d(\"Fling detected\");\n            return true;\n        }\n    }\n```\n\n在 `MyOnGestureListener` 中，我们覆写了它的一些方法。比如，单击、双击和长按等等，当检测到相应的手势的时候这些方法就会被调用。\n\n然后，我们可以这样使用 `GestureDetector`，只要在控件的触摸事件回调中调用即可：\n\n```java\n    getBinding().vg.setOnTouchListener((v, event) -> {\n        mGestureDetector.onTouchEvent(event);\n        return true;\n    });\n```\n\n## 4、事件分发机制\n\n### 4.1 事件传递的过程\n\n当讨论事件分发机制的时候，我们首先要了解 Android 中 `View` 的组成结构。在 Android 中，一个 Activity 包含一个 `PhoneWindow`，当我们在 Activity 中调用 `setContentView()` 方法的时候，会调用该 `PhoneWindow` 的 `setContentView()` 方法，并在这个方法中生成一个 `DecorView` 作为 Activity 的跟 `View`。\n\n根据上面的分析，当一个点击事件被触发的时候，首先接收到该事件的是 `Activity`。因为，`Activity` 覆盖了整个屏幕，我们需要先让它接收事件，然后它把事件传递给根 `View` 之后，再由根 `View` 向下继续传递。这样不断缩小搜索的范围，直到最顶层的 `View`。当然，任何的父容器都可以决定这个事件是不是要继续向下传递，因此，我们可以大致得到下面这个事件传递的图：\n\n![事件传递图](./resources/事件分发机制.png)\n\n左边的图是一个 Activity 内部的 `View` 和 `Window` 的组织结构。右面的图可以看作它的切面图，其中的黑色箭头表示事件的传递过程。这里事件传递的过程是先从下到上，然后再从上到下。也就是从大到小，不断定位到触摸的控件，其中每个父容器可以决定是否将事件传递下去。（需要注意的地方是，如果一个父容器有多个子元素的话，那么在这些子元素中进行遍历的时候，顺序是从上往下的，也就是按照展示的顺序）。\n\n上面我们分析了 Android 事件传递的过程，相信你有了一个大致的了解。但是，想要了解整个事件传递过程具体涉及了哪些方法、如何作用等，还需要我们对源码进行分析。\n\n### 4.2 事件传递的原理\n\n当触摸事件发生的时候，首先会被 Activity 接收到，然后该 Activity 会通过其内部的 `dispatchTouchEvent(MotionEvent)` 将事件传递给内部的 `PhoneWindow`；接着 `PhoneWindow` 会把事件交给 `DecorView`，再由 `DecorView` 交给根 `ViewGroup`。剩下的事件传递就只在 `ViewGroup` 和 `View` 之间进行。我们可以通过覆写 Activity 的 `dispatchTouchEvent(MotionEvent)` 来阻止把事件传递给 `PhoneWindow`。实际上，在我们开发的时候不会对 `Window` 的事件传递方法进行重写，一般是对 `ViewGroup` 或者 `View`。所以，下面我们的分析只在这两种控件之间进行。\n\n当讨论 View 的事件分发机制的时候，无外乎下面三个方法：\n\n1. `boolean onInterceptTouchEvent(MotionEvent ev)`：用来对事件进行拦截，该方法只存在于 ViewGroup 中。一般我们会通过覆写该方法来拦截触摸事件，使其不再继续传递给子 View。\n2. `boolean dispatchTouchEvent(MotionEvent event)`：用来分发触摸事件，一般我们不覆写该方法，返回 `true` 则表示事件被处理了。在 View 中，它负责根据手势的类型和控件的状态对事件进行处理，会回调我们的 `OnTouchListener` 或者 `OnClickListener`；在 ViewGroup 中，该方法被覆写，它的责任是对事件进行分发，会对所有的子 View 进行遍历，决定是否将事件分发给指定的 View。\n3. `boolean onTouchEvent(MotionEvent event)`：用于处理触摸事件，返回 `true` 表示触摸事件被处理了。ViewGroup 没有覆写该方法，故在 ViewGroup 中与 View 中的功能是一样的。需要注意的是，如果我们为控件设置了 `OnTouchListener` 并且在或者中返回了 `true`，那么这个方法不会被调用，也就是 `OnTouchListener` 比该方法的优先级较高。对我们开发来说，就是 `OnTouchListener` 比 `OnClickListener` 和 `OnLongClickListener` 的优先级要高。\n\n于是，我们可以得到如下的伪代码。这段代码是存在于 ViewGroup 中的，也就是事件分发机制的核心代码：\n\n```java\n    boolean dispatchTouchEvent(MotionEvent e) {\n        boolean result;\n        if (onInterceptTouchEvent(e)) {\n            result = super.dispatchTouchEvent(e);\n        } else {\n            result = child.dispatchTouchEvent(e);\n        }\n        return result;\n    }\n```\n\n按照上述分析，触摸事件经过 Activity 传递给根 ViewGroup 之后：\n\n如果 ViewGourp 覆写了 `onInterceptTouchEvent()` 并且返回了 `true` 就表示希望拦截该方法，于是就把触摸事件交给当前 ViewGroup 进行处理（触发  `OnTouchListener` 或者  `OnClickListener` 等）；否则，会交给子元素的继续分发。如果该子元素是 ViewGroup 的话，就会在该子 View 中执行一遍上述逻辑，否则会在当前的子元素中对事件进行处理（触发  `OnTouchListener` 或者  `OnClickListener` 等）……就这样一层层地遍历下去，本质上是一个深度优先的搜索算法。\n\n这里我们对整个事件分发机制的整体做了一个素描，在接下来的文章中我们会对各个方法的细节进行源码分析，为了防止您在接下来的行文中迷路，我们先把这个整体逻辑按下图进行描述：\n\n![事件分发机制原理](./resources/事件分发机制原理.png)\n\n### 4.3 事件传递的源码分析\n\n上述我们分析了事件分发机制的原理，下面我们通过源代码来更具体地了解这块是如何设计的。同样，我们的焦点也只在那三个需要重点关注的方法。\n\n#### 4.3.1 决定是否拦截事件\n\n首先，我们来看 ViewGroup 中的 `dispatchTouchEvent(MotionEvent)` 方法，我们节选了其一部分：\n\n```java\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent ev) {\n        // ...\n        boolean handled = false;\n        if (onFilterTouchEventForSecurity(ev)) {\n            final int action = ev.getAction();\n            final int actionMasked = action & MotionEvent.ACTION_MASK;\n            if (actionMasked == MotionEvent.ACTION_DOWN) { // 1\n                // 这里表示如果是一个新的触摸事件就要重置所有的状态，其中包括将 mFirstTouchTarget 置为 null\n                cancelAndClearTouchTargets(ev);\n                resetTouchState();\n            }\n            // 在这里检查是否拦截了事件，mFirstTouchTarget 是之前处理触摸事件的 View 的封装\n            final boolean intercepted;\n            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {\n                // 这里判断该 ViewGroup 是否禁用了拦截，由 requestDisallowInterceptTouchEvent 设置\n                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;\n                if (!disallowIntercept) {\n                    intercepted = onInterceptTouchEvent(ev);\n                    ev.setAction(action);\n                } else {\n                    intercepted = false;\n                }\n            } else {\n                // 非按下事件并且 mFirstTouchTarget 为 null，说明判断过拦截的逻辑并且启用了拦截\n                intercepted = true;\n            }\n            // ...           \n        }\n        // ...\n        return handled;\n    }\n```\n\n上面代码是我们节选的 ViewGroup 拦截事件的部分代码，这里的逻辑显然比伪代码复杂的多。不过，尽管如此，这些代码确实必不可少的。因为，当我们要去判断是否拦截一个触摸事件的时候，此时触摸的事件仍然在继续，这意味着这个方法会被持续调用；抬起的时候再按下，又是另一次调用。考虑到这个连续性，我们需要多做一些逻辑。\n\n这里我们首先在 1 处通过行为是否是“按下”的来判断是否是一次新的触摸事件，如果是的话我们需要重置当前的触摸状态。其次，我们需要根据事件的类型来决定是否应该调用 `onInterceptTouchEvent()`，因为对一次触摸事件，我们只需要在“按下”的时候判断一次就够了。所以，显然我们需要将 `MotionEvent.ACTION_DOWN` 作为一个判断条件。然后，我们使用 `mFirstTouchTarget` 这个全局的变量来记录上次拦截的结果——如果之前的事件交给过子元素处理，那么它就不为空。\n\n除了 `mFirstTouchTarget`，我们还需要用 `mGroupFlags` 的 `FLAG_DISALLOW_INTERCEPT` 标志位来判断该 ViewGroup 是否禁用了拦截。这个标志位可以通过 ViewGroup 的 `requestDisallowInterceptTouchEvent(boolean)` 来设置。只有没有禁用拦截事件的时候我们才需要调用 `onInterceptTouchEvent()` 判断是否开启了拦截。\n\n#### 4.3.2 分发事件给子元素\n\n如果在上面的操作中事件没有被拦截并且没有被取消，那么就会进入下面的逻辑。这部分代码处在 `dispatchTouchEvent()` 中。在下面的逻辑中会根据子元素的状态将事件传递给子元素：\n\n```java\n    // 对子元素进行倒序遍历，即从上到下进行遍历\n    final View[] children = mChildren;\n    for (int i = childrenCount - 1; i >= 0; i--) {\n        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);\n        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);\n        // ...\n        // 判断子元素是否能接收触摸事件：能接收事件并且不是正在进行动画的状态\n        if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {\n            ev.setTargetAccessibilityFocus(false);\n            continue;\n        }\n        // ...\n        // 在这里调用了 dispatchTransformedTouchEvent() 方法将事件传递给子元素\n        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {\n            // ... 记录一些状态信息\n            // 在这里完成对 mFirstTouchTarget 的赋值，表示触摸事件被子元素处理\n            newTouchTarget = addTouchTarget(child, idBitsToAssign);\n            alreadyDispatchedToNewTouchTarget = true;\n            // 结束循环，完成子元素的遍历\n            break;\n        }\n        // 显然，如果到了这一步，那么子元素的遍历仍将继续\n    }\n```\n\n当判断了指定的 View 可以接收触摸事件之后会调用 `dispatchTransformedTouchEvent()` 方法分发事件。其定义的节选如下：\n\n```java\n    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {\n        final boolean handled;\n        // ...\n        if (child == null) {\n            // 本质上逻辑与 View 的 dispatchTouchEvent() 一致\n            handled = super.dispatchTouchEvent(transformedEvent);\n        } else {\n            // ...\n            // 交给子元素继续分发事件\n            handled = child.dispatchTouchEvent(transformedEvent);\n        }\n        return handled;\n    }\n```\n\n`dispatchTransformedTouchEvent()` 会根据传入的 `child` 是否为 `null` 分成两种调用的情形：事件没有被拦截的时候，让子元素继续分发事件；另一种是当事件被拦截的时候，调用当前的 ViewGroup 的 `super.dispatchTouchEvent(transformedEvent)` 处理事件。\n\n#### 4.3.3 View 中的 dispatchTouchEvent\n\n上面我们分析的 `dispatchTouchEvent(MotionEvent)` 是 ViewGroup 中重写之后的方法。但是，正如我们上面的分析，重写之前的方法总是会被调用，只是对象不同。这里我们就来分析以下这个方法的作用。\n\n```java\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        // ...\n        boolean result = false;\n        // ....\n        if (onFilterTouchEventForSecurity(event)) {\n            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {\n                result = true;\n            }\n            // 这里回调了 setOnTouchListener() 方法传入的 OnTouchListener\n            ListenerInfo li = mListenerInfo;\n            if (li != null && li.mOnTouchListener != null\n                    && (mViewFlags & ENABLED_MASK) == ENABLED\n                    && li.mOnTouchListener.onTouch(this, event)) {\n                result = true;\n            }\n            // 如果 OnTouchListener 没有被回调过或者返回了 false，就会调用 onTouchEvent() 进行处理\n            if (!result && onTouchEvent(event)) {\n                result = true;\n            }\n        }\n        // ...\n        return result;\n    }\n```\n\n根据上面的源码分析，我们知道，如果当前的 View 设置过 `OnTouchListener`, 并且在 `onTouch()` 回调方法中返回了 `true`，那么 `onTouchEvent(MotionEvent)` 将不会得到调用。那么，我们再来看一下 `onTouchEvent()` 方法： \n\n```java\n    public boolean onTouchEvent(MotionEvent event) {\n        // ...\n        // 判断当前控件是否是可以点击的：实现了点击、长按或者设置了可点击属性\n        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE\n                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)\n                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;\n        // ...\n        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {\n            switch (action) {\n                case MotionEvent.ACTION_UP:\n                    // ...\n                    if (!focusTaken) {\n                        if (mPerformClick == null) {\n                            mPerformClick = new PerformClick();\n                        }\n                        if (!post(mPerformClick)) {\n                            performClick();\n                        }\n                    }\n                    // ...\n                    break;\n                case MotionEvent.ACTION_DOWN:\n                    // ...\n                    if (!clickable) {\n                        checkForLongClick(0, x, y);\n                        break;\n                    }\n                    // ...\n                    break;\n                // ...\n            }\n            return true;\n        }\n        return false;\n    }\n```\n\n这里先判断指定的控件是否是可点击的，即是否设置过点击或者长按的事件。然后会在手势抬起的时候调用 `performClick()` 方法，并会在这个方法中尝试从 `ListenerInfo` 取 `OnClickListener` 进行回调；会在长按的时候进行监听以调用相应长按事件；其他的事件与之类似，可以自行分析。所以，我们可以得出结论，当为控件的触摸事件进行了赋值并且在其中返回了 `false` 那么就代表这个触摸事件被消耗了。这个触摸事件的优先级比较高，即使设置过单击和长按事件的回调，它们也不会被调用。\n\n经过上述分析，我们可以知道 View 中的 `dispatchTouchEvent(MotionEvent)` 方法就是用来对手势进行处理的，所以回到 `4.3.2`，那里的意思就是：如果 ViewGroup 拦截了触摸事件，那么它就自己来对事件进行处理；否则就把触摸事件传递给子元素，让它来进行处理。\n\n#### 4.4.4 总结\n\n以上就是我们对 Android 中事件分发机制的详解，你可以通过图片和代码结合来更透彻得了解这方面的内容。虽然这部分代码比较多、比较长，但是每个地方的设计都是合情合理的。\n\n## 源代码\n\n你可以在Github获取以上程序的源代码： [Android-references](https://github.com/Shouheng88/Android-references)。\n"
  },
  {
    "path": "系统架构/控件体系/View体系详解：自定义控件.md",
    "content": "# View 体系详解：自定义控件\n\n在自定义控件之前，我们应该至少了解 Android 中控件体系的基础内容，你可以通过下面的三篇文章来获取这方面的知识：\n\n1. [《View 体系详解：坐标系、滑动、手势和事件分发机制》](https://juejin.im/post/5bbb5fdce51d450e942f6be4)\n2. [《View 体系详解：View 的工作流程》](https://shouheng88.github.io/2018/10/14/View%20%E4%BD%93%E7%B3%BB%E8%AF%A6%E8%A7%A3%EF%BC%9AView%20%E7%9A%84%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B/)\n3. [《Android 动画详解：属性动画、View 动画和帧动画》](https://juejin.im/post/5bb884506fb9a05cf9084857)\n\n根据我们的业务需求我们可以有多种方式来实现一个自定义控件，当然，最好能够复用已有的控件。下面是几种常见的方式：\n\n1. 通过继承系统控件进行自定义；\n2. 通过布局文件自定义组合控件；\n3. 通过继承 View 进行自定义。\n\n实际上，前面的两种没有太多需要介绍的东西，毕竟，就像之前有人说的，现在的 Android 开发者哪有什么新手，所以，这里我们只大致介绍一下，然后对一些细节的内容进行整理。对于第三种方式，内容会比较多一些，我们会尽量更加详尽地介绍。\n\n## 1、通过继承系统控件进行自定义\n\n这种方式通过继承某个系统的控件，以在其基础之上实现我们需要的功能，这算是 COST 比较小的一种方式。当然，具体如何去实现是根据个人的需求来定的，我们无法一一俱到。我觉得做好这种自定义的前提是对 Android 系统 View 体系的理解，你可以通过前面的一些文章来了解这块的内容。\n\n下面我们以实现正方形的布局为例来说明如何实现这种控件的定义：\n\n    public class SquareFrameLayout extends FrameLayout {\n        // ...\n        @SuppressWarnings(\"unused\")\n        @Override\n        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n            setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec));\n            int childWidthSize = getMeasuredWidth();\n            int childHeightSize = getMeasuredHeight();\n            heightMeasureSpec = widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        }\n    }\n\n上面，我们覆写了 `FrameLayout` 的测量方法，在最终确定测量结果的时候调用了基类的 `onMeasure()` 并把为高度和宽度参数传入相同的值，以实现高度和宽度相同。\n\n除了实现 `onMeasure()` 方法，我们还可以实现 `onDraw()`，或者拦截触摸事件，或者弹出一个 `Window` 等来实现特定的功能。\n\n## 2、通过布局文件自定义组合控件\n\n### 2.1 组合控件的实现方式\n\n这种方式也是比较常用的一种方式，它的做法是：先通过布局文件组合已有的控件实现需要的功能，然后通过覆写现有的容器控件，在加载控件的时候把当前的容器作为其容器，最后把整个控件的逻辑用代码实现即可。这种方式不会降低布局的复杂程度，效率比直接绘制的方式低，但是更加方便，用得也最多，其好处是把在多个地方都用到的组合控件封装起来，统一代码实现，降低维护成本。\n\n    public class EmptyView extends LinearLayout {\n        // ...\n        public EmptyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n            super(context, attrs, defStyleAttr);\n            init(context, attrs);\n        }\n        private void init(Context context, AttributeibuteSet) {\n            View root = LayoutInflater.from(context).inflate(R.layout.widget_empty_view, this, true);\n        }\n    }   \n\n像上面这样，我们在布局文件 `widget_empty_view` 中写好了这个控件的布局，然后通过 `LayoutInflater` 获取并将其父容器指向当前的控件 `this`，然后我们可以从 `root` 中获取布局中的控件，并对其进行处理即可。\n\n上面的方式我们不去详细介绍，这里对于自定义控件的布局属性进行一些整理。\n\n### 2.2 布局属性\n\n我们可以为自定义控件指定一些属性，主要用在在 `xml` 中。首先，我们在 `attr.xml` 中使用 `declare-styleable` 标签，这里的 `name` 属性是我们自定义控件的名称，然后内部的子标签用来指定自定义属性和属性的值类型。\n\n    <declare-styleable name=\"EmptyView\">\n        <attr name=\"title\" format=\"string\"/>\n        <!-- ... -->\n    </declare-styleable>\n\n然后我们可以在自定义控件中通过下面的代码来获取属性值，并赋值给控件。这里的 `attributeSet` 来自自定义控件的构造方法：\n\n    TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.EmptyView, 0, 0);\n    String bottomTitle = attr.getString(R.styleable.EmptyView_title);\n    attr.recycle();\n\n上面我们给出的是自定义属性的一个非常基本的用法，如果想要对自定义属性有更多的了解，Android 系统控件的源码是非常好的参考资料。有拿不准的地方可以直接参考系统控件的源码，这里不再做更多的说明。\n\n## 3、通过继承 View 进行自定义\n\n\n\n"
  },
  {
    "path": "系统架构/控件体系/动画体系详解.md",
    "content": "# Android 动画详解：属性动画、View 动画和帧动画\n\n在 Android 中，基本的动画共有三种类型：\n\n1. **View 动画**：也叫视图动画或者补间动画，主要是指 `android.view.animation` 包下面的一些类，只能被用来设置给 View，缺点是比如当控件移动之后，接收点击的控件的位置不会跟随移动，并且能够实现的效果只有移动、缩放、旋转和淡入淡出操作四种及其组合。\n2. **Drawable 动画**：也叫 Frame 动画或者帧动画，其实可以划分到视图动画的类别，实现方式是将一些列的 Drawable 像幻灯片一样一个一个地显示。\n3. **Property 动画**： 属性动画主要是指 `android.animation` 包下面的一些类，只对 `API 11` 以上版本的Android 系统才有效，但我们可以通过兼容库做低版本兼容。这种动画可以设置给任何 Object，包括那些还没有渲染到屏幕上的对象。这种动画是可扩展的，可以让你自定义任何类型和属性的动画。\n\n## 1、Drawable 动画\n\n在这里，我们先对 Drawable 动画进行讲解，因为它相对于后面的两种动画比较简单。在示例程序我们准备了一系列图片资源，并在 drawable 文件夹下面定义了动画资源 record_anim.xml：\n\n```\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animation-list android:oneshot=\"false\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@drawable/record0\" android:duration=\"500\"/>\n    <item android:drawable=\"@drawable/record1\" android:duration=\"500\"/>\n    <item android:drawable=\"@drawable/record2\" android:duration=\"450\"/>\n    <item android:drawable=\"@drawable/record3\" android:duration=\"400\"/>\n    <item android:drawable=\"@drawable/record4\" android:duration=\"350\"/>\n    <item android:drawable=\"@drawable/record5\" android:duration=\"400\"/>\n    <item android:drawable=\"@drawable/record6\" android:duration=\"400\"/>\n</animation-list>\n```\n\n然后，我们在代码中使用该资源，并将其赋值给 ImageView。然后，我们从该控件中获取该 Drawable 并将其转换成 AnimationDrawable，随后我们调用它的 `start()` 方法就开启了 Drawable 动画：\n\n    getBinding().ivRecord.setImageResource(R.drawable.record_anim);\n    animDraw = (AnimationDrawable) getBinding().ivRecord.getDrawable();\n    animDraw.start();\n\n此外，我们可以调用该 Drawable 的 `stop()` 方法停止动画。\n\n###  帧动画的注意事项\n\n使用帧动画的时候要注意设置的图片不宜过多、过大，以防止因为内存不够而出现 OOM。\n\n## 2、View 动画\n\n### 2.1 基本 View 动画\n\n该动画的资源处在 `android.view.animation` 包下，主要有以下几个类，它们都继承自 `Animation` ，我们可以使用它们来实现复杂的动画。这些动画类分别有对应的 xml 标签，所以，我们可以在 xml 中定义动画，也可以在代码中实现动画效果。这里的 `AnimationSet` 可以用来将多个动画效果进行组合，各预定义动画的对照可以参考下面这张图表：\n\n![Android View 动画框架](./resources/View动画框架.png)\n\n### 2.2 View 动画属性\n\n当然，要实现一种动画效果会有许多属性需要指定，在 xml 中，我们用标签的属性指定，在代码中我们用对象的 setter 方法指定。于是，我们可以得到下面这个对应关系：\n\n![Android View 动画属性详解](./resources/View动画属性详解.png)\n\n上面的对应关系是所有的 View 动画共用的，对各个具体的动画类型还有其独有的属性。你可以在各个动画的构造方法中，通过它们从 `AttributeSet` 中获取了哪些字段来了解它们都定义了哪些属性，这里我们不对其一一进行说明。各预定义的属性动画分别按照不同的方式实现了 `Animation` 的 `applyTransformation` 方法，具体的这些属性如何使用以及 View 动画的效果是如何实现的，都可通过阅读该方法的定义得知。\n\n对于 `AnimationSet`，它内部维护了一个 `Animation` 列表，并且其本身也是一个 `Animation`，所以，`AnimationSet` 内部可以添加子 `AnimationSet`。\n\n### 2.3 插值器\n\n上文中我们提到过，View 动画的具体实现是通过覆写 `Animation` 的 `applyTransformation` 方法来完成的。这里我们以 `AlphaAnimation` 为例来看它是如何作用的，同时你应该注意插值器的作用原理。该方法会在 `Animation` 中被循环调用，调用的时候会根据插值器计算出一个时间，并将其传递到 `applyTransformation` 方法中。\n\n`Animation` 的 `getTransformation` 方法片段：\n\n    if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {\n        if (!mStarted) {\n            fireAnimationStart();\n            mStarted = true;\n            if (NoImagePreloadHolder.USE_CLOSEGUARD) {\n                guard.open(\"cancel or detach or getTransformation\");\n            }\n        }\n\n        if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);\n\n        if (mCycleFlip) {\n            normalizedTime = 1.0f - normalizedTime;\n        }\n\n        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);\n        applyTransformation(interpolatedTime, outTransformation);\n    }\n\n`AlphaAnimation` 的 `applyTransformation` 方法:\n\n    protected void applyTransformation(float interpolatedTime, Transformation t) {\n        final float alpha = mFromAlpha;\n        t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));\n    }\n\n显然，这里的 `interpolatedTime` 的是一个比例。比如，假如一个透明动画需要持续 `10s`，透明度需要从 `0.5f` 到 `1.0f`，而插值的规则是一个二次函数。那么第 `t (0<t<10)` 秒的时候控件的透明度应该是：\n\n    alpha = 0.5f + (1.0f - 0.5f) * t^2 / 100\n\n以上就是插值器的作用原理，你也可以按照自己的需求实现自己的插值器，从而实现期待的动画效果。\n\n### 2.4 使用 View 动画\n\n作为一个例子，这里我们实现一个让控件抖动的动画。在 anim 文件夹下面，我们定义一个平移的动画，并使用插值器使其重复：\n\n`anim/shake.xml` 的定义：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <translate xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:duration=\"700\"\n        android:fromXDelta=\"0.0\"\n        android:interpolator=\"@anim/cycle_7\"\n        android:toXDelta=\"15.0\" />\n\n插值器 `anim/cicle_7.xml` 的定义：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <cycleInterpolator xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:cycles=\"4.0\" />\n\n然后，我们在代码中加载 `Animation` 并调用控件的 `startAnimation()` 方法开启动画：\n\n    getBinding().v.startAnimation(AnimationUtils.loadAnimation(this, R.anim.shake));\n\n对于 `View` ，我们有 `startAnimation()` 用来对 `View` 开始动画；有 `clearAnimation()` 用来取消 `View` 在执行的动画。\n\n不使用 xml，仅使用代码我们一样可以实现上述的效果，这里我们不再进行说明。\n\n### 2.5 View 动画的特殊使用场景\n\n#### 2.5.1 LayoutAnimation\n\n`LayoutAnimation` 作用于 `ViewGroup`，可以使其子元素出场时都均有某种动画效果，通常用于 `ListView`。我们可以像下面这样定义布局动画：\n\n    <?xml version=\"1.0\" encoding=\"utf-8\"?>\n    <layoutAnimation \n        android:delay=\"500\"\n        android:animation=\"@anim/shake\"\n        xmlns:android=\"http://schemas.android.com/apk/res/android\" />\n\n显然，这里我们需要引用一个其他的动画。然后，我们可以在 `ListView` 的 `layoutAnimation` 属性中指定布局动画。或者调用 `ListView` 的 `setLayoutAnimation()` 方法应用上述动画。\n\n#### 2.5.2 Activity 切换\n\n我们可以通过在 `Activity` 中调用 `overridePendingTransition(R.anim.shake, R.anim.shake);` 方法来重写 `Activity` 的切换动画。注意这个方法应该在 `startActivity(Intent)` 或者 `finish()` 之后立即调用。\n \n## 3、属性动画\n\n### 3.1 基础梳理\n\n我们可以对比 View 动画来学习属性动画。\n\n1. 属性动画主要是指 `android.animation` 包下面的一些类。\n2. 属性动画基础的动画类是 `Animator`；属性动画也为我们提供了几个预定义的类：`AnimatorSet`, `ObjectAnimator`, `TimeAnimator` 和 `ValueAnimator`；这几个预定义类之间的继承关系是，`AnimatorSet` 和 `ValueAnimator` 直接继承自 `Animator`，而 `ObjectAnimator` 和 `TimeAnimator` 继承自 `ValueAnimator`。\n3. 与 View 动画不同的是，属性动画的使用范围更加宽泛，它不局限于 View，本质上它是通过修改控件的属性值实现的动画。当你尝试对某个对象的某个属性进行修改的时候会有一些限制，即属性动画要求该对象必须提供了该属性的 `setter` 方法。\n4. 属性动画也有 `xml` 和代码两种定义方式，它的 `xml` 通常定义在 `animator` 文件夹下面，而 View 动画定义在 `anim` 文件夹下面。\n5. 属性动画提供了类似于 `AnimationUtils` 的方法用来从布局文件夹中加载属性动画：`AnimatorInflater` 类的 `loadAnimator()` 方法。\n6. 属性动画也有自己的插值器：`TimeInterpolator`，并且也提供了几个预定义的插值器。\n7. 我们也可以调用 `View` 的方法来使用属性动画，我们可以通过 View 的 `animate()` 方法获取一个 `ViewPropertyAnimator`，然后调用 `ViewPropertyAnimator` 的其他方法进行链式调用以实现复杂的属性动画效果。\n\n下面是属性动画的代码实现和 `xml` 实现两种方式的对比：\n\n![属性动画](./resources/属性动画.png)\n\n上文中，我们总结了属性动画的一些知识，并将其与 View 动画进行了对比。这里是一个简单的梳理，在下文中我们会对属性动画进行更加详细的介绍。\n\n### 3.2 使用属性动画\n\n#### 3.2.1 ValueAnimator\n\n上面说过 `ValueAnimator` 是 `ObjectAnimator` 和 `TimeAnimator` 的基类，我们可以这样使用它：\n\n    ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);\n    anim.setDuration(300);\n    anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n        @Override\n        public void onAnimationUpdate(ValueAnimator animation) {\n            float currentValue = (float) animation.getAnimatedValue();\n            Log.d(\"TAG\", \"cuurent value is \" + currentValue);\n        }\n    });\n    anim.start();\n\n这里我们使用 `log` 输出了值渐变的过程，从日志中可以看出它的效果是值从 `0` 不断递增直到 `1`。如果我们在这个监听方法中根据值修改控件的属性一样可以实现动画效果。除了 `ofFloat()` 还有 `ofInt()` 等方法，它们的效果相似。 \n\n#### 3.2.2 ObjectAnimator\n\n上面，如果我们想要实现动画效果，需要在 `ValueAnimator` 的监听事件中修改对象的属性，这里的 `ObjectAnimator` ，我们只需要传入对象实例和属性的字符串名称，修改对象属性的操作就可以自动完成。比如下面的程序的效果是控件 `textview` 的透明度会在 `5s` 之内从 1 变成 0 再变回 1.\n\n    ObjectAnimator animator = ObjectAnimator.ofFloat(textview, \"alpha\", 1f, 0f, 1f);\n    animator.setDuration(5000);\n    animator.start();\n\n注意这里我们传入的是 `alpha`，这个字段本身并不存在于控件中，而是有一个 `setAlpha()` 的方法。也就是说，`ObjectAnimator` 作用的原理是通过反射触发 `setter` 方法而不是修改属性来实现的。你可以在类 `PropertyValuesHolder` 中更详细地了解这方面的内容。\n\n`PropertyValuesHolder` 包装了我们要修改的属性的对象和方法等信息，然后会使用反射触发指定对象的方法来完成对对象属性的修改。其中\n\n    void setupSetter(Class targetClass) {\n        Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();\n        mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, \"set\", propertyType);\n    }\n\n会去寻找我们要修改属性的 `setter` 方法，然后\n\n    void setAnimatedValue(Object target) {\n        if (mProperty != null) {\n            mProperty.set(target, getAnimatedValue());\n        }\n        if (mSetter != null) {\n            try {\n                mTmpValueArray[0] = getAnimatedValue();\n                mSetter.invoke(target, mTmpValueArray);\n            } catch (InvocationTargetException e) {\n                Log.e(\"PropertyValuesHolder\", e.toString());\n            } catch (IllegalAccessException e) {\n                Log.e(\"PropertyValuesHolder\", e.toString());\n            }\n        }\n    }\n    \n会去触发 `setter` 方法，以修改对象的属性。\n\n#### 3.2.3 AnimatorSet\n\n`AnimatorSet` 内部提供了一个构建者 `AnimatorSet.Builder` 来帮助我们构建组合动画，`AnimatorSet.Builder` 提供了下面四种方法：\n\n1. `after(Animator anim)`：将现有动画插入到传入的动画之后执行\n2. `after(long delay)`：将现有动画延迟指定毫秒后执行\n3. `before(Animator anim)`：将现有动画插入到传入的动画之前执行\n4. `with(Animator anim)`：将现有动画和传入的动画同时执行\n\n当我们调用 `AnimatorSet` 的 `play()` 方法的时候就能获取一个 `AnimatorSet.Builder` 实例，然后我们就可以使用构建者的方法进行链式调用了：\n\n    ObjectAnimator moveIn = ObjectAnimator.ofFloat(textview, \"translationX\", -500f, 0f);\n    ObjectAnimator rotate = ObjectAnimator.ofFloat(textview, \"rotation\", 0f, 360f);\n    ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(textview, \"alpha\", 1f, 0f, 1f);\n    AnimatorSet animSet = new AnimatorSet();\n    animSet.play(rotate).with(fadeInOut).after(moveIn);\n    animSet.setDuration(5000);\n    animSet.start();\n\n#### 3.2.4 TypeEvaluator\n\n正如前文所述，属性动画也有自己的插值器，我们可以通过插值函数指定在某个时间段内属性改变的速率。插值函数得到的是一个比例，是没有意义的。在 View 动画的 `AlphaAnimation` 中，如果我们指定了起止的透明度，那么我们可以通过透明度的计算规则得到某个时刻的透明度。但是对于属性动画，因为它可以应用于任何属性，这个属性又可能是任何类型的，那么这个属性将采用什么样的计算规则呢？这就需要我们使用 `TypeEvaluator` 来指定一个计算规则。也就是说，`TypeEvaluator` 是属性动画的属性的计算规则。\n\n下面是 `TypeEvaluator` 的定义，这里的三个参数的含义分别是，`fraction` 是当前的比例，可以通过插值器计算得到；`startValue` 和 `endValue` 分别是属性变化的起止值。它的返回结果就是在某个时刻某个属性的值。\n\n    public interface TypeEvaluator<T> {\n        public T evaluate(float fraction, T startValue, T endValue);\n    }\n\n属性动画中已经为我们提供了几个预定义的 `TypeEvaluator`，比如 `FloatEvaluator`：\n\n    public class FloatEvaluator implements TypeEvaluator<Number> {\n        public Float evaluate(float fraction, Number startValue, Number endValue) {\n            float startFloat = startValue.floatValue();\n            return startFloat + fraction * (endValue.floatValue() - startFloat);\n        }\n    }\n\n在属性动画的 `PropertyValuesHolder` 中会根据属性的类型选择预定义的 `TypeEvaluator`。但是如果我们的属性的类型不在预定义的范围之内就需要自己实现一个 `TypeEvaluator`。下面我们以日期类型为例来实现一个 `TypeEvaluator`。\n\n当我们使用 `ValueAnimator` 的 `ofObject()` 方法获取 `ValueAnimator` 实例的时候，要求我们传入一个 `TypeEvaluator`，于是我们可以像下面这样定义：\n\n    private static class DateEvaluator implements TypeEvaluator<Date> {\n\n        @Override\n        public Date evaluate(float fraction, Date startValue, Date endValue) {\n            long startTime = startValue.getTime();\n            return new Date((long) (startTime + fraction * (endValue.getTime() - startTime)));\n        }\n    }\n\n然后，我们可以这样使用它：\n\n    ValueAnimator animator = ValueAnimator.ofObject(new DateEvaluator(), new Date(0), new Date());\n    animator.setDuration(5000);\n    animator.addUpdateListener(animation -> {\n        Date date = (Date) animation.getAnimatedValue();\n        LogUtils.d(date);\n    });\n    animator.start();\n\n这样就可以得到在 5s 之内输出的从时间戳为0，到当前时刻的所有的日期变化。\n\n#### 3.2.5 TimeInterpolator\n\n就像 View 动画一样，我们可以为属性动画指定一个插值器。插值器的作用是用来设置指定时间段内数值的变化的速率。在属性动画中，插值器是 `TimeInterpolator`，同样也有几个默认的实现：\n\n1. `AccelerateDecelerateInterolator`：先加速后减速。\n2. `AccelerateInterpolator`：加速。\n3. `DecelerateInterpolator`：减速。\n4. `AnticipateInterpolator`：先向相反方向改变一段再加速播放。\n5. `AnticipateOvershootInterpolator`：先向相反方向改变，再加速播放，会超出目标值然后缓慢移动至目标值，类似于弹簧回弹。\n6. `BounceInterpolator`：快到目标值时值会跳跃。\n7. `CycleIinterpolator`：动画循环一定次数，值的改变为一正弦函数：Math.sin(2 * mCycles * Math.PI * input)。\n8. `LinearInterpolator`：线性均匀改变。\n9. `OvershottInterpolator`：最后超出目标值然后缓慢改变到目标值。\n\n#### 3.2.6 在 xml 中使用属性动画\n\n我们可以像下面这样定义一个属性动画，\n\n    <set android:ordering=[\"together\" | \"sequentially\"]>\n        <objectAnimator\n            android:propertyName=\"string\"\n            android:duration=\"int\"\n            android:valueFrom=\"float | int | color\"\n            android:valueTo=\"float | int | color\"\n            android:startOffset=\"int\"\n            android:repeatCount=\"int\"\n            android:repeatMode=[\"repeat\" | \"reverse\"]\n            android:valueType=[\"intType\" | \"floatType\"]/>\n        <animator\n            android:duration=\"int\"\n            android:valueFrom=\"float | int | color\"\n            android:valueTo=\"float | int | color\"\n            android:startOffset=\"int\"\n            android:repeatCount=\"int\"\n            android:repeatMode=[\"repeat\" | \"reverse\"]\n            android:valueType=[\"intType\" | \"floatType\"]/>\n        <set>\n            ...\n        </set>\n    </set>\n\n这里的`android:ordering`用于控制子动画启动方式是先后有序的还是同时进行，两个可选参数： `sequentially` 表示动画按照先后顺序；`together`(默认)表示动画同时启动。\n\n这里的 `<objectAnimator>` 标签的含义如下：\n\n![属性动画的XML属性详解](./resources/属性动画XML.png)\n\n这样在 XML 中定义了属性动画之后，我们可以在代码中通过工具类获取到动画实例并使用：\n\n    AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext, R.animtor.property_animator);\n    set.setTarget(myObject);\n    set.start();\n\n## 4、使用动画的注意事项\n\n1. **内存耗尽**：使用帧动画的时候防止因为图片过多导致 OOM。\n2. **View 动画并没有真正改变 View 的位置**：`View` 动画并没有真正改变 `View` 的属性，即 `View` 动画执行之后并未改变 `View` 的真实布局属性值。譬如我们在布局中有一个 `Button` 在屏幕上方，我们设置了平移动画移动到屏幕下方然后保持动画最后执行状态呆在屏幕下方，这时如果点击屏幕下方动画执行之后的 `Button` 是没有任何反应的，而点击原来屏幕上方没有 `Button` 的地方却响应的是点击 `Button` 的事件。\n3. **内存泄漏**：使用属性动画的时候，当使用无限循环动画，需要在 Activity 退出的时候停止动画，不然可能会因为无法释放资源而导致 Activity 内存泄漏。\n4. **动画兼容**：当 APP 需要兼容到 API 11 以下的时候就需要注意动画的兼容问题。\n5. **使用 dp 而不是 px**：因为 `px` 在不同设备上面的兼容问题，使用动画的时候尽量使用 `dp` 作为单位。\n6. **硬件加速**：使用硬件加速可以提升动画的流畅性。\n\n"
  },
  {
    "path": "系统架构/窗口机制/Android的Window管理机制.md",
    "content": "# Android 窗口管理机制详解\n\n在之前的文章 [《View 体系详解：View 的工作流程》](https://juejin.im/post/5bc5e3fbe51d450e7e51befa) 中，我们分析了 View 的工作流程。在这篇文章中，我们提到过在 Activity 初始化 View 的时候会将 DecorView 添加到 Window 中。但是我们并没有更多得分析 Window 的整个管理过程。在这篇文章中我们将详细分析一下 Window 的添加、更新和管理等过程。\n\n## \n\n添加一个 View\n\n        WindowManager wm = getWindowManager();\n\n        Button button = new Button(getContext());\n        button.setText(\"My Window Button\");\n\n        WindowManager.LayoutParams params = new WindowManager.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0, Color.TRANSPARENT);\n        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL\n                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;\n        params.gravity = Gravity.CENTER;\n        params.type = WindowManager.LayoutParams.TYPE_APPLICATION;\n\n        wm.addView(button, params);\n\nWindowManager 实现了 ViewManager 接口\n\n这里的 type 和 flag 是有限制的，具体可以看官方文档\n\n1. Q :Window 和 Activity 之间的关系？\n\nActivity#handleResumeActivity() 方法中从 Activity 中获取 WindowManager\n\n            ViewManager wm = a.getWindowManager();\n            WindowManager.LayoutParams l = r.window.getAttributes();\n\nActivity 会从内部的 mWindow 中获取 WindowManager，Window 中的 WindowManager 初始化过程：\n\n    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,\n            boolean hardwareAccelerated) {\n        // ...\n        if (wm == null) {\n            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);\n        }\n        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);\n    }\n\n    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {\n        return new WindowManagerImpl(mContext, parentWindow);\n    }\n\n这里 WindowManagerImpl，WindowManagerImpl，会把所有任务都委托给 WindowManagerGlobal\n\n    public final class WindowManagerImpl implements WindowManager {\n        private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();\n\n        // ...\n    }\n\nWindowManagerGlobal，全局单例：\n\n    public static WindowManagerGlobal getInstance() {\n        synchronized (WindowManagerGlobal.class) {\n            if (sDefaultWindowManager == null) {\n                sDefaultWindowManager = new WindowManagerGlobal();\n            }\n            return sDefaultWindowManager;\n        }\n    }\n\n所以增删 Window 的操作可以直接看 WindowManagerGlobal 部分的代码。\n\nWindowManagerGlobal 内部的三个列表：\n\n    private final ArrayList<View> mViews = new ArrayList<View>();\n    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();\n    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();\n    private final ArraySet<View> mDyingViews = new ArraySet<View>();\n\n增删 View 的时候操作的就是这些列表？\n\n添加 View 的时候：\n\n    public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {\n            // ... 无关代码\n            // new 出一个 ViewRootImpl 来对 View 进行绘制\n            root = new ViewRootImpl(view.getContext(), display);\n            view.setLayoutParams(wparams);\n\n            mViews.add(view);\n            mRoots.add(root);\n            mParams.add(wparams);\n            try {\n                // 会在 ViewRootImpl 的 setView() 方法中对 View 进行测量和绘制\n                root.setView(view, wparams, panelParentView);\n            } catch (RuntimeException e) {\n                if (index >= 0) {\n                    removeViewLocked(index, true);\n                }\n                throw e;\n            }\n    }\n\n在 setView() 方法中会：\n\n会调用 requestLayout() 方法进行布局，然后，并最终调用 performTraversals() 完成对整个 View 树进行遍历，并依次调用各控件的 onMeasre onLayout onDraw 来将视图绘制出来\n\n最后\n\n                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,\n                            getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,\n                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,\n                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);\n\n通过 mWindowSession 来将视图呈现给用户（mWindowSession 在 ViewRootImpl 定义）\n\n这里的 mWindowSession 是怎么来的呢？ [IWindowSession](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/IWindowSession.aidl) 从 [IWindowManager](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/IWindowManager.aidl) 的 `openSession()` 方法中获取到，两者都是 AIDL 文件。IWindowManager 由 [ServiceManager](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/ServiceManager.java) 中获取的 `window` 服务的 `Binder` 对象得到，就是得到了远程的窗口服务。然后，再使用该窗口服务来打开对话。该窗口服务的定义是 [WindowManagerService](https://android.googlesource.com/platform/frameworks/base/+/b267554/services/java/com/android/server/wm/WindowManagerService.java)。其内部会实例化一个 `Session ` 并将其返回，这就是 IWindowSession。\n\n从上面可以看出，添加 Window 的过程中需要使用 IPC，请求远程的窗口服务来最终完成添加窗口的操作。\n\n\n移除 View：\n\n    public void removeView(View view, boolean immediate) {\n        // ... 无关代码\n        synchronized (mLock) {\n            // 返回要移除的 View 的索引\n            int index = findViewLocked(view, true);\n            View curView = mRoots.get(index).getView();\n            // 移除 View\n            removeViewLocked(index, immediate);\n            if (curView == view) {\n                return;\n            }\n\n            throw new IllegalStateException(\"Calling with view \" + view + \" but the ViewAncestor is attached to \" + curView);\n        }\n    }\n\n    private void removeViewLocked(int index, boolean immediate) {\n        // 获取对应的 ViewRootImple\n        ViewRootImpl root = mRoots.get(index);\n        View view = root.getView();\n\n        if (view != null) {\n            InputMethodManager imm = InputMethodManager.getInstance();\n            if (imm != null) {\n                imm.windowDismissed(mViews.get(index).getWindowToken());\n            }\n        }\n        // 调用 ViewRootImpl 的 die() 方法\n        boolean deferred = root.die(immediate);\n        if (view != null) {\n            view.assignParent(null);\n            if (deferred) {\n                // 将其添加到正在销毁的视图的队列中\n                mDyingViews.add(view);\n            }\n        }\n    }\n\nViewRootImpl 的 die() 方法会根据布尔类型变量 `immediate` 决定是立即执行 doDie() 还是把消息发送给 Handler 让它来调用 doDie()\n\n本质上都会调用 doDie() 来移除视图\n\ndoDie() 方法主要：\n\n调用 \n\n调用 WindowManagerGlobal.getInstance().doRemoveView(this); 从上述列表中将视图相关的内容移除\n\n\nWindow 更新\n\n    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {\n        if (view == null) {\n            throw new IllegalArgumentException(\"view must not be null\");\n        }\n        if (!(params instanceof WindowManager.LayoutParams)) {\n            throw new IllegalArgumentException(\"Params must be WindowManager.LayoutParams\");\n        }\n\n        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;\n\n        view.setLayoutParams(wparams);\n\n        synchronized (mLock) {\n            int index = findViewLocked(view, true);\n            ViewRootImpl root = mRoots.get(index);\n            mParams.remove(index);\n            mParams.add(index, wparams);\n            root.setLayoutParams(wparams, false);\n        }\n    }\n\n最终会调用 ViewRootImpl 的 scheduleTraversals(); 方法从新对 View 树进行测量布局和绘制，从而整个视图就以新的面貌呈现给用户了。\n\n\n\n"
  },
  {
    "path": "网络访问/OKHttp源码阅读.md",
    "content": "# Andriod 网络框架 OkHttp 源码解析\n\n## 1、OkHttp 的基本使用\n\nOkHttp 是 Square 的一款应用于 Android 和 Java 的 Http 和 Http/2 客户端。使用的时候只需要在 Gradle 里面加入下面一行依赖即可引入：\n\n```groovy\nimplementation 'com.squareup.okhttp3:okhttp:3.11.0'\n```\n\n我们知道，Http 请求有多种类型，常用的分为 Get 和 Post，而 POST 又分为 Form 和 Multiple 等。下面我们以 Form 类型的请求为例来看下 OkHttp 的 API 设计逻辑：\n\n```java\nOkHttpClient internalHttpClient = new OkHttpClient();\nFormBody.Builder formBodyBuilder = new FormBody.Builder();\nRequestBody body = formBodyBuilder.build();\nRequest.Builder builder = new Request.Builder().url(\"host:port/url\").post(body);\nRequest request = builder.build();\nResponse response = internalHttpClient.newCall(request).execute();\nString retJson = response.body().string();\n```\n\n这里我们先用了 `FormBody` 的构建者模式创建 Form 类型请求的请求体，然后使用 `Request` 的构建者创建完整的 Form 请求。之后，我们用创建好的 OkHttp 客户端 `internalHttpClient` 来获取一个请求，并从请求的请求体中获取 Json 数据。\n\n根据 OkHttp 的 API，如果我们希望发送一个 Multipart 类型的请求的时候就需要使用 `MultipartBody` 的构建者创建 Multipart 请求的请求体。然后同样使用 `Request` 的构建者创建完整的 Multipart 请求，剩下的逻辑相同。\n\n除了使用上面的直接实例化一个 OkHttp 客户端的方式，我们也可以使用 `OkHttpClient` 的构建者 `OkHttpClient.Builder` 来创建 OkHttp 客户端。\n\n所以，我们可以总结：\n\n1. OkHttp 为不同的请求类型都提供了一个构建者方法用来创建请求体 `RequestBody`；\n2. 因为请求体只是整个请求的一部分，所以，又要用 `Request.Builder` 构建一个请求对象 `Request`；\n3. 这样我们得到了一个完整的 Http 请求，然后使用 `OkHttpClient` 对象进行网络访问得到响应对象 `Response`。\n\nOkHttp 本身的设计比较友好，思路非常清晰，按照上面的思路搞懂了人家的 API 设计逻辑，自己再基于 OkHttp 封装一个库自然问题不大。\n\n## 2、OkHttp 源码分析\n\n上面我们提到的一些是基础的 API 类，是提供给用户使用的。这些类的设计只是基于构建者模式，非常容易理解。这里我们关注点也不在这些 API 类上面，而是 OkHttp 内部的请求执行相关的类。下面我们就开始对 OkHttp 的请求过程进行源码分析（源码版本：3.10.0）。\n\n### 2.1 一个请求的大致流程\n\n参考之前的示例程序，抛弃构建请求的过程不讲，单从请求的发送过程来看，我们的线索应该从 `OkHttpClient.newCall(Request)` 开始。下面是这个方法的定义，它会创建一个 `RealCall` 对象，并把 `OkHttpClient` 对象和 `Request` 对象作为参数传入进去：\n\n```java\n@Override public Call newCall(Request request) {\n    return RealCall.newRealCall(this, request, false /* for web socket */);\n}\n```\n然后，RealCall 调用内部的静态方法 `newRealCall` 在其中创建一个 `RealCall` 实例并将其返回：\n\n```java\nstatic RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {\n    RealCall call = new RealCall(client, originalRequest, forWebSocket);\n    call.eventListener = client.eventListenerFactory().create(call);\n    return call;\n}\n```\n\n然后，当返回了 `RealCall` 之后，我们又会调用它的 `execute()` 方法来获取响应结果，下面是这个方法的定义：\n\n```java\n    @Override public Response execute() throws IOException {\n        synchronized (this) {\n            if (executed) throw new IllegalStateException(\"Already Executed\");\n            executed = true;\n        }\n        captureCallStackTrace();\n        eventListener.callStart(this);\n        try {\n            // 加入到一个双端队列中\n            client.dispatcher().executed(this);\n            // 从这里拿的响应Response\n            Response result = getResponseWithInterceptorChain();\n            if (result == null) throw new IOException(\"Canceled\");\n            return result;\n        } catch (IOException e) {\n            eventListener.callFailed(this, e);\n            throw e;\n        } finally {\n            client.dispatcher().finished(this);\n        }\n    }\n```\n\n这里我们会用 `client` 对象（实际也就是上面创建 `RealCall` 的时候传入的 `OkHttpClient`）的 `dispatcher()` 方法来获取一个 `Dispatcher` 对象，并调用它的 `executed()` 方法来将当前的 `RealCall` 加入到一个双端队列中，下面是 `executed(RealCall)` 方法的定义，这里的 `runningSyncCalls` 的类型是 `Deque<RealCall>`：\n\n```java\n    synchronized void executed(RealCall call) {\n        runningSyncCalls.add(call);\n    }\n```\n\n让我们回到上面的 `execute()`  方法，在把 `RealCall` 加入到双端队列之后，我们又调用了 `getResponseWithInterceptorChain()` 方法，下面就是该方法的定义。\n\n```java\n    Response getResponseWithInterceptorChain() throws IOException {\n        // 添加一系列拦截器，注意添加的顺序\n        List<Interceptor> interceptors = new ArrayList<>();\n        interceptors.addAll(client.interceptors());\n        interceptors.add(retryAndFollowUpInterceptor);\n        // 桥拦截器\n        interceptors.add(new BridgeInterceptor(client.cookieJar()));\n        // 缓存拦截器：从缓存中拿数据\n        interceptors.add(new CacheInterceptor(client.internalCache()));\n        // 网络连接拦截器：建立网络连接\n        interceptors.add(new ConnectInterceptor(client));\n        if (!forWebSocket) {\n            interceptors.addAll(client.networkInterceptors());\n        }\n        // 服务器请求拦截器：向服务器发起请求获取数据\n        interceptors.add(new CallServerInterceptor(forWebSocket));\n        // 构建一条责任链\n        Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,\n            originalRequest, this, eventListener, client.connectTimeoutMillis(),\n            client.readTimeoutMillis(), client.writeTimeoutMillis());\n        // 处理责任链\n        return chain.proceed(originalRequest);\n    }\n```\n\n这里，我们创建了一个列表对象之后把 `client` 中的拦截器、重连拦截器、桥拦截器、缓存拦截器、网络连接拦截器和服务器请求拦截器等**依次**加入到列表中。然后，我们用这个列表创建了一个拦截器链。这里使用了`责任链设计模式`，每当一个拦截器执行完毕之后会调用下一个拦截器或者不调用并返回结果。显然，我们最终拿到的响应就是这个链条执行之后返回的结果。当我们自定义一个拦截器的时候，也会被加入到这个拦截器链条里。\n\n这里我们遇到了很多的新类，比如 `RealCall`、`Dispatcher` 以及责任链等。下文中，我们会对这些类之间的关系以及责任链中的环节做一个分析，而这里我们先对整个请求的流程做一个大致的梳理。下面是这个过程大致的时序图：\n\n![OkHttp请求时序图](https://user-gold-cdn.xitu.io/2018/10/19/1668c58f05078818)\n\n### 2.2 分发器 Dispatcher\n\n上面我们提到了 `Dispatcher` 这个类，它的作用是对请求进行分发。以最开始的示例代码为例，在使用 OkHttp 的时候，我们会创建一个 `RealCall` 并将其加入到双端队列中。但是请注意这里的双端队列的名称是 `runningSyncCalls`，也就是说这种请求是同步请求，会在当前的线程中立即被执行。所以，下面的 `getResponseWithInterceptorChain()` 就是这个同步的执行过程。而当我们执行完毕的时候，又会调用 `Dispatcher` 的 `finished(RealCall)` 方法把该请求从队列中移除。所以，这种同步的请求无法体现分发器的“分发”功能。\n\n除了同步的请求，还有异步类型的请求：当我们拿到了 `RealCall` 的时候，调用它的 `enqueue(Callback responseCallback)` 方法并设置一个回调即可。该方法会执行下面这行代码：\n\n```java\nclient.dispatcher().enqueue(new AsyncCall(responseCallback));\n```\n\n即使用上面的回调创建一个  `AsyncCall` 并调用 `enqueue(AsyncCall)`。这里的 `AsyncCall` 间接继承自 `Runnable`，是一个可执行的对象，并且会在 `Runnable` 的 `run()` 方法里面调用 `AsyncCall` 的 `execute()` 方法。`AsyncCall` 的 `execute()` 方法与 `RealCall` 的 `execute()` 方法类似，都使用责任链来完成一个网络请求。只是后者可以放在一个异步的线程中进行执行。\n\n当我们调用了 `Dispatcher` 的 `enqueue(AsyncCall)` 方法的时候也会将 `AsyncCall` 加入到一个队列中，并会在请求执行完毕的时候从该队列中移除，只是这里的队列是 `runningAsyncCalls` 或者 `readyAsyncCalls`。它们都是一个双端队列，并用来存储异步类型的请求。它们的区别是，`runningAsyncCalls` 是正在执行的队列，当正在执行的队列达到了限制的时候，就会将其放置到就绪队列 `readyAsyncCalls` 中：\n\n```java\n    synchronized void enqueue(AsyncCall call) {\n        if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {\n            runningAsyncCalls.add(call);\n            executorService().execute(call);\n        } else {\n            readyAsyncCalls.add(call);\n        }\n    }\n```\n\n当把该请求加入到了正在执行的队列之后，我们会立即使用一个线程池来执行该 `AsyncCall`。这样这个请求的责任链就会在一个线程池当中被异步地执行了。这里的线程池由 `executorService()` 方法返回：\n\n```java\n    public synchronized ExecutorService executorService() {\n        if (executorService == null) {\n            executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,\n            new SynchronousQueue<Runnable>(), Util.threadFactory(\"OkHttp Dispatcher\", false));\n        }\n        return executorService;\n    }\n```\n\n显然，当线程池不存在的时候会去创建一个线程池。除了上面的这种方式，我们还可以在构建 `OkHttpClient` 的时候，自定义一个 `Dispacher`，并在其构造方法中为其指定一个线程池。下面我们类比 OkHttp 的同步请求绘制了一个异步请求的时序图。你可以通过将两个图对比来了解两种实现方式的不同：\n\n![OkHttp异步请求](https://user-gold-cdn.xitu.io/2018/10/19/1668c5c04f04eab2?w=1099&h=506&f=png&s=26593)\n\n以上就是分发器 `Dispacher` 的逻辑，看上去并没有那么复杂。并且从上面的分析中，我们可以看出实际请求的执行过程并不是在这里完成的，这里只能决定在哪个线程当中执行请求并把请求用双端队列缓存下来，而实际的请求执行过程是在责任链中完成的。下面我们就来分析一下 OkHttp 里的责任链的执行过程。\n\n### 2.3 责任链的执行过程\n\n在典型的责任链设计模式里，很多对象由每一个对象对其下级的引用而连接起来形成一条链。请求在这个链上传递，直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求，这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。责任链在现实生活中的一种场景就是面试，当某轮面试官觉得你没有资格进入下一轮的时候可以否定你，不然会让下一轮的面试官继续面试。\n\n在 OkHttp 里面，责任链的执行模式与之稍有不同。这里我们主要来分析一下在 OkHttp 里面，责任链是如何执行的，至于每个链条里面的具体逻辑，我们会在随后一一说明。\n\n回到 2.1 的代码，有两个地方需要我们注意：\n\n1. 是当创建一个责任链 `RealInterceptorChain` 的时候，我们传入的第 5 个参数是 0。该参数名为 `index`，会被赋值给 `RealInterceptorChain` 实例内部的同名全局变量。\n2. 当启用责任链的时候，会调用它的 `proceed(Request)` 方法。\n\n下面是 `proceed(Request)` 方法的定义：\n\n```java\n    @Override public Response proceed(Request request) throws IOException {\n        return proceed(request, streamAllocation, httpCodec, connection);\n    }\n```\n\n这里又调用了内部的重载的 `proceed()` 方法。下面我们对该方法进行了简化：\n\n```java\n    public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,\n        RealConnection connection) throws IOException {\n        if (index >= interceptors.size()) throw new AssertionError();\n        // ...\n        // 调用责任链的下一个拦截器\n        RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,\n            connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,\n            writeTimeout);\n        Interceptor interceptor = interceptors.get(index);\n        Response response = interceptor.intercept(next);\n        // ...\n        return response;\n    }\n```\n\n注意到这里使用责任链进行处理的时候，会新建下一个责任链并把 `index+1` 作为下一个责任链的 `index`。然后，我们使用 `index` 从拦截器列表中取出一个拦截器，调用它的 `intercept()` 方法，并把下一个执行链作为参数传递进去。\n\n这样，当下一个拦截器希望自己的下一级继续处理这个请求的时候，可以调用传入的责任链的 `proceed()` 方法；如果自己处理完毕之后，下一级不需要继续处理，那么就直接返回一个 `Response` 实例即可。因为，每次都是在当前的 `index` 基础上面加 1，所以能在调用 `proceed()` 的时候准确地从拦截器列表中取出下一个拦截器进行处理。\n\n我们还要注意的地方是之前提到过重试拦截器，这种拦截器会在内部启动一个 `while` 循环，并在循环体中调用执行链的 `proceed()` 方法来实现请求的不断重试。这是因为在它那里的拦截器链的 `index` 是固定的，所以能够每次调用 `proceed()` 的时候，都能够从自己的下一级执行一遍链条。下面就是这个责任链的执行过程：\n\n![责任链执行过程](https://user-gold-cdn.xitu.io/2018/10/19/1668c5c6363ea20f?w=853&h=875&f=png&s=84443)\n\n清楚了 OkHttp 的拦截器链的执行过程之后，我们来看一下各个拦截器做了什么逻辑。\n\n### 2.3 重试和重定向：RetryAndFollowUpInterceptor\n\n`RetryAndFollowUpInterceptor` 主要用来当请求失败的时候进行重试，以及在需要的情况下进行重定向。我们上面说，责任链会在进行处理的时候调用第一个拦截器的 `intercept()` 方法。如果我们在创建 OkHttp 客户端的时候没有加入自定义拦截器，那么\n`RetryAndFollowUpInterceptor` 就是我们的责任链中最先被调用的拦截器。\n\n```java\n    @Override public Response intercept(Chain chain) throws IOException {\n        // ...\n        // 注意这里我们初始化了一个 StreamAllocation 并赋值给全局变量，它的作用我们后面会提到\n        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),\n                createAddress(request.url()), call, eventListener, callStackTrace);\n        this.streamAllocation = streamAllocation;\n        // 用来记录重定向的次数\n        int followUpCount = 0;\n        Response priorResponse = null;\n        while (true) {\n            if (canceled) {\n                streamAllocation.release();\n                throw new IOException(\"Canceled\");\n            }\n\n            Response response;\n            boolean releaseConnection = true;\n            try {\n                // 这里从当前的责任链开始执行一遍责任链，是一种重试的逻辑\n                response = realChain.proceed(request, streamAllocation, null, null);\n                releaseConnection = false;\n            } catch (RouteException e) {\n                // 调用 recover 方法从失败中进行恢复，如果可以恢复就返回true，否则返回false\n                if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {\n                    throw e.getLastConnectException();\n                }\n                releaseConnection = false;\n                continue;\n            } catch (IOException e) {\n                // 重试与服务器进行连接\n                boolean requestSendStarted = !(e instanceof ConnectionShutdownException);\n                if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;\n                releaseConnection = false;\n                continue;\n            } finally {\n                // 如果 releaseConnection 为 true 则表明中间出现了异常，需要释放资源\n                if (releaseConnection) {\n                    streamAllocation.streamFailed(null);\n                    streamAllocation.release();\n                }\n            }\n\n            // 使用之前的响应 priorResponse 构建一个响应，这种响应的响应体 body 为空\n            if (priorResponse != null) {\n                response = response.newBuilder()\n                        .priorResponse(priorResponse.newBuilder().body(null).build())\n                        .build();\n            }\n\n            // 根据得到的响应进行处理，可能会增加一些认证信息、重定向或者处理超时请求\n            // 如果该请求无法继续被处理或者出现的错误不需要继续处理，将会返回 null\n            Request followUp = followUpRequest(response, streamAllocation.route());\n\n            // 无法重定向，直接返回之前的响应\n            if (followUp == null) {\n                if (!forWebSocket) {\n                    streamAllocation.release();\n                }\n                return response;\n            }\n\n            // 关闭资源\n            closeQuietly(response.body());\n\n            // 达到了重定向的最大次数，就抛出一个异常\n            if (++followUpCount > MAX_FOLLOW_UPS) {\n                streamAllocation.release();\n                throw new ProtocolException(\"Too many follow-up requests: \" + followUpCount);\n            }\n\n            if (followUp.body() instanceof UnrepeatableRequestBody) {\n                streamAllocation.release();\n                throw new HttpRetryException(\"Cannot retry streamed HTTP body\", response.code());\n            }\n\n            // 这里判断新的请求是否能够复用之前的连接，如果无法复用，则创建一个新的连接\n            if (!sameConnection(response, followUp.url())) {\n                streamAllocation.release();\n                streamAllocation = new StreamAllocation(client.connectionPool(),\n                        createAddress(followUp.url()), call, eventListener, callStackTrace);\n                this.streamAllocation = streamAllocation;\n            } else if (streamAllocation.codec() != null) {\n                throw new IllegalStateException(\"Closing the body of \" + response\n                        + \" didn't close its backing stream. Bad interceptor?\");\n            }\n\n            request = followUp;\n            priorResponse = response;\n        }\n    }\n```\n\n以上的代码主要用来根据错误的信息做一些处理，会根据服务器返回的信息判断这个请求是否可以重定向，或者是否有必要进行重试。如果值得去重试就会新建或者复用之前的连接在下一次循环中进行请求重试，否则就将得到的请求包装之后返回给用户。这里，我们提到了 `StreamAllocation` 对象，它相当于一个管理类，维护了服务器连接、并发流和请求之间的关系，该类还会初始化一个 `Socket` 连接对象，获取输入/输出流对象。同时，还要注意这里我们通过 `client.connectionPool()` 传入了一个连接池对象 `ConnectionPool`。这里我们只是初始化了这些类，但实际在当前的方法中并没有真正用到这些类，而是把它们传递到下面的拦截器里来从服务器中获取请求的响应。稍后，我们会说明这些类的用途，以及之间的关系。\n\n### 2.4 BridgeInterceptor\n\n桥拦截器 `BridgeInterceptor` 用于从用户的请求中构建网络请求，然后使用该请求访问网络，最后从网络响应当中构建用户响应。相对来说这个拦截器的逻辑比较简单，只是用来对请求进行包装，并将服务器响应转换成用户友好的响应：\n\n```java\n    public final class BridgeInterceptor implements Interceptor {\n        @Override public Response intercept(Chain chain) throws IOException {\n            Request userRequest = chain.request();\n            // 从用户请求中获取网络请求构建者\n            Request.Builder requestBuilder = userRequest.newBuilder();\n            // ...\n            // 执行网络请求\n            Response networkResponse = chain.proceed(requestBuilder.build());\n            // ...\n            // 从网络响应中获取用户响应构建者\n            Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);\n            // ...\n            // 返回用户响应\n            return responseBuilder.build();\n        }\n    }\n```\n\n### 2.5 使用缓存：CacheInterceptor\n\n缓存拦截器会根据请求的信息和缓存的响应的信息来判断是否存在缓存可用，如果有可以使用的缓存，那么就返回该缓存该用户，否则就继续责任链来从服务器中获取响应。当获取到响应的时候，又会把响应缓存到磁盘上面。以下是这部分的逻辑：\n\n```java\n    public final class CacheInterceptor implements Interceptor {\n        @Override public Response intercept(Chain chain) throws IOException {\n            Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;\n            long now = System.currentTimeMillis();\n            // 根据请求和缓存的响应中的信息来判断是否存在缓存可用\n            CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();\n            Request networkRequest = strategy.networkRequest; // 如果该请求没有使用网络就为空\n            Response cacheResponse = strategy.cacheResponse; // 如果该请求没有使用缓存就为空\n            if (cache != null) {\n                cache.trackResponse(strategy);\n            }\n            if (cacheCandidate != null && cacheResponse == null) {\n                closeQuietly(cacheCandidate.body());\n            }\n            // 请求不使用网络并且不使用缓存，相当于在这里就拦截了，没必要交给下一级（网络请求拦截器）来执行\n            if (networkRequest == null && cacheResponse == null) {\n                return new Response.Builder()\n                        .request(chain.request())\n                        .protocol(Protocol.HTTP_1_1)\n                        .code(504)\n                        .message(\"Unsatisfiable Request (only-if-cached)\")\n                        .body(Util.EMPTY_RESPONSE)\n                        .sentRequestAtMillis(-1L)\n                        .receivedResponseAtMillis(System.currentTimeMillis())\n                        .build();\n            }\n            // 该请求使用缓存，但是不使用网络：从缓存中拿结果，没必要交给下一级（网络请求拦截器）执行\n            if (networkRequest == null) {\n                return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();\n            }\n            Response networkResponse = null;\n            try {\n                // 这里调用了执行链的处理方法，实际就是交给自己的下一级来执行了\n                networkResponse = chain.proceed(networkRequest);\n            } finally {\n                if (networkResponse == null && cacheCandidate != null) {\n                    closeQuietly(cacheCandidate.body());\n                }\n            }\n            // 这里当拿到了网络请求之后调用，下一级执行完毕会交给它继续执行，如果使用了缓存就把请求结果更新到缓存里\n            if (cacheResponse != null) {\n                // 服务器返回的结果是304，返回缓存中的结果\n                if (networkResponse.code() == HTTP_NOT_MODIFIED) {\n                    Response response = cacheResponse.newBuilder()\n                            .headers(combine(cacheResponse.headers(), networkResponse.headers()))\n                            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())\n                            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())\n                            .cacheResponse(stripBody(cacheResponse))\n                            .networkResponse(stripBody(networkResponse))\n                            .build();\n                    networkResponse.body().close();\n                    cache.trackConditionalCacheHit();\n                    // 更新缓存\n                    cache.update(cacheResponse, response);\n                    return response;\n                } else {\n                    closeQuietly(cacheResponse.body());\n                }\n            }\n            Response response = networkResponse.newBuilder()\n                    .cacheResponse(stripBody(cacheResponse))\n                    .networkResponse(stripBody(networkResponse))\n                    .build();\n            // 把请求的结果放进缓存里\n            if (cache != null) {\n                if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {\n                    CacheRequest cacheRequest = cache.put(response);\n                    return cacheWritingResponse(cacheRequest, response);\n                }\n                if (HttpMethod.invalidatesCache(networkRequest.method())) {\n                    try {\n                        cache.remove(networkRequest);\n                    } catch (IOException ignored) {\n                        // The cache cannot be written.\n                    }\n                }\n            }\n            return response;\n        }\n    }\n```\n\n对缓存，这里我们使用的是全局变量 `cache`，它是 `InternalCache` 类型的变量。`InternalCache` 是一个接口，在 OkHttp 中只有一个实现类 `Cache`。在 `Cache` 内部，使用了 `DiskLruCache` 来将缓存的数据存到磁盘上。`DiskLruCache` 以及 `LruCache` 是 Android 上常用的两种缓存策略。前者是基于磁盘来进行缓存的，后者是基于内存来进行缓存的，它们的核心思想都是 Least Recently Used，即最近最少使用算法。我们会在以后的文章中详细介绍这两种缓存框架，也请继续关注我们的文章。\n\n另外，上面我们根据请求和缓存的响应中的信息来判断是否存在缓存可用的时候用到了 `CacheStrategy` 的两个字段，得到这两个字段的时候使用了非常多的判断，其中涉及 Http 缓存相关的知识，感兴趣的话可以自己参考源代码。\n\n### 2.6 连接复用：ConnectInterceptor\n\n连接拦截器 `ConnectInterceptor` 用来打开到指定服务器的网络连接，并交给下一个拦截器处理。这里我们只打开了一个网络连接，但是并没有发送请求到服务器。从服务器获取数据的逻辑交给下一级的拦截器来执行。虽然，这里并没有真正地从网络中获取数据，而仅仅是打开一个连接，但这里有不少的内容值得我们去关注。因为在获取连接对象的时候，使用了连接池 `ConnectionPool` 来复用连接。\n\n```java\n    public final class ConnectInterceptor implements Interceptor {\n\n        @Override public Response intercept(Chain chain) throws IOException {\n            RealInterceptorChain realChain = (RealInterceptorChain) chain;\n            Request request = realChain.request();\n            StreamAllocation streamAllocation = realChain.streamAllocation();\n\n            boolean doExtensiveHealthChecks = !request.method().equals(\"GET\");\n            HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);\n            RealConnection connection = streamAllocation.connection();\n\n            return realChain.proceed(request, streamAllocation, httpCodec, connection);\n        }\n    }\n```\n\n这里的 `HttpCodec` 用来编码请求并解码响应，`RealConnection` 用来向服务器发起连接。它们会在下一个拦截器中被用来从服务器中获取响应信息。下一个拦截器的逻辑并不复杂，这里万事具备之后，只要它来从服务器中读取数据即可。可以说，OkHttp 中的核心部分大概就在这里，所以，我们就先好好分析一下，这里在创建连接的时候如何借助连接池来实现连接复用的。\n\n根据上面的代码，当我们调用 `streamAllocation` 的 `newStream()` 方法的时候，最终会经过一系列的判断到达 `StreamAllocation` 中的 `findConnection()` 方法。\n\n```java\n    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,\n                                          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {\n        // ...\n        synchronized (connectionPool) {\n            // ...\n            // 尝试使用已分配的连接，已经分配的连接可能已经被限制创建新的流\n            releasedConnection = this.connection;\n            // 释放当前连接的资源，如果该连接已经被限制创建新的流，就返回一个Socket以关闭连接\n            toClose = releaseIfNoNewStreams();\n            if (this.connection != null) {\n                // 已分配连接，并且该连接可用\n                result = this.connection;\n                releasedConnection = null;\n            }\n            if (!reportedAcquired) {\n                // 如果该连接从未被标记为获得，不要标记为发布状态，reportedAcquired 通过 acquire() 方法修改\n                releasedConnection = null;\n            }\n\n            if (result == null) {\n                // 尝试供连接池中获取一个连接\n                Internal.instance.get(connectionPool, address, this, null);\n                if (connection != null) {\n                    foundPooledConnection = true;\n                    result = connection;\n                } else {\n                    selectedRoute = route;\n                }\n            }\n        }\n        // 关闭连接\n        closeQuietly(toClose);\n\n        if (releasedConnection != null) {\n            eventListener.connectionReleased(call, releasedConnection);\n        }\n        if (foundPooledConnection) {\n            eventListener.connectionAcquired(call, result);\n        }\n        if (result != null) {\n            // 如果已经从连接池中获取到了一个连接，就将其返回\n            return result;\n        }\n\n        boolean newRouteSelection = false;\n        if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {\n            newRouteSelection = true;\n            routeSelection = routeSelector.next();\n        }\n\n        synchronized (connectionPool) {\n            if (canceled) throw new IOException(\"Canceled\");\n\n            if (newRouteSelection) {\n                // 根据一系列的 IP 地址从连接池中获取一个链接\n                List<Route> routes = routeSelection.getAll();\n                for (int i = 0, size = routes.size(); i < size; i++) {\n                    Route route = routes.get(i);\n                    // 从连接池中获取一个连接\n                    Internal.instance.get(connectionPool, address, this, route);\n                    if (connection != null) {\n                        foundPooledConnection = true;\n                        result = connection;\n                        this.route = route;\n                        break;\n                    }\n                }\n            }\n\n            if (!foundPooledConnection) {\n                if (selectedRoute == null) {\n                    selectedRoute = routeSelection.next();\n                }\n\n                // 创建一个新的连接，并将其分配，这样我们就可以在握手之前进行终端\n                route = selectedRoute;\n                refusedStreamCount = 0;\n                result = new RealConnection(connectionPool, selectedRoute);\n                acquire(result, false);\n            }\n        }\n\n        // 如果我们在第二次的时候发现了一个池连接，那么我们就将其返回\n        if (foundPooledConnection) {\n            eventListener.connectionAcquired(call, result);\n            return result;\n        }\n\n        // 进行 TCP 和 TLS 握手\n        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,\n                connectionRetryEnabled, call, eventListener);\n        routeDatabase().connected(result.route());\n\n        Socket socket = null;\n        synchronized (connectionPool) {\n            reportedAcquired = true;\n\n            // 将该连接放进连接池中\n            Internal.instance.put(connectionPool, result);\n\n            // 如果同时创建了另一个到同一地址的多路复用连接，释放这个连接并获取那个连接\n            if (result.isMultiplexed()) {\n                socket = Internal.instance.deduplicate(connectionPool, address, this);\n                result = connection;\n            }\n        }\n        closeQuietly(socket);\n\n        eventListener.connectionAcquired(call, result);\n        return result;\n    }\n```\n\n该方法会被放置在一个循环当中被不停地调用以得到一个可用的连接。它优先使用当前已经存在的连接，不然就使用连接池中存在的连接，再不行的话，就创建一个新的连接。所以，上面的代码大致分成三个部分：\n\n1. 判断当前的连接是否可以使用：流是否已经被关闭，并且已经被限制创建新的流；\n2. 如果当前的连接无法使用，就从连接池中获取一个连接；\n3. 连接池中也没有发现可用的连接，创建一个新的连接，并进行握手，然后将其放到连接池中。\n\n在从连接池中获取一个连接的时候，使用了 `Internal` 的 `get()` 方法。`Internal` 有一个静态的实例，会在 OkHttpClient 的静态代码快中被初始化。我们会在 `Internal` 的 `get()` 中调用连接池的 `get()` 方法来得到一个连接。\n\n从上面的代码中我们也可以看出，实际上，我们使用连接复用的一个好处就是省去了进行 TCP 和 TLS 握手的一个过程。因为建立连接本身也是需要消耗一些时间的，连接被复用之后可以提升我们网络访问的效率。那么这些连接被放置在连接池之后是如何进行管理的呢？我们会在下文中分析 OkHttp 的 `ConnectionPool` 中是如何管理这些连接的。\n\n### 2.7 CallServerInterceptor\n\n服务器请求拦截器 `CallServerInterceptor` 用来向服务器发起请求并获取数据。这是整个责任链的最后一个拦截器，这里没有再继续调用执行链的处理方法，而是把拿到的响应处理之后直接返回给了上一级的拦截器：\n\n```java\n    public final class CallServerInterceptor implements Interceptor {\n\n        @Override public Response intercept(Chain chain) throws IOException {\n            RealInterceptorChain realChain = (RealInterceptorChain) chain;\n            // 获取 ConnectInterceptor 中初始化的 HttpCodec\n            HttpCodec httpCodec = realChain.httpStream();\n            // 获取 RetryAndFollowUpInterceptor 中初始化的 StreamAllocation\n            StreamAllocation streamAllocation = realChain.streamAllocation();\n            // 获取 ConnectInterceptor 中初始化的 RealConnection\n            RealConnection connection = (RealConnection) realChain.connection();\n            Request request = realChain.request();\n\n            long sentRequestMillis = System.currentTimeMillis();\n\n            realChain.eventListener().requestHeadersStart(realChain.call());\n            // 在这里写入请求头 \n            httpCodec.writeRequestHeaders(request);\n            realChain.eventListener().requestHeadersEnd(realChain.call(), request);\n\n            Response.Builder responseBuilder = null;\n            if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {\n                if (\"100-continue\".equalsIgnoreCase(request.header(\"Expect\"))) {\n                    httpCodec.flushRequest();\n                    realChain.eventListener().responseHeadersStart(realChain.call());\n                    responseBuilder = httpCodec.readResponseHeaders(true);\n                }\n                 // 在这里写入请求体\n                if (responseBuilder == null) {\n                    realChain.eventListener().requestBodyStart(realChain.call());\n                    long contentLength = request.body().contentLength();\n                    CountingSink requestBodyOut =\n                            new CountingSink(httpCodec.createRequestBody(request, contentLength));\n                    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);\n                    // 写入请求体\n                    request.body().writeTo(bufferedRequestBody);\n                    bufferedRequestBody.close();\n                    realChain.eventListener()\n                            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);\n                } else if (!connection.isMultiplexed()) {\n                    streamAllocation.noNewStreams();\n                }\n            }\n            httpCodec.finishRequest();\n            if (responseBuilder == null) {\n                realChain.eventListener().responseHeadersStart(realChain.call());\n                // 读取响应头\n                responseBuilder = httpCodec.readResponseHeaders(false);\n            }\n            Response response = responseBuilder\n                    .request(request)\n                    .handshake(streamAllocation.connection().handshake())\n                    .sentRequestAtMillis(sentRequestMillis)\n                    .receivedResponseAtMillis(System.currentTimeMillis())\n                    .build();\n            // 读取响应体\n            int code = response.code();\n            if (code == 100) {\n                responseBuilder = httpCodec.readResponseHeaders(false);\n                response = responseBuilder\n                        .request(request)\n                        .handshake(streamAllocation.connection().handshake())\n                        .sentRequestAtMillis(sentRequestMillis)\n                        .receivedResponseAtMillis(System.currentTimeMillis())\n                        .build();\n                code = response.code();\n            }\n            realChain.eventListener().responseHeadersEnd(realChain.call(), response);\n            if (forWebSocket && code == 101) {\n                response = response.newBuilder()\n                        .body(Util.EMPTY_RESPONSE)\n                        .build();\n            } else {\n                response = response.newBuilder()\n                        .body(httpCodec.openResponseBody(response))\n                        .build();\n            }\n            // ...\n            return response;\n        }\n    }\n```\n\n### 2.8 连接管理：ConnectionPool\n\n与请求的缓存类似，OkHttp 的连接池也使用一个双端队列来缓存已经创建的连接：\n\n```java\nprivate final Deque<RealConnection> connections = new ArrayDeque<>();\n```\n\nOkHttp 的缓存管理分成两个步骤，一边当我们创建了一个新的连接的时候，我们要把它放进缓存里面；另一边，我们还要来对缓存进行清理。在 `ConnectionPool` 中，当我们向连接池中缓存一个连接的时候，只要调用双端队列的 `add()` 方法，将其加入到双端队列即可，而清理连接缓存的操作则交给线程池来定时执行。\n\n在 `ConnectionPool` 中存在一个静态的线程池：\n\n```java\n    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,\n        Integer.MAX_VALUE /* maximumPoolSize */, \n        60L /* keepAliveTime */,\n        TimeUnit.SECONDS,\n        new SynchronousQueue<Runnable>(), \n        Util.threadFactory(\"OkHttp ConnectionPool\", true));\n```\n\n每当我们向连接池中插入一个连接的时候就会调用下面的方法，将连接插入到双端队列的同时，会调用上面的线程池来执行清理缓存的任务：\n\n```java\n    void put(RealConnection connection) {\n        assert (Thread.holdsLock(this));\n        if (!cleanupRunning) {\n            cleanupRunning = true;\n            // 使用线程池执行清理任务\n            executor.execute(cleanupRunnable);\n        }\n        // 将新建的连接插入到双端队列中\n        connections.add(connection);\n    }\n```\n\n这里的清理任务是 `cleanupRunnable`，是一个 `Runnable` 类型的实例。它会在方法内部调用 `cleanup()` 方法来清理无效的连接：\n\n```java\n    private final Runnable cleanupRunnable = new Runnable() {\n        @Override public void run() {\n            while (true) {\n                long waitNanos = cleanup(System.nanoTime());\n                if (waitNanos == -1) return;\n                if (waitNanos > 0) {\n                    long waitMillis = waitNanos / 1000000L;\n                    waitNanos -= (waitMillis * 1000000L);\n                    synchronized (ConnectionPool.this) {\n                        try {\n                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);\n                        } catch (InterruptedException ignored) {\n                        }\n                    }\n                }\n            }\n        }\n    };\n```\n\n下面是 `cleanup()` 方法：\n\n```java\n    long cleanup(long now) {\n        int inUseConnectionCount = 0;\n        int idleConnectionCount = 0;\n        RealConnection longestIdleConnection = null;\n        long longestIdleDurationNs = Long.MIN_VALUE;\n\n        synchronized (this) {\n            // 遍历所有的连接\n            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {\n                RealConnection connection = i.next();\n                // 当前的连接正在使用中\n                if (pruneAndGetAllocationCount(connection, now) > 0) {\n                    inUseConnectionCount++;\n                    continue;\n                }\n                idleConnectionCount++;\n                // 如果找到了一个可以被清理的连接，会尝试去寻找闲置时间最久的连接来释放\n                long idleDurationNs = now - connection.idleAtNanos;\n                if (idleDurationNs > longestIdleDurationNs) {\n                    longestIdleDurationNs = idleDurationNs;\n                    longestIdleConnection = connection;\n                }\n            }\n\n            if (longestIdleDurationNs >= this.keepAliveDurationNs \n                    || idleConnectionCount > this.maxIdleConnections) {\n                // 该连接的时长超出了最大的活跃时长或者闲置的连接数量超出了最大允许的范围，直接移除\n                connections.remove(longestIdleConnection);\n            } else if (idleConnectionCount > 0) {\n                // 闲置的连接的数量大于0，停顿指定的时间（等会儿会将其清理掉，现在还不是时候）\n                return keepAliveDurationNs - longestIdleDurationNs;\n            } else if (inUseConnectionCount > 0) {\n                // 所有的连接都在使用中，5分钟后再清理\n                return keepAliveDurationNs;\n            } else {\n                // 没有连接\n                cleanupRunning = false;\n                return -1;\n            }\n        }\n\n        closeQuietly(longestIdleConnection.socket());\n        return 0;\n    }\n```\n\n在从缓存的连接中取出连接来判断是否应该将其释放的时候使用到了两个变量 `maxIdleConnections` 和 `keepAliveDurationNs`，分别表示最大允许的闲置的连接的数量和连接允许存活的最长的时间。默认空闲连接最大数目为5个，`keepalive` 时间最长为5分钟。\n\n上面的方法会对缓存中的连接进行遍历，以寻找一个闲置时间最长的连接，然后根据该连接的闲置时长和最大允许的连接数量等参数来决定是否应该清理该连接。同时注意上面的方法的返回值是一个时间，如果闲置时间最长的连接仍然需要一段时间才能被清理的时候，会返回这段时间的时间差，然后会在这段时间之后再次对连接池进行清理。\n\n## 总结：\n\n以上就是我们对 OkHttp 内部网络访问的源码的分析。当我们发起一个请求的时候会初始化一个 Call 的实例，然后根据同步和异步的不同，分别调用它的 `execute()` 和 `enqueue()` 方法。虽然，两个方法一个会在当前的线程中被立即执行，一个会在线程池当中执行，但是它们进行网络访问的逻辑都是一样的：通过拦截器组成的责任链，依次经过重试、桥接、缓存、连接和访问服务器等过程，来获取到一个响应并交给用户。其中，缓存和连接两部分内容是重点，因为前者涉及到了一些计算机网络方面的知识，后者则是 OkHttp 效率和框架的核心。\n\n"
  },
  {
    "path": "网络访问/Retrofit源码阅读.md",
    "content": "# 动态代理在 Android 中的应用：Retrofit 源码解析\n\n在之前的文章 [《Andriod 网络框架 OkHttp 源码解析》](https://shouheng88.github.io/2018/10/17/Andriod%20%E7%BD%91%E7%BB%9C%E6%A1%86%E6%9E%B6%20OkHttp%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/#more) 中我们分析了 OkHttp 的源代码。现在我们就来分析一下 OkHttp 的兄弟框架 Retrofit。关于 Retrofit 的注解的使用，可以参考其官方文档：[https://square.github.io/retrofit/](https://square.github.io/retrofit/)。\n\nRetrofit 也是 Square 发布的一个开源的库，它是一个类型安全的 Http 客户端，适用于 Android 和 Java。本质上，Retrofit 使用了 Java 的动态代理，内部使用 OkHttp 来进行网络访问，并且可以通过指定 “请求适配器” 和 “类型转换器” 来完成：请求的适配，方法参数到 OkHttp 请求的转换，以及响应到 Java 类型的转换。\n\n## 1、基本使用\n\nRetrofit 设计的一个好的地方就是它把我们上面提到的 “请求适配器” 和 “类型转换器” 使用策略模式解耦出来。用户可以根据自己的需求通过实现指定的接口来自定义自己的类型转换器。所以，当我们使用 Gson 进行转换和 RxJava2 进行适配的时候，就需要指定下面三个依赖：\n\n```groovy\n    api 'com.squareup.retrofit2:retrofit:2.4.0'\n    api 'com.squareup.retrofit2:converter-gson:2.4.0'\n    api 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'\n```\n\n然后，我们需要根据自己的 API 接口的信息，在代码里用一个接口来对该 API 进行声明：\n\n```java\npublic interface WXInfoService {\n\t@GET(\"/sns/userinfo\")\n\tObservable<WXUserInfo> getWXUserInfo(\n        @Query(\"access_token\") String accessToken, @Query(\"openid\") String openId);\n}\n```\n\n这里的 `WXUserInfo` 是由该 API 接口返回的 Json 生成的 Java 对象。然后，我们可以像下面这样获取一个该接口的代理对象：\n\n```java\n\tWXInfoService wXInfoService = new Retrofit.Builder()\n        .baseUrl(\"https://api.weixin.qq.com/\")\n        .addConverterFactory(GsonConverterFactory.create())\n        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n        .client(okHttpClient)\n        .build().create(WXInfoService.class);\n```\n\n然后，我们就可以使用该对象并调用其方法来获取接口返回的信息了：\n\n```java\n    Disposable disposable = wxInfoService.getWXUserInfo(accessToken, openId)\n        .subscribeOn(Schedulers.io())\n        .observeOn(AndroidSchedulers.mainThread())\n        .subscribe(wxUserInfo -> { /*...拿到结果之后进行处理...*/ });\n```\n\n上面我们只使用了 Retrofit 最基础的 `GET` 接口。当然，Retrofit 本身的功能远比这要丰富得多，关于其更多的使用，可以参考其官方的文档。\n\n## 2、动态代理：魔力发生的地方\n\n上面我们使用 Retrofit 进行网络请求，实际其内部使用 OkHttp 来完成网络请求的。仅定义了一个接口并调用了该接口的方法，就拿到了请求的结果，这看上去非常简洁，而这其中的功不可没的就是**动态代理**。\n\n当我们使用 `Retrofit.Builder` 的 `create()` 方法获取一个 `WXInfoService` 实例的时候，实际返回的是经过代理之后的对象。该方法内部会调用 `Proxy` 的静态方法 `newProxyInstance()` 来得到一个代理之后的实例。为了说明这个方法的作用，我们写了一个例子：\n\n```java\n    public static void main(String...args) {\n        Service service = getProxy(Service.class);\n        String aJson = service.getAInfo();\n        System.out.println(aJson);\n        String bJson = service.getBInfo();\n        System.out.println(bJson);\n    }\n\n    private static <T> T getProxy(final Class<T> service) {\n        InvocationHandler h = (proxy, method, args) -> {\n            String json = \"{}\";\n            if (method.getName().equals(\"getAInfo\")) {\n                json = \"{A请求的结果}\";\n            } else if (method.getName().equals(\"getBInfo\")) {\n                json = \"{B请求的结果}\";\n            }\n            return json;\n        };\n        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, h);\n    }\n```\n\n    该程序的输出结果：\n\n     {A请求的结果}\n     {B请求的结果}\n\n在上面的这个例子中，我们先使用 `getProxy()` 获取一个代理之后的实例，然后依次调用它的 `getAInfo()` 和 `getBInfo()` 方法，来模拟调用 A 接口和 B 接口的情形，并依次得到了 A 请求的结果和 B 请求的结果。\n\n上面的效果近似于我们使用 Retrofit 访问接口的过程。为了说明这个过程中发生了什么，我们需要先了解一下这里的 `newProxyInstance()` 方法：\n\n```java\n    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {\n        // ...\n    }\n```\n\n该方法接收三个参数：\n\n1. 第一个是类加载器；\n2. 第二个是接口的 Class 类型；\n3. 第三个是一个处理器，你可以将其看作一个用于回调的接口。当我们的代理实例触发了某个方法的时候，就会触发该回调接口的方法。\n\n`InvocationHandler` 是一个接口，它内部定义了一个方法如下：\n\n    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;\n\n该方法也接收三个参数，第一个是触发该方法的代理实例；第二个是触发的方法；第三个是触发的方法的参数。`invoke()` 方法的返回结果会作为代理类的方法执行的结果。\n\n所以，当了解了 `newProxyInstance()` 方法的定义之后，我们可以做如下总结：当我们使用 `newProxyInstance()` 方法获取了一个代理实例 service 并调用其 `getAInfo()` 方法之后，该方法的信息和参数信息会分别通过 method 和 args 传入到 h 的 `invoke()` 中。所以，最终的效果就是，当我们调用 service 的 `getAInfo()` 时候会触发 h 的 `invoke()`。在 `invoke()` 方法中我们根据 method 得知触发的方法是 `getAInfo`。于是，我们把它对应的请求从 `invoke()` 方法中返回，并作为 `service.getAInfo()` 的返回结果。\n\n所以，我们可以总结 Retrofit 的大致工作流程：当我们获取了接口的代理实例，并调用它的 `getWXUserInfo()` 方法之后，该方法的请求参数会传递到代理类的 `InvocationHandler.invoke()` 方法中。然后在该方法中，我们将这些信息转换成 OkHttp 的 Request 并使用 OkHttp 进行访问。从网络中拿到结果之后，我们使用 “转换器” 将响应转换成接口指定的 Java 类型。\n\n上面是 Retrofit 请求处理的基本流程，下面我们看一下 Retrofit 的代理方法内部究竟发生了什么。\n\n## 3、Retrofit 的源码解析\n\n### 3.1 创建 Retrofit\n\n根据上面的例子，当使用 Retrofit 的时候，首先我们需要使用 Retrofit 的构建者来创建 Retrofit 的实例。这里的构建者有几个重要的方法需要提及一下：\n\n#### 3.1.1 addConverterFactory 方法\n\n该方法用来向 Retrofit 中添加一个 `Converter.Factory`。`Converter.Factory`，顾名思义是一种工厂模式。它是一个接口需要，实现两个重要的方法。每个方法需要返回一个转换器：某种数据类型到请求体的转换器，响应体到我们需要的数据类型的转换器。当我们使用 Gson 来完成这个转换，那么我们就需要使用 `GsonConverterFactory.create()` 来得到一个适用于 Gson 的 `Converter.Factory`。\n\n    public Builder addConverterFactory(Converter.Factory factory) {\n      converterFactories.add(checkNotNull(factory, \"factory == null\"));\n      return this;\n    }\n\n#### 3.1.2 addCallAdapterFactory 方法\n\n`CallAdapter.Factory` 用于获取 `CallAdapter` 对象， `CallAdapter` 对象用于把原生的 OkHttp 的 `Call` 转换成我们指定的请求类型。比如，上面的例子中，我们用来将其转换成 `Observable<WXUserInfo>`。下面是该方法的定义：\n\n    public Builder addCallAdapterFactory(CallAdapter.Factory factory) {\n      callAdapterFactories.add(checkNotNull(factory, \"factory == null\"));\n      return this;\n    }\n\n#### 3.1.3 build 方法\n\n当根据用户的自定义设置完了参数之后，就可以调用 `build()` 方法，来获取一个 Retrofit 的实例。在该方法中会将上述方法传入的 “适配器” 和 “转换器” 添加到各自的列表中，然后 `new` 出一个 `Retrofit` 的实例并返回。\n\n#### 3.1.4 小结\n\n为了说明适配器 `CallAdapter` 和转换器 `Converter` 的作用，我们绘制了下图：\n\n![Retrofit类图](https://user-gold-cdn.xitu.io/2018/10/20/1669137d82462ec1?w=888&h=530&f=png&s=24731)\n\n从上面我们看出，`CallAdapter` 主要用来将某个请求转换成我们指定的类型。比如，在我们最开始的例子中，要将请求转换成 `Observable<WXUserInfo>`。如果转换之后的请求是 `Observable` 类型的，那么当我们对转换后的请求进行订阅的时候，就启动了 OkHttp 的网络请求过程。\n\n在进行网络请求之前会先使用 `Converter` 将请求的参数转换成一个 `RequestBody`。这里将其作为一个接口的好处是便于解耦。比如，上面我们用 Gson 来完成转换过程，你也可以通过自定义转换器来使用其他的框架，比如 Moshi 等。当拿到了响应之后，我们又会再次使用 `Converter` 来将响应体 `ResponseBody` 转换成我们要求的类型。比如 `WXUserInfo`。\n\n从上面我们看出，Retrofit 设计的非常妙的地方就在于上面的两个过程的解耦（策略模式+工厂模式+适配器模式）。一次是将请求转换成 `Observable` 的过程，一次是将请求体和响应体转换成 OkHttp 要求的 `RequestBody` 和 `ResponseBody` 的过程。对于前者，不论我们使用的是 RxJava 1 还是 RxJava 2，只要传入一个 `CallAdapter` 即可。对于后者，不论我们使用哪种 Json 转换框架，只要实现了 `Converter` 接口皆可。\n\n### 3.2 获取代理实例\n\n#### 3.2.1 划分平台：Platform\n\n创建了 Retrofit 的实例之后，我们就可以使用它的 `create()` 方法来获取代理之后的服务实例。下面是这个方法的定义。在这里，我们会先根据 `validateEagerly` 变量来判断是否立即对传入的服务接口的方法进行解析。然后，我们使用 `Proxy` 的静态方法获取一个代理实例。\n\n    public <T> T create(final Class<T> service) {\n        Utils.validateServiceInterface(service);\n        // 这里的 validateEagerly 在 Retrofit 构建的时候设置\n        if (validateEagerly) {\n            // 是否立即对 Service 方法的内容进行解析\n            eagerlyValidateMethods(service);\n        }\n        // 获取代理实例\n        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },\n                new InvocationHandler() {\n                    private final Platform platform = Platform.get();\n                    @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)\n                            throws Throwable {\n                        // 该方法是 Object 的方法，直接触发该方法\n                        if (method.getDeclaringClass() == Object.class) {\n                            return method.invoke(this, args);\n                        }\n                        // 如果是 default 方法，那么使用该 Java8 平台的方法执行\n                        if (platform.isDefaultMethod(method)) {\n                            return platform.invokeDefaultMethod(method, service, proxy, args);\n                        }\n                        // 获取服务方法的信息，并将其包装成 ServiceMethod\n                        ServiceMethod<Object, Object> serviceMethod =\n                                (ServiceMethod<Object, Object>) loadServiceMethod(method);\n                        OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);\n                        return serviceMethod.adapt(okHttpCall);\n                    }\n                });\n    }\n\n这里的 `eagerlyValidateMethods()` 方法定义如下：\n\n    private void eagerlyValidateMethods(Class<?> service) {\n        // 获取程序当前运行的平台\n        Platform platform = Platform.get();\n        for (Method method : service.getDeclaredMethods()) {\n            // 判断该方法是否是 default 方法\n            if (!platform.isDefaultMethod(method)) {\n                loadServiceMethod(method);\n            }\n        }\n    }\n\n它的作用是立即对服务接口的方法进行解析，并将解析之后的结果放进一个缓存中。这样，当这个服务方法被触发的时候，直接从缓存当中获取解析之后的 `ServiceMethod` 来使用即可。该方法会先会根据当前程序运行的平台来决定是否应该加载服务的方法。因为，Java 8 之后，我们可以为接口增加 `default` 类型的方法，所以，如果是 `default` 类型的话，我们不会调用 `loadServiceMethod()` 进行解析，而是调用 Java8 平台的 `invokeDefaultMethod()` 来处理。在 `invokeDefaultMethod()` 中，会根据传入的信息创建一个实例并使用反射触发它的方法。此时，就间接地触发了该 `default` 方法。\n\n判断平台的时候，使用了如下这段代码：\n\n    platform.isDefaultMethod(Method)\n\n这里的 platform 是调用 `Platform.get()` 的时候得到的。它会在 `get()` 方法中尝试使用反射去获取一个只有 Java8 平台才具有的类，以此来判断是否是 Java8 的环境。在 Retrofit 中，提供了 `Java8` 和 `Android` 两个类来区分所在的平台，并会根据运行环境来决定返回哪个实例。\n\n所以，Platform 应用了策略模式，以对不同的平台做不同的处理。在当前的版本中，它的主要作用是对 `default` 类型的方法进行处理。\n\n#### 3.2.2 解析服务方法：ServiceMethod\n\n上面我们提到过 `loadServiceMethod()` 方法，它的主要作用：首先会尝试从缓存当中获取该方法对应的 `ServiceMethod` 实例，如果取到的话，就将其返回；否则，就使用构建者模式创建一个并放进缓存中，然后将其返回。\n\n    ServiceMethod<?, ?> loadServiceMethod(Method method) {\n        // 从缓存中进行获取\n        ServiceMethod<?, ?> result = serviceMethodCache.get(method);\n        if (result != null) return result;\n        synchronized (serviceMethodCache) {\n            result = serviceMethodCache.get(method);\n            if (result == null) {\n                // 创建ServiceMethod实例\n                result = new ServiceMethod.Builder<>(this, method).build();\n                serviceMethodCache.put(method, result);\n            }\n        }\n        return result;\n    }\n\n`ServiceMethod` 的构建过程比较简单，只需要把当前的 `Retrofit` 实例和服务方法 `method` 传入进去，然后调用它的 `build()` 方法就完成了整个创建过程。在 `build()` 方法中，会完成对 `method` 的解析，比如根据注解判断是什么类型的请求，根据方法的参数来解析请求的请求体等等。\n\n所以，`ServiceMethod` 的作用是缓存服务方法对应的请求信息，这样下次我们就不需要再次解析了。同时，它提供了以下几个方法。它们的主要作用是用来从 `ServiceMethod` 中获取请求相关的信息：\n\n`toCall()` 用来获取用于 OkHttp 请求的 `Call` 对象：\n\n    okhttp3.Call toCall(@Nullable Object... args) throws IOException {\n        RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,\n                contentType, hasBody, isFormEncoded, isMultipart);\n        // ...\n        return callFactory.newCall(requestBuilder.build());\n    }\n\n`toResponse(ResponseBody)` 用来把 OkHttp 得到的响应体转换成 Java 对象等（在示例中是`WXUserInfo`）：\n\n    R toResponse(ResponseBody body) throws IOException {\n        return responseConverter.convert(body);\n    }\n\n`adapt(Call<R>)` 用来将 OkHttp 的请求转换成我们的服务方法的返回类型（在示例中是`Observable<WXUserInfo>`）：\n\n    T adapt(Call<R> call) {\n        return callAdapter.adapt(call);\n    }\n\n#### 3.2.3 请求封装：OkHttpCall\n\n解析完毕服务方法之后，我们得到了 `ServiceMethod` 实例。然后，我们使用它来创建 `OkHttpCall` 实例。这里的 `OkHttpCall` 实现了 Retrofit 中定义的 `Call` 接口，会在方法内调用 `ServiceMethod` 的 `toCall()` 方法来获取 OkHttp 中的 `Call` 对象，然后使用它进行网络访问。当拿到了请求的结果之后又使用 `ServiceMethod` 的 `toResponse()` 把响应转换成我们指定的类型。下面是该类中的几个比较重要的方法：\n\n1. `execute()` 方法，用来同步执行网络请求：\n\n```\n    @Override\n    public Response<T> execute() throws IOException {\n        okhttp3.Call call;\n        synchronized (this) {\n            // ...\n            call = rawCall;\n            if (call == null) {\n                try {\n                    // 创建 OkHttp 的 Call 实例\n                    call = rawCall = createRawCall();\n                } catch (IOException | RuntimeException | Error e) {\n                    throwIfFatal(e);\n                    creationFailure = e;\n                    throw e;\n                }\n            }\n        }\n        if (canceled) {\n            call.cancel();\n        }\n        // 同步执行请求，并解析结果\n        return parseResponse(call.execute());\n    }\n```\n\n2. `createRawCall()` 用来创建 OkHttp 的 `Call` 实例：\n\n```\n    // 使用 serviceMethod 的 toCall 方法获取 OkHttp 的 Call 实例\n    private okhttp3.Call createRawCall() throws IOException {\n        okhttp3.Call call = serviceMethod.toCall(args);\n        if (call == null) {\n            throw new NullPointerException(\"Call.Factory returned null.\");\n        }\n        return call;\n    }\n```\n\n3. `parseResponse()` 用来将 OkHttp 的响应转换成我们接口中定义的类型。比如，在我们的例子中，返回的是 `Observable<WXUserInfo>`:\n\n```\n    // 使用 serviceMethod 的 toResponse 方法获取 OkHttp 的 Response 实例\n    Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {\n        ResponseBody rawBody = rawResponse.body();\n        rawResponse = rawResponse.newBuilder()\n                .body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))\n                .build();\n        // ...\n        ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);\n        try {\n            // 使用 serviceMethod 的 toResponse 方法获取 OkHttp 的 Response 实例\n            T body = serviceMethod.toResponse(catchingBody);\n            return Response.success(body, rawResponse);\n        } catch (RuntimeException e) {\n            catchingBody.throwIfCaught();\n            throw e;\n        }\n    }\n```\n\n### 3.3 Retrofit 的工作过程\n\n上面是 Retrofit 框架设计中几个关键的部分的功能的解析。下面，我们再来具体看一下，从触发代理类的方法到拿到响应的结果，这一整个过程中，都有哪些类的哪些方法参与，以及它们在什么时候，扮演什么样的角色。这里我们仍然使用最初的示例：\n\n![Retrofit的执行过程](https://user-gold-cdn.xitu.io/2018/10/20/1669137d857c4084?w=1567&h=831&f=png&s=114379)\n\n上图中，我们将 Retrofit 的请求的过程分成三个过程来进行说明：\n\n1. **创建代理实例的过程**：在这个过程中主要是调用 `Proxy.newProxyInstance()` 来获取一个代理实例。相关的主要参数是 `validateEagerly`，我们会使用它来决定是否立即对传入的接口的方法进行解析。不论我们什么时候进行解析，都会把解析的结果缓存起来。\n2. **触发代理方法的过程**：触发代理方法是整个请求的第二过程。这个时候，我们调用了 `WXInfoService` 代理实例的 `getWXUserInfo()` 方法。此时，会触发 `InvocationHandler.invoke()` 方法。在该方法内部会调用 `ServiceMethod` 的构建者模式来创建 `serviceMethod` 实例。当调用构建者模式的 `build()` 方法的时候，会对方法 `getWXUserInfo()` 的信息进行解析。然后，使用 `serviceMethod` 创建 `okHttpCall`。最后，调用 `serviceMethod.adapt()` 方法将 `okHttpCall` 实例转换成 `Observable<WXUserInfo>`。在转换的过程中会使用 `CallAdapter` 的 `adapt()` 方法来完成适配。\n3. **执行网络请求的过程**：拿到了 `Observable<WXUserInfo>` 之后，需要对其进行订阅才能触发网络请求。相关的逻辑在 `CallAdapter` 中完成。首先，它会根据你使用同步还是异步的来决定使用哪个执行器。这里存在两个执行器，它们的区别是一个会在内部调用 `OkHttpCall` 的 `enqueue()`，另一个会在执行器中调用 `OkHttpCall` 的 `execute()` 方法。不论调用 `enqueue()` 还是 `execute()`，都会先使用 `OkHttpCall` 的 `toCall()` 方法获取一个 `Call` 请求。获取请求的过程中会使用 `Converter` 来将某个实例转换成请求体。拿到了请求之后，使用该请求来进行网络访问。当从网络中拿到了响应之后，会使用 `Converter` 来将响应体转换成对象。这样，拿到了实际的结果之后，就会调用 `Observer` 的 `onNext()` 方法把结果通知给观察者。\n\n## 4、总结\n\n在这篇文章中，我们先简单介绍了 Retrofit 的使用，然后，因为 Retrofit 内部使用动态代理来实现的，所以，我们对动态代理相关内容进行了介绍。最后，我们对 Retrofit 的源码进行了分析，先从设计思路，后从各个环节的执行过程进行了说明。最后的最后，我们将两者结合起来用一个时序图做了说明。\n\n从上文中可以看出来，Retrofit 设计的几个值得我们借鉴的地方：\n\n1. 使用运行时注解和反射简化请求描述，但是考虑到反射的效率比较低，所以将一次反射之后的结果缓存起来，以便于下次使用。\n2. 动态代理：使用接口描述请求的好处是它简洁，而且 “描述” 本来就是它的责任。但是，一般我们需要去实现接口才能使用。而这里告诉我们，使用动态代理一样可以使用接。\n3. 解耦：从我们上面的图中也可以看出来，Retrofit 的设计的思路是比较清晰的。它将一个请求的几个过程解耦出来。首先是我们 `Observable` 到请求的转换，这里使用适配器来完成；然后是请求体和响应体的转换，基本就是 Json 的转换，使用转换器来完成。这样，不论你使用 RxJava 1 还是 RxJava 2，不论是 Gson 还是 FastXml 都可以和 Retrifut 配合使用。\n\n以上就是我们对 Retrofit 的源码的分析。"
  },
  {
    "path": "高阶技术/Android插件化.md",
    "content": "# Android 插件化框架 DynamicLoadApk 源码分析\n\nDynamicLoadApk 应该算是 Android 插件化诸多框架中资历比较老的一个了。它的项目地址在：[dynamic-load-apk](https://github.com/singwhatiwanna/dynamic-load-apk)。该项目运行之后的效果是，使用 Gradle 编译出插件包和宿主包，都是以 APK 的形式。安装宿主包之后，通过 ADB 将插件包 push 到手机中。启动宿主包时，它会自动进行扫描将插件加载到应用中。点击插件之后，进入到插件的应用界面。\n\n印象中最初接触的插件都是以单独安装的形式存在的，比如我可以做一个基础的应用，然后在该应用的基础上开发插件。用户可以对插件进行选择，然后下载并安装，以让自己的应用具有更丰富的功能。插件化也算是一种比较实用的技术，毕竟我们使用 Chrome 和 AS 的时候不是一样要加载插件。只是比较反感的是去修改底层的代码，容易给系统带来不稳定因素不说，技术到了一些人手里，你知道他用来干什么。插件化挺好，但真的要去推广这项技术，还是看好 Google 官方去进行规范。\n\n技术要服务于产品，好的产品不一定要高超的技术，技术并不是最重要的，重要的是你究竟想要表达什么。这就像国内很多人只注重数理化，不注重人文学科。相比于国内的技术精英，我还是比较赞同 Google 站在整个生态的角度去考虑技术演进。前些日子社区里对插件化的讨论：[移动开发的罗曼蒂克消亡史](https://infoq.cn/article/V4623ZeOsEI*HGKmX2M1)。好吧，我自己的理解是，这从来就不是什么罗曼蒂克。\n\nDynamicLoadApk 插件化的实现方式还是挺有意思的，它使用纯 Java 实现，没有涉及 Native 层的代码，下面我理了下 DynamicLoadApk 的 Demo 程序的整个执行过程。后续的文章我们就围绕这张图进行，\n\n![DynamicLoadApk 的插件化执行过程](res/QQ截图20190302122352.png)\n\n首先是扫描文件路径并加载 APK，这里需要解析 APK 文件的信息，它是本质上是通过 PMS 实现的；\n\n```java\n    public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {\n        mFrom = DLConstants.FROM_EXTERNAL;\n\n        // 通过 PMS 获取包信息，这里获取了 Activity 和 Service 的信息\n        PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,\n                PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);\n        if (packageInfo == null) {\n            return null;\n        }\n\n        DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);\n        if (hasSoLib) {\n            copySoLib(dexPath);\n        }\n\n        return pluginPackage;\n    }\n```\n\n然后，它通过调用 `preparePluginEnv()` 方法来创建 AssetManager, DexClassLoader 和 Resource 等。我们的插件类加载各种资源和类的时候使用的就是这哥仨：\n\n```java\n    private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {\n\n        DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);\n        if (pluginPackage != null) {\n            return pluginPackage;\n        }\n        DexClassLoader dexClassLoader = createDexClassLoader(dexPath);\n        AssetManager assetManager = createAssetManager(dexPath);\n        Resources resources = createResources(assetManager);\n        // create pluginPackage\n        pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);\n        mPackagesHolder.put(packageInfo.packageName, pluginPackage);\n        return pluginPackage;\n    }\n\n    private DexClassLoader createDexClassLoader(String dexPath) {\n        File dexOutputDir = mContext.getDir(\"dex\", Context.MODE_PRIVATE);\n        dexOutputPath = dexOutputDir.getAbsolutePath();\n        DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir, mContext.getClassLoader());\n        return loader;\n    }\n\n    private AssetManager createAssetManager(String dexPath) {\n        try {\n            AssetManager assetManager = AssetManager.class.newInstance();\n            Method addAssetPath = assetManager.getClass().getMethod(\"addAssetPath\", String.class);\n            addAssetPath.invoke(assetManager, dexPath);\n            return assetManager;\n        } catch (Exception e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    private Resources createResources(AssetManager assetManager) {\n        Resources superRes = mContext.getResources();\n        Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());\n        return resources;\n    }\n```\n\n然后点击插件的时候要启动插件的 Activity，\n\n```java\n    PluginItem item = mPluginItems.get(position);\n    DLPluginManager pluginManager = DLPluginManager.getInstance(this);\n    pluginManager.startPluginActivity(this, new DLIntent(item.packageInfo.packageName, item.launcherActivityName));\n```\n\n这里会把要启动的包名和启动类名包装到 DLIntent 中。DLIntent 是 Intent 的子类。启动插件的进一步的逻辑在 DLPluginManager 的 `startPluginActivity()` 方法中。按照上文的描述，这里主要做了以下四件事情：\n\n1. 判断要启动的 Activity 是否是插件 Activity：因为要启动的类也可能不是插件类，所以我们需要分成两种情况来进行处理，普通的 Activity 直接调用 `Context.startActivity()` 插件 Activity 需要调用代理 Activity 来执行。\n2. 判断包名，获取插件相关信息：这里就算是一个安全的校验吧，主要是从之前解析的 APK 信息中进行校验。\n3. 使用插件的 DexClassLoader 加载启动类：先要使用类加载器加载插件的 Activity 到内存中，插件 Activity 的信息会作为 Intent 的参数一起传递给代理 Activity。\n4. 使用 `DLIntent.setClass()` 启动代理类：要启动的代理类可能是 DLProxyFragmentActivity 和 DLProxyActivity，所以这里我们先使用 `getProxyActivityClass()` 得到代理类。该方法中使用了 Class 的 `isAssignableFrom()` 方法来判断某个实例是否是指定类型的。比如 `DLBasePluginActivity.class.isAssignableFrom(clazz)` 表示 clazz 是否是 DLBasePluginActivity 类型的。\n\n```java\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {\n        // 1.判断要启动的 Activity 是否是插件 Activity\n        if (mFrom == DLConstants.FROM_INTERNAL) {\n            dlIntent.setClassName(context, dlIntent.getPluginClass());\n            performStartActivityForResult(context, dlIntent, requestCode);\n            return DLPluginManager.START_RESULT_SUCCESS;\n        }\n\n        // 2.判断包名，获取插件相关信息\n        String packageName = dlIntent.getPluginPackage();\n        if (TextUtils.isEmpty(packageName)) {\n            throw new NullPointerException(\"disallow null packageName.\");\n        }\n\n        DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);\n        if (pluginPackage == null) {\n            return START_RESULT_NO_PKG;\n        }\n\n        // 3.使用插件的 DexClassLoader 加载启动类\n        final String className = getPluginActivityFullPath(dlIntent, pluginPackage);\n        Class<?> clazz = loadPluginClass(pluginPackage.classLoader, className);\n        if (clazz == null) {\n            return START_RESULT_NO_CLASS;\n        }\n\n        Class<? extends Activity> activityClass = getProxyActivityClass(clazz);\n        if (activityClass == null) {\n            return START_RESULT_TYPE_ERROR;\n        }\n\n        // 4.使用 DLIntent.setClass() 启动代理类，并传入插件类和包信息\n        dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);\n        dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);\n        dlIntent.setClass(mContext, activityClass);\n        performStartActivityForResult(context, dlIntent, requestCode);\n        return START_RESULT_SUCCESS;\n    }\n\n    private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {\n        Class<? extends Activity> activityClass = null;\n        if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {\n            activityClass = DLProxyActivity.class;\n        } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {\n            activityClass = DLProxyFragmentActivity.class;\n        }\n\n        return activityClass;\n    }\n\n    private void performStartActivityForResult(Context context, DLIntent dlIntent, int requestCode) {\n        if (context instanceof Activity) {\n            ((Activity) context).startActivityForResult(dlIntent, requestCode);\n        } else {\n            context.startActivity(dlIntent);\n        }\n    }\n```\n\n这里需要注意下，我们的插件 Activity 是需要继承 DLBasePluginActivity 或者 DLProxyFragmentActivity。这两个类中重写了 Activity 的许多生命周期方法。在代理 Activity 启动之后，代理 Activity 会被传递到前面两个基类中。比如，当插件类想要获取 AssetsManager 的时候，会调用到这两个基类的 `getAssetsManager()`，然后基类通过代理类得到之前我们创建的 AssetsManager. \n\n按照上述流程，代理类被正常启动。启动之后它会创建 DLProxyImpl 实例，并在 `onCreate()` 方法中调用 DLProxyImpl 的 `onCreate()` 方法:\n\n```java\n    public void onCreate(Intent intent) {\n        intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);\n\n        mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);\n        mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);\n\n        mPluginManager = DLPluginManager.getInstance(mProxyActivity);\n        mPluginPackage = mPluginManager.getPackage(mPackageName);\n        mAssetManager = mPluginPackage.assetManager;\n        mResources = mPluginPackage.resources;\n\n        initializeActivityInfo();\n        handleActivityInfo();\n        launchTargetActivity();\n    }\n\n    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)\n    protected void launchTargetActivity() {\n        try {\n            Class<?> localClass = getClassLoader().loadClass(mClass);\n            Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});\n            Object instance = localConstructor.newInstance(new Object[] {});\n            mPluginActivity = (DLPlugin) instance;\n            ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);\n            mPluginActivity.attach(mProxyActivity, mPluginPackage);\n\n            Bundle bundle = new Bundle();\n            bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);\n            mPluginActivity.onCreate(bundle);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n```\n\n这里的主要逻辑在上述两个方法中。第一个方法中会根据包名从 DLPluginManager 中获取包的类加载器，然后使用该加载其加载器加载插件类，反射触发其构造方法，获取实例。然后调用代理 Activity 的 `attach()` 方法将该插件类复制给代理类。然后当 AMS 回调代理类的各个生命周期的时候，代理类调用插件类的各个生命周期。（这里会使用类加载器再次加载插件类，其实这是没必要的，我们可以直接使用 Intent 将插件类的 Class 通过序列化的方式传递过来，然后直接触发其构造方法即可，无需再次执行类加载逻辑。）\n\n好了，以上就是 DynamicLoadApk 的原理，其实本质就是：插件类作为一个普通的类被调用，它不归 AMS 负责。当我们启动插件的时候，实际启动的是代理类，当 AMS 回调代理类的生命周期的时候，代理类再调用插件类的各个生命周期方法。只是，对资源和类加载的部分需要注意下，因为我们需要进行自定义配置来把它们的路径指向我们的插件包。\n\n\n"
  },
  {
    "path": "高阶技术/Dagger从集成到源码.md",
    "content": "# Dagger 从集成到源码带你理解依赖注入框架\n\n> 本文从例子到源码来帮助你学习和理解 Dagger 的集成，因为只有例子没有源码的博文看了之后经常让人一头雾水。\n> 学编程重点在于理解，而不是死记硬背每个注解该怎么使用！\n> 所以，本文先用一个例子介绍 Dagger 的基本集成方式，然后，我们再看一下每个点具体的源码是如何实现的。\n\n## 啥是依赖注入？\n\n依赖注入就是取代了我们常用的 setter 和 getter 方法，也就是你不用每次调用某个示例的方法为它的一个变量赋值，\n你可以使用依赖注入直接将值注入进去，也就是使用依赖注入为实例的变量赋值。\n\n依赖注入在服务端比较常见，经典的如 Spring。而 Dagge r是一个小型的依赖注入框架，毕竟运行在移动端的代码要考虑程序的体积之类的。\n\n简单了解了依赖注入的概念，我们看下 Dagger 的基本使用方法和它的源码。\n其实，Activity 和 Service 的实现逻辑大同小异，我们没有必要面面俱到，所以，这里我们只以 Activity 的集成和源码为例。\n\n## 以Activity的集成为例\n\n我们以 Activity 的集成为例：首先我们自定义一个 Application 并将其配置到 Manifest 文件中：\n\n```java\n  public class MyApplication extends Application implements HasActivityInjector {\n\n    @Inject DispatchingAndroidInjector<Activity> activityInjector;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        DaggerAppComponent.builder().application(this).build().inject(this);\n    }\n\n    @Override\n    public AndroidInjector<Activity> activityInjector() {\n        return activityInjector;\n    }\n  }\n```\n\n这里，我们让自定义的 Application 实现 HasActivityInjector 接口，该接口中只有一个 `activityInjector()` 方法。\n正如上文所示，我们实现了该接口，并将注入到 Application 中的 `activityInjector` 作为值返回。\n\n先不管 `activityInjector` 是如何注入到 Application 中的，我们先看一下如何配置向A ctivity 中进行注入。\n\n```java\npublic abstract class CommonDaggerActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        AndroidInjection.inject(this);\n        super.onCreate(savedInstanceState);\n    }\n}\n```\n\n我们定义一个名为 CommonDaggerActivity 的抽象类，并在它的 `onCreate()` 方法中使用 `AndroidInjection.inject(this);` 进行注入。\n我们进入看它的源码，简化一下，移除没有用的代码之后：\n\n```java\n  public static void inject(Activity activity) {\n    Application application = activity.getApplication();\n    if (!(application instanceof HasActivityInjector)) { throw new RuntimeException(...) }\n    AndroidInjector<Activity> activityInjector = ((HasActivityInjector) application).activityInjector();\n    activityInjector.inject(activity);\n  }\n```\n\n所以，本质上就是获取当前 Activity 对应的 Application，然后将该 Application 向下转型为 HasActivityInjector.\n因为我们的 Application 是实现了 HasActivityInjector 接口的，所以可以成功向下转型，并获取到 AndroidInjector<Activity>.\n在获取了 AndroidInjector<Activity> 之后，并将当前的 Activity 注入进去。\n\n那么，现在我们有了一些思路了。不过还有几个问题：\n\n1. Application 中的 `activityInjector` 是如何被注入进去的，以及它是如何被初始化的？\n2. 当在 Activity 中调用了 `AndroidInjection.inject(this)` 之后发生了什么？\n\n## 更完整的示例\n\n你可能已经注意到，实际上在 `MyApplication` 中还有下面一行代码：\n\n```java\nDaggerAppComponent.builder().application(this).build().inject(this)\n```\n\n我们的 DaggerAppComponent 是由 AppComponent 在编译时自动生成的。（你可以在代码编译之后，从 `Android` 切换到 `Project` 来查看生成的代码。）\n\n这里的 `AppComponent` 的定义如下：\n\n```java\n@Singleton\n@Component(modules = {ActivityModule.class, ViewModelModule.class})\npublic interface AppComponent extends AndroidInjector<MyApplication> {\n\n    @Component.Builder\n    interface Builder {\n        @BindsInstance Builder application(Application application);\n        AppComponent build();\n    }\n}\n```\n\n我们用 `@Component` 注解，该注解中还定义了 `@Builder` 注解，正如你从上面的代码看到的。\n你可以对照这个代码和生成的 DaggerAppComponent，你会发现其实这里使用的是构建者模式。就是说：\n\n**使用@Component注解定义的类会生成DaggerComponent，使用@Component.Builder注解定义的内部类会作为构建器来使用。你可以通过在@Component.Builder注解的接口中按照需要添加自己的方法，**\n\n然后，这里在注解 `@Component` 中还通过modules引用 了`ActivityModule.class` 和 `ViewModelModule.class`.\n它们是定义的模块，我们在其中声明自己需要使用的变量等。下面给出它们的定义：\n\n```java\n@Module\npublic abstract class ActivityModule {\n\n    @ActivityScoped\n    @ContributesAndroidInjector\n    abstract MainActivity mainActivity();\n}\n\n@Module\npublic abstract class ViewModelModule {\n\n    @Binds\n    @IntoMap\n    @ViewModelKey(MainViewModel.class)\n    abstract ViewModel bindMainViewModel(MainViewModel mainViewModel);\n}\n```\n\n这里用到了两个自定义注解：\n\n```java\n@Documented\n@Scope\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface ActivityScoped { }\n\n@Documented\n@Target({ElementType.METHOD})\n@Retention(RetentionPolicy.RUNTIME)\n@MapKey\npublic @interface ViewModelKey {\n    Class<? extends ViewModel> value();\n}\n```\n\n这里的 `MainViewModel` 是我们定义的 ViewModel，用来演示注入到 Activity 中之后发生了什么。\n\n```java\npublic class MainViewModel  extends AndroidViewModel {\n    private static final String TAG = \"MainViewModel\";\n\n    @Inject\n    public MainViewModel(@NonNull Application application) {\n        super(application);\n    }\n\n    public void log() {\n        Log.d(TAG, \"log: \");\n    }\n}\n```\n\n这里还有个 MainActivity，以下是它的定义：\n\n```java\npublic class MainActivity extends CommonDaggerActivity {\n\n    @Inject\n    public MainViewModel mainViewModel;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n        mainViewModel.log();\n    }\n}\n```\n\n显然，这里我们希望通过注入来为 MainActivity 的局部变量 `mainViewModel` 赋值，并在 `onCreate()` 方法调用它的方法。\n\n当我们编译并执行程序之后，一切跟我们预期的一样：MainActivity 被执行，MainActivity 被注入进去，并成功输出了日志。\n\n## 点击build之后发生了什么\n\n上面我们通过一些简单的分析，知道了在 Activity 中调用 `AndroidInjection.inject(this)`，实际上调用了 `MyApplication` 的 `activityInjector` 的 `inject(activity)` 方法。\n然后，将 MainActivity 的字段注入进去。不过，我们还存在一些疑问，现在我们就对这些问题进行解答。\n\n尝试在 AS 中先执行 clean 然后再执行 build，到 build 下面去看下，生成了一些代码，其中就包含了 DaggerComponent，似乎一切的魔力就发生在这几秒钟的时间里。\n\n我们已经知道了在 Activity 中调用 `AndroidInjection.inject(this)`，实际上调用了`MyApplication` 的 `activityInjector` 的 `inject(activity)` 方法。\n还需要知道 `MyApplication` 中的 `activityInjector` 是如何被创建并注入的。\n\n从DaggerComponent 那里作为分析的起点，当调用了 `inject(MyApplication)`  之后最终调用了：\n\n```java\nMyApplication_MembersInjector.injectActivityInjector(instance, getDispatchingAndroidInjectorOfActivity());\n```\n\n而 `activityInjector` 就是从这里传入并初始化的。所以，`activityInjector` 的创建是在 `getDispatchingAndroidInjectorOfActivity()` 中完成的。\n\n果然，这里的 `getDispatchingAndroidInjectorOfActivity()` 通过下面的代码创建`activityInjector` 并将其返回：\n\n```java\nDispatchingAndroidInjector_Factory.newDispatchingAndroidInjector(getMapOfClassOfAndProviderOfFactoryOf())\n```\n\n而这里的 `getMapOfClassOfAndProviderOfFactoryOf()` 方法返回的是一个映射表，将我们配置的 Activity 通过字典与 Provider 关联起来。\n\n那 `newDispatchingAndroidInjector()` 方法又做了什么呢？它使用上述字典作为参数，`new` 一个 `DispatchingAndroidInjector` 实例。\n\n好了，整理一下：实际上，当我们调用 `AndroidInjection.inject(this)` 的时候，调用了 `new` 出的 `DispatchingAndroidInjector` 实例的 `inject(Activity)` 方法。\n\n那么，我们再来看一下 `DispatchingAndroidInjector` 中的 `inject(Activity)` 方法做了什么：\n\n```java\n  public void inject(T instance) {\n    // 实际调用inject方法的时候会调用maybeInject方法\n    boolean wasInjected = maybeInject(instance);\n    if (!wasInjected) {\n      throw new IllegalArgumentException(errorMessageSuggestions(instance));\n    }\n  }\n\n  public boolean maybeInject(T instance) {\n    // 这里先从我们上述的字典中取出Provider\n    Provider<AndroidInjector.Factory<? extends T>> factoryProvider = injectorFactories.get(instance.getClass());\n    if (factoryProvider == null) {\n      return false;\n    }\n\n\t// 然后从Provider中取出AndroidInjector.Factory方法\n    AndroidInjector.Factory<T> factory = (AndroidInjector.Factory<T>) factoryProvider.get();\n    try {\n\t  // 最后调用AndroidInjector.Factory的create()方法，获取一个“注入器”\n      AndroidInjector<T> injector = factory.create(instance);\n\t  // 调用\"注入器\"进行注入\n      injector.inject(instance);\n      return true;\n    } catch (ClassCastException e) {\n      throw new InvalidInjectorBindingException(...);\n    }\n  }\n```\n\n对应上面的代码分析：\n\n首先获取被传入对象的 Class，并从字典中获取 Provider，这里是使用 `MainActivity.class` 获取到 `mainActivitySubcomponentBuilderProvider`.\n`mainActivitySubcomponentBuilderProvider` 是在 DaggerComponent 中创建的，我们可以到 DaggerComponent 中看它的逻辑。\n\n调用 Provider 的 `get` 方法将创建并返回一个 `MainActivitySubcomponentBuilder` 实例（`MainActivitySubcomponentBuilder` 最终的继承自`AndroidInjector.Factory<T>`）。\n\n然后，我们调用了 `MainActivitySubcomponentBuilder` 的 `create()` 方法，会先执行了 `seedInstance(instance)`，然后执行了 `build()` 创建并返回一个“注入器”。\n最后，就是使用该\"注入器\"的 `inject()` 方法向 MainActivity 中的字段赋值的。\n\n这里的 `seedInstance(instance)` 和 `build()` 是两个模板方法，它们在 `MainActivitySubcomponentBuilder` 中实现并返回\"注入器\"。\n而\"注入器“实际上是 `MainActivitySubcomponentImpl` 的一个实例。那也就是说，实际上是使用了 `MainActivitySubcomponentImpl` 的 `inject()` 方法完成值的注入的。\n\n我们看下这个 `inject()` 方法的定义，它最终会执行下面这串代码：\n\n```java\nMainActivity_MembersInjector.injectMainViewModel(instance, getMainViewModel());\n```\n\n这里的`getMainViewModel()`方法也定义在`MainActivitySubcomponentImpl`中：\n\n```java\n    private MainViewModel getMainViewModel() {\n      return new MainViewModel(DaggerAppComponent.this.application);\n    }\n```\n\n可以看出，它的定义方式与我们定义的构造方法一致。\n\n最后的最后，我们会执行 MainActivity_MembersInjector 的方法完成注入：\n\n```java\n  public static void injectMainViewModel(MainActivity instance, MainViewModel mainViewModel) {\n    instance.mainViewModel = mainViewModel;\n  }\n```\n\n## 总结\n\n在上文中，我们通过分析生成的代码和我们的源码对 Dagger 的注入的原理进行了简单分析. 当然,在这里我们并没有深入去分析 Dagger 的框架的实现原理. \n因为这些生成的代码命名非常不规范,所以也导致我们分析的过程不那么简洁.\n\n我们做简单的总结如下:\n1. `@Component` 使用了构建者模式,我们可以对构建的过程需要的字段进行自定义;\n2. 需要注入变量的类会在编译期间生成一个名为 `类名_MembersInjector` 的注入器,并在使用名为 `inject变量名` 的静态方法进行变量注入;\n\n我们已经分析了 Dagger 的作用的原理, 相信通过这些简单的分析, 你至少已经不会对 Dagger 那么陌生了, 以后我们有机会会更多的分析它的实现的原理. \n"
  },
  {
    "path": "高阶技术/JNI技术总结.md",
    "content": "# 在 Android 中使用 JNI 的总结\n\n最近在研究 Android 相机相关的东西，因为想要对相机做一个封装，于是想到要提供支持滤镜和图像动态识别相关的接口。在我找到一些资料中，它们的实现：一个是基于 OpenGL 的，一个是基于 OpenCV 的。两者都可以直接使用 Java 进行开发，受制于 Java 语言的限制，所以当对程序的性能要求很高的时候，Java 就有些心有余力不足了。所以，有些实现 OpenCV 的方式是在 Native 层进行处理的。这就需要涉及 JNI 的一些知识。\n\n当然，JNI 并非 Android 中提出的概念，而是在 Java 中本来提供的。所以，在这篇文章中，我们先尝试在 IDEA 中使用 JNI 进行开发，以了解 JNI 运行的原理和一些基础知识。然后，再介绍下 AS 中使用更高效的开发方式。\n\n## 1、声明 native 方法\n\n### 1.1 静态注册\n\n首先，声明 Java 类，\n\n```java\npackage me.shouheng.jni;\n\npublic class JNIExample {\n\n    static {\n        // 函数System.loadLibrary()是加载dll（windows）或so（Linux）库，只需名称即可，\n        // 无需加入文件名后缀（.dll或.so）\n        System.loadLibrary(\"JNIExample\");\n        init_native();\n    }\n\n    private static native void init_native();\n\n    public static native void hello_world();\n\n    public static void main(String...args) {\n        JNIExample.hello_world();\n    }\n}\n```\n\nnative 的方法可以定义成 static 的和非 static 的，使用上和普通的方法没有区别。这里使用 `System.loadLibrary(\"JNIExample\")` 加载 JNI 的库。在 Window 上面是 dll，在 Linux 上面是 so. 这里的 `JNIExample` 只是库的名称，甚至都没有包含文件类型的后缀，那么 IDEA 怎么知道到哪里加载库呢？这就需要我们在运行 JVM 的时候，通过虚拟机参数来指定。在 IDEA 中的方式是使用 `Edit Configuration...`，然后在 `VM options` 一栏中输入 `-Djava.library.path=F:\\Codes\\Java\\Project\\Java-advanced\\java-advanced\\lib`，这里的路径是我的库文件所在的位置。\n\n使用 JNI 第一步是生成头文件，我们可以使用如下的指令，\n\n```shell\njavah -jni -classpath (搜寻类目录) -d (输出目录) (类名)\n```\n\n或者简单一些，先把 java 文件编译成 class，然后使用 class 生成 h 头文件，\n\n```powershell\njavac me/shouheng/jni/JNIExample.java\njavah me.shouheng.jni.JNIExample\n```\n\n上面的两个命令是可行的，只是要注意下文件的路径的问题。(也许我们可以使用 Java 或者其他的语言写些程序调用这些可执行文件来简化它的使用！)\n\n生成的头文件代码如下，\n\n```c\n/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class me_shouheng_jni_JNIExample */\n\n#ifndef _Included_me_shouheng_jni_JNIExample\n#define _Included_me_shouheng_jni_JNIExample\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n/*\n * Class:     me_shouheng_jni_JNIExample\n * Method:    init_native\n * Signature: ()V\n */\nJNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native\n  (JNIEnv *, jclass);\n\n/*\n * Class:     me_shouheng_jni_JNIExample\n * Method:    hello_world\n * Signature: ()V\n */\nJNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world\n  (JNIEnv *, jclass);\n\n#ifdef __cplusplus\n}\n#endif\n#endif\n```\n\n可以看出，它跟普通的 c 头文件多了 JNIEXPORT 和 JNICALL 两个指令，剩下的东西完全符合一般 c 头文件的规则。这里的 `Java_me_shouheng_jni_JNIExample_init_1native` 对应 Java 层的代码，可见它的规则是 `Java_Java层的方法路径` 只是方法路径使用了下划线取代了逗号，并且 Java 层的下划线使用 `_1` 替代，这是因为 Native 层的下划线已经用来替代 Java 层的逗号了，所以 Java 层的下划线只能用 `_1` 表示了。\n\n这里的 `JNIEnv` 是一个指针类型，我们可以用它访问 Java 层的代码，它不能跨进程被调用。你可以在 JDK 下面的 include 文件夹中的 `jni.h` 中找到它的定义。`jclass` 对应 Java 层的 Class 类。Java 层的类和 Native 层的类之间按照指定的规则进行映射，当然还有方法签名的映射关系。所谓方法签名，比如上面的 `()V`，当你使用 javap 反编译 class 的时候可以看到这种符号。它们实际上是 class 文件中的一种简化的描述方式，主要是为了节省 class 文件的内存。此外，方法签名还被用来进行动态注册 JNI 方法。\n\n![Native-Java 类型对应关系](res/QQ截图20190227124949.png)\n\n引用类型的对应关系如下，\n\n![引用类型的对应关系](res/QQ截图20190227125150.png)\n\n上面注册 JNI 的方式属于静态注册，可以理解为在 Java 层注册 Native 的方法；此外，还有动态注册，就是在 Native 层注册 Java 层的方法。\n\n### 1.2 动态注册\n\n除了按照上面的方式静态注册 native 方法，我们还可以动态进行注册。动态注册的方式需要我们使用方法的签名，下面是 Java 类型与方法签名之间的映射关系：\n\n![JNI方法签名](res/20140507203312765.jpg)\n\n注意这里的全限定类名以 `/` 分隔，而不是用 `.` 或 `_` 分隔。方法签名的规则是：`(参数1类型签名参数2类型签名……参数n类型签名)返回类型签名`。比如，`long fun(int n, String str, int[] arr)` 对应的方法签名为 `(ILjava/lang/String;[I)J`。\n\n一般 JNI 方法动态注册的流程是：\n\n1. 利用结构体 `JNINativeMethod` 数组记录 java 方法与 JNI 函数的对应关系；\n2. 实现 `JNI_OnLoad` 方法，在加载动态库后，执行动态注册；\n3. 调用 `FindClass` 方法，获取 java 对象；\n4. 调用 `RegisterNatives` 方法，传入 java 对象，以及 JNINativeMethod 数组，以及注册数目完成注册。\n\n比如上面的代码如果使用动态注册将会是如下形式：\n\n```c++\nvoid init_native(JNIEnv *env, jobject thiz) {\n    printf(\"native_init\\n\");\n    return;\n}\n\nvoid hello_world(JNIEnv *env, jobject thiz) {\n    printf(\"Hello World!\");\n    return;\n}\n\nstatic const JNINativeMethod gMethods[] = {\n        {\"init_native\", \"()V\", (void*)init_native},\n        {\"hello_world\", \"()V\", (void*)hello_world}\n};\n\nJNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {\n    __android_log_print(ANDROID_LOG_INFO, \"native\", \"Jni_OnLoad\");\n    JNIEnv* env = NULL;\n    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) // 从 JavaVM 获取JNIEnv，一般使用 1.4 的版本\n        return -1;\n    jclass clazz = env->FindClass(\"me/shouheng/jni/JNIExample\");\n    if (!clazz){\n        __android_log_print(ANDROID_LOG_INFO, \"native\", \"cannot get class: com/example/efan/jni_learn2/MainActivity\");\n        return -1;\n    }\n    if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])))\n    {\n        __android_log_print(ANDROID_LOG_INFO, \"native\", \"register native method failed!\\n\");\n        return -1;\n    }\n    return JNI_VERSION_1_4;\n}\n```\n## 2、执行 JNI 程序\n\n了解了如何加载，剩下的就是如何得到 dll 和 so. 在 Window 平台上面，我们使用 VS 或者 GCC 将代码编译成 dll. GCC 有两种选择，MinGW 和 Cygwin。这里注意下 GCC 和 JVM 的位数必须一致，即要么都是 32 位的要么都是 64 位的，否则将有可能抛出 `Can't load IA 32-bit .dll on a AMD 64-bit platform` 异常。\n\n查看虚拟机的位数使用 `java -version`，其中有明确写明 `64-bit` 的是 64 位的，否则是 32 位的。（参考：[如何识别JKD的版本号和位数，操作系统位数](https://jingyan.baidu.com/article/a378c960cbc339b32828300a.html).）MinGW 的下载可以到如下的链接：[MinGW Distro - nuwen.net](https://nuwen.net/mingw.html)。安装完毕之后输入 `gcc -v`，能够输出版本信息就说明安装成功。\n\n有了头文件，我们还要实现 native 层的方法，我们新建一个 c 文件 JNIExample.c 然后实现各个函数如下，\n\n```c\n#include<jni.h>\n#include <stdio.h>\n#include \"me_shouheng_jni_JNIExample.h\"\n\nJNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native(JNIEnv * env, jclass cls) {\n    printf(\"native_init\\n\");\n    return;\n}\n\nJNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world(JNIEnv * env, jclass cls) {\n    printf(\"Hello World!\");\n    return;\n}\n```\n\n看上去还是比较清晰的，除去 JNIEXPORT 和 JNICALL 两个符号之外，剩下的都是基本的 c 语言的东西。然后我们在方法中简单输出一个老朋友 `Hello World`. 注意下，这里除了基本的输入输出头文件 `stdio.h` 之外，我们还引入了刚才生成的头文件，以及 `jni.h`，后者定义在 JDK 当中，当我们使用 gcc 生成 dll 的时候就需要引用这个头文件。\n\n我们使用如下的命令来先生成 o 文件，\n\n```powershell\ngcc -c -I\"E:\\JDK\\include\" -I\"E:\\JDK\\include\\win32\" jni/JNIExample.c\n```\n\n这里的两个 `-I` 后面指定的是 JDK 中的头文件的路径。因为，按照我们上面说的，我们在 c 文件中引用了 `jni.h`，而该文件就位于 JDK 的 `include` 目录中。因为 include 中的头文件又引用了目录 win32 中的头文件，所以，我们需要两个都引用进来（心累）。\n\n然后，我们使用如下的命令将上述 o 文件转成 dll 文件，\n\n```powershell\ngcc -Wl,--add-stdcall-alias -shared -o JNIExample.dll JNIExample.o\n```\n\n如果你发现使用了 `,` 之后 PowerShell 无法执行，那么可以将 `,` 替换为 `\",\"` 再执行。\n\n生成 dll 之后，我们将其放入自定义的 lib 目录中。如我们上述所说的，需要在虚拟机的参数中指定这个目录。\n\n然后运行并输出久违的 `Hello world!` 即可。\n\n## 3、进一步接触 JNI：在 Native 中调用 Java 层的方法\n\n我们定义如下的类，\n\n```java\npublic class JNIInteraction {\n\n    static {\n        System.loadLibrary(\"interaction\");\n    }\n\n    private static native String outputStringFromJava();\n\n    public static String getStringFromJava(String fromString) {\n        return \"String from Java \" + fromString;\n    }\n\n    public static void main(String...args) {\n        System.out.println(outputStringFromJava());\n    }\n}\n```\n\n这里我们希望的结果是，Java 层调用 Native 层的 `outputStringFromJava()` 方法。在 Native 层中，该方法调用到 Java 层的静态方法 `getStringFromJava()` 并传入字符串，最后整个拼接的字符串通过 `outputStringFromJava()` 传递给 Java 层。\n\n以上是 Java 层的代码，下面是 Native 层的代码。Native 层去调用 Java 层的方法的步骤基本是固定的：\n\n1. 通过 JNIEnv 的 `FindClass()` 函数获取要调用的 Java 层的类；\n2. 通过 JNIEnv 的 `GetStaticMethodID()` 函数和上述 Java 层的类、方法名称和方法签名，得到 Java 层的方法的 id；\n3. 通过 JNIEnv 的 `CallStaticObjectMethod()` 函数、上述得到的类和上述方法的 id，调用 Java 层的方法。\n\n这里有两点地方需要说明：\n\n1. 这里因为我们要调用 Java 层的静态函数，所以我们使用的函数是 `GetStaticMethodID()` 和 `CallStaticObjectMethod()` 。如果你需要调用类的实例方法，那么你需要调用 `GetMethodID()` 和 `CallObjectMethod()`。诸如此类，JNIEnv 中还有许多其他有用的函数，你可以通过查看 jni.h 头文件来了解。\n2. Java 层和 Native 层的方法相互调用本身并不难，使用的逻辑也是非常清晰的。唯一比较复杂的地方在于，你需要花费额外的时间去处理两个环境之间的数据类型转换的问题。比如，按照我们上述的目标，我们需要实现一个将 Java 层传入的字符串转换成 Native 层字符串的函数。其定义如下，\n\n```c++\nchar* Jstring2CStr(JNIEnv* env, jstring jstr) {\n    char* rtn = NULL;\n    jclass clsstring = (*env)->FindClass(env, \"java/lang/String\");\n    jstring strencode = (*env)->NewStringUTF(env,\"GB2312\");\n    jmethodID mid = (*env)->GetMethodID(env, clsstring, \"getBytes\", \"(Ljava/lang/String;)[B\");\n    \n    // String.getByte(\"GB2312\");\n    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode);\n    jsize alen = (*env)->GetArrayLength(env, barr);\n    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);\n    \n    if(alen > 0) {\n        rtn = (char*)malloc(alen+1); //\"\\0\"\n        memcpy(rtn, ba, alen);\n        rtn[alen]=0;\n    }\n    (*env)->ReleaseByteArrayElements(env,barr,ba,0); //\n    return rtn;\n}\n```\n\n在上述函数中，我们通过调用 Java 层的 `String.getBytes()` 获取到 Java 层的字符数组，然后将其通过内存拷贝的方式复制到字符数组中。（通过 `malloc()` 函数申请内存，并将字符指针的指向申请的内存的首地址。）最后，还要调用 JNIEnv 的方法来释放字符数组的内存。这里也是一次 Native 调 Java 函数的过程，只是这里的调用 String 类的实例方法。（从这里也可以看出，Native 层写代码要考虑的因素比 Java 层多得多，好在这是 C 语言，如果 C++ 的化可能处理起来会好一些。）\n\n回到之前的讨论中，我们需要继续实现 Native 层的函数：\n\n```c++\nJNIEXPORT jstring JNICALL Java_me_shouheng_jni_interaction_JNIInteraction_outputStringFromJava (JNIEnv *env, jclass _cls) {\n    jclass clsJNIInteraction = (*env)->FindClass(env, \"me/shouheng/jni/interaction/JNIInteraction\"); // 得到类\n    jmethodID mid = (*env)->GetStaticMethodID(env, clsJNIInteraction, \"getStringFromJava\", \"(Ljava/lang/String;)Ljava/lang/String;\"); // 得到方法\n    jstring params = (*env)->NewStringUTF(env, \"Hello World!\");\n    jstring result = (jstring)(*env)->CallStaticObjectMethod(env, clsJNIInteraction, mid, params);\n    return result;\n}\n```\n\n其实它的逻辑也是比较简单的了。跟我们上面调用 String 的实例方法的步骤基本一致，只是这里调用的是静态方法。\n\n这样上述程序的效果是，当 Java 层调用 Native 层的 `outputStringFromJava()` 函数的时候：首先，Native 层通过调用 Java 层的 JNIInteraction 的静态方法 `getStringFromJava()` 并传入参数得到 `String from Java Hello World!` 之后将其作为 `outputStringFromJava()` 函数的结果返回。\n\n## 4、在 Android Studio 中使用 JNI\n\n上面在程序中使用 JNI 的方式可以说很笨拙了，还好在 Android Studio 中，许多过程被简化了。这让我们得以将跟多的精力放在实现 Native 层和 Java 层代码逻辑上，而无需过多关注编译环节这个复杂的问题。\n\n在 AS 中启用 JNI 的方式很简单：在使用 AS 创建一个新项目的时候注意勾选 `include C++ support` 即可。其他的步骤与创建一个普通的 Android 项目并无二致。然后你需要对开发的环境进行简单的配置。你需要安装下面几个库，即 CMake, LLDB 和 NDK：\n\n![AS 环境需求](https://img-blog.csdn.net/20171219103149422?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXV3ZWkzOTMwOTIx/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)\n\nAS 之所以能够简化我们的编译流程，很大程度上是得益于编译工具 [CMake](https://baike.baidu.com/item/cmake/7138032?fr=aladdin)。CMake 是一个跨平台的安装（编译）工具，可以用简单的语句来描述所有平台的安装 (编译过程)。我们只需要在它指定的 CMakeLists.txt 文件中使用它特定的语法描述整个编译流程，然后使用 CMake 的指令即可。你可以通过文档来了解如何在 AS 中使用 CMake：[add-native-code](https://d.android.com/studio/projects/add-native-code.html). 或者通过下面这篇文章简单入门下 CMake：[CMake 入门实战](https://www.hahack.com/codes/cmake/)。\n\n支持 JNI 开发的 Android 项目与普通的项目没有太大的区别，除了在 `local.properties` 中额外指定了 NDK 的目录之外，项目结构和 Gradle 的配置主要有如下的区别：\n\n![项目区别](res/QQ截图20190302004456.png)\n\n可以看出区别主要在于：\n\n1. main 目录下面多了个 cpp 目录用来编写 C++ 代码；\n2. app 目录下面多了各 CMakeLists.txt 就是我们上面提到的 CMake 的配置文件；\n3. 另外 Gradle 中里面一处指定了 CMakeLists.txt 文件的位置，另一处配置了 CMake 的编译；\n\n在 AS 中进行 JNI 开发的优势除了 CMake 之外，还有：\n\n1. 无需手动对方法进行动态注册和静态注册，当你在 Java 层定义了一个 native 方法之后，可以通过右键直接生成 Native 层对应的方法；\n2. 此外，AS 中可以建立 Native 层和 Java 层方法之间的联系，你可以直接在两个方法之间跳转；\n3. 当使用 AS 进行编程的时候，调用 Native 层的类的时候也会给出提示选项，比如上面的 JNIEnv 就可以给出其内部各种方法的提示。\n\n另外，从该初始化的项目以及 Android 的 Native 层的源码来看，Google 是支持我们使用 C++ 开发的。所以，吃了那么久灰的 C++ 书籍又可以派上用场了……\n\n## 总结\n\n以上。\n\n------\n\n**Android 从基础到高级，关注作者及时获取更多知识**\n\n本系列以及其他系列的文章均维护在 Github 上面：[Github / Android-notes](https://github.com/Shouheng88/Android-notes)，欢迎 Star & Fork. 如果你喜欢这篇文章，愿意支持作者的工作，请为这篇文章点个赞👍！"
  },
  {
    "path": "高阶技术/探索Android架构设计.md",
    "content": "# Android 应用架构设计探索：MVC、MVP、MVVM和组件化\n\nMVC、MVP和MVVM是常见的三种架构设计模式，当前MVP和MVVM的使用相对比较广泛，当然MVC也并没有过时之说。而所谓的组件化就是指将应用根据业务需求划分成各个模块来进行开发，每个模块又可以编译成独立的APP进行开发。理论上讲，组件化和前面三种架构设计不是一个层次的。它们之间的关系是，组件化的各个组件可以使用前面三种架构设计。我们只有了解了这些架构设计的特点之后，才能在进行开发的时候选择适合自己项目的架构模式，这也是本文的目的。\n\n## 1、MVC\n\nMVC (Model-View-Controller, 模型-视图-控制器)，标准的MVC是这个样子的：\n\n- 模型层 (Model)：业务逻辑对应的数据模型，无View无关，而与业务相关；\n- 视图层 (View)：一般使用XML或者Java对界面进行描述；\n- 控制层 (Controllor)：在Android中通常指Activity和Fragment，或者由其控制的业务类。\n\nActivity并非标准的Controller，它一方面用来控制了布局，另一方面还要在Activity中写业务代码，造成了Activity既像View又像Controller。\n\n在Android开发中，就是指直接使用Activity并在其中写业务逻辑的开发方式。显然，一方面Activity本身就是一个视图，另一方面又要负责处理业务逻辑，因此逻辑会比较混乱。\n\n这种开发方式不太适合Android开发。\n\n## 2、MVP\n\n### 2.1 概念梳理\n\nMVP (Model-View-Presenter) 是MVC的演化版本，几个主要部分如下：\n\n- 模型层 (Model)：主要提供数据存取功能。\n- 视图层 (View)：处理用户事件和视图。在Android中，可能是指Activity、Fragment或者View。\n- 展示层 (Presenter)：负责通过Model存取书数据，连接View和Model，从Model中取出数据交给View。\n\n所以，对于MVP的架构设计，我们有以下几点需要说明：\n\n1. 这里的Model是用来存取数据的，也就是用来从指定的数据源中获取数据，不要将其理解成MVC中的Model。在MVC中Model是数据模型，在MVP中，我们用Bean来表示数据模型。\n2. Model和View不会直接发生关系，它们需要通过Presenter来进行交互。在实际的开发中，我们可以用接口来定义一些规范，然后让我们的View和Model实现它们，并借助Presenter进行交互即可。\n\n为了说明MVP设计模式，我们给出一个示例程序。你可以在[Github](https://github.com/Shouheng88/Android-references)中获取到它的源代码。\n\n### 2.2 示例程序\n\n在该示例中，我们使用了：\n\n1. 开眼视频的API作为数据源；\n2. Retrofit进行数据访问；\n3. 使用ARouter进行路由；\n4. 使用MVP设计模式作为程序架构。\n\n下面是该模块的基本的包结构：\n\n![包结构](res/mvp_package.png)\n\n这里核心的代码是MVP部分。\n\n这里我们首先定义了MVP模式中的最顶层的View和Presenter，在这里分别是`BaseView`和`BasePresenter`，它们在该项目中是两个空的接口，在一些项目中，我们可以根据自己的需求在这两个接口中添加自己需要的方法。\n\n然后，我们定义了`HomeContract`。它是一个抽象的接口，相当于一层协议，用来规定指定的功能的View和Presenter分别应该具有哪些方法。通常，对于不同的功能，我们需要分别实现一个MVP，每个MVP都会又一个对应的Contract。笔者认为它的好处在于，将指定的View和Presenter的接口定义在一个接口中，更加集中。它们各自需要实现的方法也一目了然地展现在了我们面前。\n\n这里根据我们的业务场景，该接口的定义如下：\n\n```java\npublic interface HomeContract {\n\n    interface IView extends BaseView {\n        void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists);\n        void setNextPage(List<HomeBean.IssueList.ItemList> itemLists);\n        void onError(String msg);\n    }\n\n    interface IPresenter extends BasePresenter {\n        void requestFirstPage();\n        void requestNextPage();\n    }\n}\n```\n\n`HomeContract`用来规定View和Presenter应该具有的操作，在这里它用来指定主页的View和Presenter的方法。从上面我们也可以看出，这里的`IView`和`IPresenter`分别实现了`BaseView`和`BasePresenter`。\n\n上面，我们定义了V和P的规范，MVP中还有一项Model，它用来从网络中获取数据。这里我们省去网络相关的具体的代码，你只需要知道`APIRetrofit.getEyepetizerService()`是用来获取Retrofit对应的Service，而`getMoreHomeData()`和`getFirstHomeData()`是用来从指定的接口中获取数据就行。下面是`HomeModel`的定义：\n\n```java\npublic class HomeModel {\n\n    public Observable<HomeBean> getFirstHomeData() {\n        return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis());\n    }\n\n    public Observable<HomeBean> getMoreHomeData(String url) {\n        return APIRetrofit.getEyepetizerService().getMoreHomeData(url);\n    }\n}\n\n```\n\nOK，上面我们已经完成了Model的定义和View及Presenter的规范的定义。下面，我们就需要具体去实现View和Presenter。\n\n首先是Presenter，下面是我们的`HomePresenter`的定义。在下面的代码中，为了更加清晰地展示其中的逻辑，我删减了一部分无关代码：\n\n```java\npublic class HomePresenter implements HomeContract.IPresenter {\n\n    private HomeContract.IView view;\n\n    private HomeModel homeModel;\n\n    private String nextPageUrl;\n\n    // 传入View并实例化Model\n    public HomePresenter(HomeContract.IView view) {\n        this.view = view;\n        homeModel = new HomeModel();\n    }\n\n    // 使用Model请求数据，并在得到请求结果的时候调用View的方法进行回调\n    @Override\n    public void requestFirstPage() {\n        Disposable disposable = homeModel.getFirstHomeData()\n                // ....\n                .subscribe(itemLists -> { view.setFirstPage(itemLists); },\n                        throwable -> { view.onError(throwable.toString()); });\n    }\n\n    // 使用Model请求数据，并在得到请求结果的时候调用View的方法进行回调\n    @Override\n    public void requestNextPage() {\n        Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)\n                // ....\n                .subscribe(itemLists -> { view.setFirstPage(itemLists); },\n                        throwable -> { view.onError(throwable.toString()); });\n    }\n}\n\n```\n\n从上面我们可以看出，在Presenter需要将View和Model建立联系。我们需要在初始化的时候传入View，并实例化一个Model。Presenter通过Model获取数据，并在拿到数据的时候，通过View的方法通知给View层。\n\n然后，就是我们的View层的代码，同样，我对代码做了删减：\n\n```java\n@Route(path = BaseConstants.EYEPETIZER_MENU)\npublic class HomeActivity extends CommonActivity<ActivityEyepetizerMenuBinding> implements HomeContract.IView {\n\n    // 实例化Presenter\n    private HomeContract.IPresenter presenter;\n    {\n        presenter = new HomePresenter(this);\n    }\n\n    @Override\n    protected int getLayoutResId() {\n        return R.layout.activity_eyepetizer_menu;\n    }\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        // ...\n        // 使用Presenter请求数据\n        presenter.requestFirstPage();\n        loading = true;\n    }\n\n    private void configList() {\n        // ...\n        getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {\n            @Override\n            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {\n                    // 请求下一页的数据\n                    presenter.requestNextPage();\n                }\n            }\n        });\n    }\n\n    // 当请求到结果的时候在页面上做处理，展示到页面上\n    @Override\n    public void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists) {\n        loading = false;\n        homeAdapter.addData(itemLists);\n    }\n\n    // 当请求到结果的时候在页面上做处理，展示到页面上\n    @Override\n    public void setNextPage(List<HomeBean.IssueList.ItemList> itemLists) {\n        loading = false;\n        homeAdapter.addData(itemLists);\n    }\n\n    @Override\n    public void onError(String msg) {\n        ToastUtils.makeToast(msg);\n    }\n\n    // ...\n}\n```\n\n从上面的代码中我们可以看出实际在View中也要维护一个Presenter的实例。\n当需要请求数据的时候会使用该实例的方法来请求数据，所以，在开发的时候，我们需要根据请求数据的情况，在Presenter中定义接口方法。\n\n实际上，MVP的原理就是View通过Presenter获取数据，获取到数据之后再回调View的方法来展示数据。\n\n### 2.3 MVC 和 MVP 的区别\n\n1. MVC 中是允许 Model 和 View 进行交互的，而MVP中，Model 与 View 之间的交互由Presenter完成；\n2. MVP 模式就是将 P 定义成一个接口，然后在每个触发的事件中调用接口的方法来处理，也就是将逻辑放进了 P 中，需要执行某些操作的时候调用 P 的方法就行了。\n\n### 2.4 MVP的优缺点\n\n优点：\n\n1. 降低耦合度，实现了 Model 和 View 真正的完全分离，可以修改 View 而不影响 Modle；\n2. 模块职责划分明显，层次清晰；\n3. 隐藏数据；\n4. Presenter 可以复用，一个 Presenter 可以用于多个 View，而不需要更改 Presenter 的逻辑；\n5. 利于测试驱动开发，以前的Android开发是难以进行单元测试的；\n6. View 可以进行组件化，在MVP当中，View 不依赖 Model。\n\n缺点：\n\n1. Presenter 中除了应用逻辑以外，还有大量的 View->Model，Model->View 的手动同步逻辑，造成 Presenter 比较笨重，维护起来会比较困难；\n2. 由于对视图的渲染放在了 Presenter 中，所以视图和 Presenter 的交互会过于频繁；\n3. 如果 Presenter 过多地渲染了视图，往往会使得它与特定的视图的联系过于紧密，一旦视图需要变更，那么Presenter也需要变更了。\n\n## 3、MVVM (分手大师)\n\n### 3.1 基础概念\n\nMVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVC 的改进版。MVVM 就是将其中的 View 的状态和行为抽象化，让我们将视图 UI 和业务逻辑分开。\n\n- 模型层 (Model)：负责从各种数据源中获取数据；\n- 视图层 (View)：在 Android 中对应于 Activity 和 Fragment，用于展示给用户和处理用户交互，会驱动 ViewModel 从 Model 中获取数据；\n- ViewModel 层：用于将 Model 和 View 进行关联，我们可以在 View 中通过 ViewModel 从 Model 中获取数据；当获取到了数据之后，会通过自动绑定，比如 DataBinding，来将结果自动刷新到界面上。\n\n使用 Google 官方的 Android Architecture Components ，我们可以很容易地将 MVVM 应用到我们的应用中。下面，我们就使用它来展示一下 MVVM 的实际的应用。你可以在[Github](https://github.com/Shouheng88/Android-references)中获取到它的源代码。\n\n### 3.2 示例程序\n\n在该项目中，我们使用了：\n\n1. 果壳网的 API 作为数据源；\n2. 使用 Retrofit 进行网络数据访问；\n3. 使用 ViewMdeol 作为整体的架构设计。\n\n该项目的包结构如下图所示：\n\n![mvvm](res/mvvm_package.png)\n\n这里的`model.data`下面的类是对应于网络的数据实体的，由JSON自动生成，这里我们不进行详细描述。这里的`model.repository`下面的两个类是用来从网络中获取数据信息的，我们也忽略它的定义。\n\n上面就是我们的 Model 的定义，并没有太多的内容，基本与 MVP 一致。\n\n下面的是 ViewModel 的代码，我们选择了其中的一个方法来进行说明。当我们定义 ViewModel 的时候，需要继承 ViewModel 类。\n\n```java\npublic class GuokrViewModel extends ViewModel {\n\n    public LiveData<Resource<GuokrNews>> getGuokrNews(int offset, int limit) {\n        MutableLiveData<Resource<GuokrNews>> result = new MutableLiveData<>();\n        GuokrRetrofit.getGuokrService().getNews(offset, limit)\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Observer<GuokrNews>() {\n                    @Override\n                    public void onError(Throwable e) {\n                        result.setValue(Resource.error(e.getMessage(), null));\n                    }\n\n                    @Override\n                    public void onComplete() { }\n\n                    @Override\n                    public void onSubscribe(Disposable d) { }\n\n                    @Override\n                    public void onNext(GuokrNews guokrNews) {\n                        result.setValue(Resource.success(guokrNews));\n                    }\n                });\n        return result;\n    }\n}\n```\n\n这里的 ViewModel 来自 `android.arch.lifecycle.ViewModel`，所以，为了使用它，我们还需要加入下面的依赖：\n\n    api \"android.arch.lifecycle:runtime:$archVersion\"\n    api \"android.arch.lifecycle:extensions:$archVersion\"\n    annotationProcessor \"android.arch.lifecycle:compiler:$archVersion\"\n\n在 ViewModel 的定义中，我们直接使用 Retrofit 来从网络中获取数据。然后当获取到数据的时候，我们使用 LiveData 的方法把数据封装成一个对象返回给 View 层。在 View 层，我们只需要调用该方法，并对返回的 LiveData 进行\"监听\"即可。这里，我们将错误信息和返回的数据信息进行了封装，并且封装了一个代表当前状态的枚举信息，你可以参考源代码来详细了解下这些内容。\n\n上面我们定义完了 Model 和 ViewModel，下面我们看下 View 层的定义，以及在 View 层中该如何使用 ViewModel。\n\n```java\n@Route(path = BaseConstants.GUOKR_NEWS_LIST)\npublic class NewsListFragment extends CommonFragment<FragmentNewsListBinding> {\n\n    private GuokrViewModel guokrViewModel;\n\n    private int offset = 0;\n\n    private final int limit = 20;\n\n    private GuokrNewsAdapter adapter;\n\n    @Override\n    protected int getLayoutResId() {\n        return R.layout.fragment_news_list;\n    }\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        // ...\n\n        guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);\n\n        fetchNews();\n    }\n\n    private void fetchNews() {\n        guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {\n            if (guokrNewsResource == null) {\n                return;\n            }\n            switch (guokrNewsResource.status) {\n                case FAILED:\n                    ToastUtils.makeToast(guokrNewsResource.message);\n                    break;\n                case SUCCESS:\n                    adapter.addData(guokrNewsResource.data.getResult());\n                    adapter.notifyDataSetChanged();\n                    break;\n            }\n        });\n    }\n}\n```\n\n以上就是我们的 View 层的定义，这里我们先使用了 \n\n这里的`view.fragment`包下面的类对应于实际的页面，这里我们 `ViewModelProviders` 的方法来获取我们需要使用的 ViewModel，然后，我们直接使用该 ViewModel 的方法获取数据，并对返回的结果进行“监听”即可。\n\n以上就是 MVVM 的基本使用，当然，这里我们并没有使用 DataBinding 直接与返回的列表信息进行绑定，它被更多的用在了整个 Fragment 的布局中。\n\n### 3.3 MVVM 的优点和缺点\n\nMVVM模式和MVC模式一样，主要目的是分离视图（View）和模型（Model），有几大优点：\n\n1. **低耦合**：视图（View）可以独立于Model变化和修改，一个 ViewModel 可以绑定到不同的 View 上，当 View 变化的时候 Model 可以不变，当 Model 变化的时候 View 也可以不变。\n2. **可重用性**：你可以把一些视图逻辑放在一个 ViewModel 里面，让很多 view 重用这段视图逻辑。\n3. **独立开发**：开发人员可以专注于业务逻辑和数据的开发（ViewModel），设计人员可以专注于页面设计。\n4. **可测试**：界面素来是比较难于测试的，而现在测试可以针对 ViewModel 来写。\n\n## 4、组件化\n\n### 4.1 基础概念\n\n所谓的**组件化**，通俗理解就是将一个工程分成各个模块，各个模块之间相互解耦，可以独立开发并编译成一个独立的 APP 进行调试，然后又可以将各个模块组合起来整体构成一个完整的 APP。它的好处是当工程比较大的时候，便于各个开发者之间分工协作、同步开发；被分割出来的模块又可以在项目之间共享，从而达到复用的目的。组件化有诸多好处，尤其适用于比较大型的项目。\n\n简单了解了组件化之后，让我们来看一下如何实现组件化开发。你可能之前听说过组件化开发，或者被其高大上的称谓吓到了，但它实际应用起来并不复杂，至少借助了现成的框架之后并不复杂。这里我们先梳理一下，在应用组件化的时候需要解决哪些问题：\n\n1. 如何分成各个模块？我们可以根据业务来进行拆分，对于比较大的功能模块可以作为应用的一个模块来使用，但是也应该注意，划分出来的模块不要过多，否则可能会降低编译的速度并且增加维护的难度。\n2. 各个模块之间如何进行数据共享和数据通信？我们可以把需要共享的数据划分成一个单独的模块来放置公共数据。各个模块之间的数据通信，我们可以使用阿里的 ARouter 进行页面的跳转，使用封装之后的 RxJava 作为 EventBus 进行全局的数据通信。\n3. 如何将各个模块打包成一个独立的 APP 进行调试？首先这个要建立在2的基础上，然后，我们可以在各个模块的 gradle 文件里面配置需要加载的 AndroidManifest.xml 文件，并可以为每个应用配置一个独立的 Application 和启动类。\n4. 如何防止资源名冲突问题？遵守命名规约就能规避资源名冲突问题。\n5. 如何解决 library 重复依赖以及 sdk 和依赖的第三方版本号控制问题？可以将各个模块公用的依赖的版本配置到 settings.gradle 里面，并且可以建立一个公共的模块来配置所需要的各种依赖。\n\nTalk is cheap，下面让我们动手实践来应用组件化进行开发。你可以在[Github](https://github.com/Shouheng88/Android-references)中获取到它的源代码。\n\n### 4.2 组件化实践\n\n#### 包结构\n\n首先，我们先来看整个应用的包的结构。如下图所示，该模块的划分是根据各个模块的功能来决定的。图的右侧白色的部分是各个模块的文件路径，我推荐使用这种方式，而不是将各个模块放置在 app 下面，因为这样看起来更加的清晰。为了达到这个目的，你只需要按照下面的方式在 settings.gralde 里面配置一下各个模块的路径即可。注意在实际应用的时候模块的路径的关系，不要搞错了。\n\n![组件化](res/Component.png)\n\n然后，我们介绍一下这里的 commons 模块。它用来存放公共的资源和一些依赖，这里我们将两者放在了一个模块中以减少模块的数量。下面是它的 gradle 的部分配置。这里我们使用了 api 来引入各个依赖，以便在其他的模块中也能使用这些依赖。\n\n```groovy\ndependencies {\n    api fileTree(include: ['*.jar'], dir: 'libs')\n    // ...\n    // router\n    api 'com.alibaba:arouter-api:1.3.1'\n    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'\n    // walle\n    api 'com.meituan.android.walle:library:1.1.6'\n    // umeng\n    api 'com.umeng.sdk:common:1.5.3'\n    api 'com.umeng.sdk:analytics:7.5.3'\n    api files('libs/pldroid-player-1.5.0.jar')\n}\n```\n\n#### 路由\n\n接着，我们来看一下路由框架的配置。这里，我们使用阿里的 ARouter 来进行页面之间的跳转，你可以在[Github](https://github.com/alibaba/ARouter)上面了解该框架的配置和使用方式。这里我们只讲解一下在组件化开发的时候需要注意的地方。注意到 ARouter 是通过注解来进行页面配置的，并且它的注解是在编译的时候进行处理的。所以，我们需要引入`arouter-compiler`来使用它的编译时处理功能。需要注意的地方是，我们只要在公共的模块中加入`arouter-api`就可以使用ARouter的API了，但是需要在每个模块中引入`arouter-compiler`才能使用编译时注解。也就是说，我们需要在每个模块中都加入`arouter-compiler`依赖。\n\n#### 模块独立\n\n为了能够将各个模块编译成一个独立的 APP，我们需要在 Gradle 里面做一些配置。\n\n首先，我们需要在`gradle.properties`定义一些布尔类型的变量用来判断各个模块是作为一个 library 还是 application 进行编译。这里我的配置如下面的代码所示。也就是，我为每个模块都定义了这么一个布尔类型的变量，当然，你也可以只定义一个变量，然后在各个模块中使用同一个变量来进行判断。\n\n```groovy\nisGuokrModuleApp=false\nisLiveModuleApp=false\nisLayoutModuleApp=false\nisLibraryModuleApp=false\nisEyepetizerModuleApp=false\n```\n\n然后，我们来看一下各个模块中的 gradle 该如何配置，这里我们以开眼视频的功能模块作为例子来进行讲解。首先，一个模块作为 library 还是 application 是根据引用的 plugin 来决定的，所以，我们要根据之前定义的布尔变量来决定使用的 plugin：\n\n```groovy\nif (isEyepetizerModuleApp.toBoolean()) {\n    apply plugin: 'com.android.application'\n} else {\n    apply plugin: 'com.android.library'\n}\n```\n\n假如我们要将某个模块作为一个独立的 APP，那么启动类你肯定需要配置。这就意味着你需要两个 AndroidManifest.xml 文件，一个用于 library 状态，一个用于 application 状态。所以，我们可以在 main 目录下面再定义一个 AndroidManifest.xml，然后，我们在该配置文件中不只指定启动类，还使用我们定义的 Application。指定 Application 有时候是必须的，比如你需要在各个模块里面初始化 ARouter 等等。这部分代码就不给出了，可以参考源码，这里我们给出一下在 Gradle 里面指定 AndroidManifest.xml 的方式。\n\n如下所示，我们可以根据之前定义的布尔值来决定使用哪一个配置文件：\n\n```groovy\n    sourceSets {\n        main {\n            jniLibs.srcDirs = ['libs']\n            if (isEyepetizerModuleApp.toBoolean()) {\n                manifest.srcFile \"src/main/debug/AndroidManifest.xml\"\n            } else {\n                manifest.srcFile \"src/main/AndroidManifest.xml\"\n            }\n        }\n    }\n```\n\n此外，还需要注意的是，如果我们希望在每个模块中都能应用 DataBinding 和 Java 8 的一些特性，那么你需要在每个模块里面都加入下面的配置：\n\n```gradle\n    // use data binding\n    dataBinding {\n        enabled = true\n    }\n    // use java 8 language\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n```\n\n对于编译时注解之类的配置，我们也需要在每个模块里面都进行声明。\n\n完成了以上的配置，我们只要根据需要编译的类型，修改之前定义的布尔值，来决定是将该模块编译成 APP 还是作为类库来使用即可。\n\n以上就是组件化在 Android 开发当中的应用。\n\n## 总结\n\nMVC、MVP和MVVM各有各自的特点，可以根据应用开发的需要选择适合自己的架构模式。组件化的目的就在于保持各个模块之间的独立从而便于分工协作。它们之间的关系就是，你可以在组件化的各个模块中应用前面三种架构模式的一种或者几种。\n\n"
  },
  {
    "path": "高阶技术/注解在Android中的应用.md",
    "content": "# Java 注解及其两种使用方法\n\n一般的，注解在 Android 中有两种应用方式，一种方式是基于反射的，即在程序的运行期间获取类信息进行反射调用；另一种是使用注解处理，在编译期间生成许多代码，然后在运行期间通过调用这些代码来实现目标功能。\n\n在本篇文章中，我们会先重温一下 Java 的注解相关的知识，然后分别介绍一下上面两种方式的实际应用。\n\n## 1、Java 注解回顾\n\n### 1. Java 注解的基础知识\n\nJava 中的注解分成标准注解和元注解。标准注解是 Java 为我们提供的预定义的注解，共有四种：`@Override`、`@Deprecated`、`@SuppressWarnnings` 和 `@SafeVarags`。元注解是用来提供给用户自定义注解用的，共有五种（截止到Java8）：`@Target`、`@Retention`、`@Documented`、`@Inherited` 和 `@Repeatable`，这里我们重点介绍这五种元注解。\n\n不过，首先我们还是先看一下一个基本的注解的定义的规范。下面我们自定义了一个名为`UseCase`的注解，可以看出我们用到了上面提及的几种元注解：\n\n```java\n    @Documented\n    @Retention(RetentionPolicy.RUNTIME)\n    @Target(value={METHOD, FIELD})\n    public @interface UseCase {\n        public int id();\n        public String description() default \"default value\";\n    }\n```\n\n这是一个普通的注解的定义。从上面我们也可以总结出，在定义注解的时候，有以下几个地方需要注意：\n\n1. 使用 `@interface` 声明并且指定注解的名称；\n2. 注解的定义类似于接口中的方法的定义，但要注意两者之间本质上是不同的；\n3. 可以通过 `default` 为指定的元素指定一个默认值，如果用户没有为其指定值，就使用默认值。\n\n### 2. 元注解\n\n好的，看完了一个基本的注解的定义，我们来看一下上面用到的 Java 元注解的含义。\n\n#### @Target\n\n`@Target` 用来指定注解能够修饰的对象的类型。因为 `@Target` 本身也是一个注解，所以你可以在源码中查看它的定义。该注解接收的参数是一个 `ElementType` 类型的数组，所以，就是说我们自定义的注解可以应用到多种类型的对象，而对象的类型由 `ElementType` 定义。`ElementType` 是一个枚举，它的枚举值如下：\n\n- TYPE：类、接口或者enum声明\n- FIELD：域声明，包括enum实例\n- METHOD：方法声明\n- PARAMETER：参数声明\n- CONSTRUCTOR：构造器声明\n- LOCAL_VARIABLE：局部变量声明\n- ANNOTATION_TYPE：注解声明\n- PACKAGE：包声明\n- TYPE_PARAMETER：类型参数声明\n- TYPE_USE：使用类型\n\n所以，比如根据上面的内容，我们可以直到我们的自定义注解 `@UseCase` 只能应用于方法和字段。\n\n#### @Retention\n\n用来指定注解的保留策略，比如有一些注解，当你在自己的代码中使用它们的时候你会把它写在方法上面，但是当你反编译之后却发现这些注解不在了；而有些注解反编译之后依然存在，发生这种情况的原因就是在使用该注解的时候指定了不同的参数。\n\n与 `@Target` 相同的是这个注解也使用枚举来指定值的类型，不同的是它只能指定一个值，具体可以看源码。这里它使用的是 `RetentionPolicy` 枚举，它的几个值的含义如下：\n\n- SOURCE：注解将被编译器丢弃\n- CLASS：注解在class文件中使用，但会被JVM丢弃\n- RUNTIME：VM将在运行期保留注解，故可以通过反射读取注解的信息\n\n当我们在 Android 中使用注解的时候，一种是在运行时使用的，所以我们要用 `RUNTIME`；另一种是在编译时使用的，所以我们用 `CLASS`。\n\n#### @Documented、@Inherited 和 @Repeatable\n\n这三个元注解的功能比较简单和容易理解，这里我们一起给出即可：\n\n- `@Documented` 表示此注解将包含在 javadoc 中；\n- `@Inherited` 表示允许子类继承父类的注解；\n- `@Repeatable` 是 Java8 中新增的注解，表示指定的注解可以重复应用到指定的对象上面。\n\n上文，我们回顾了 Java 中注解相关的知识点，相信你已经对注解的内容有了一些了解，那么我们接下来看一下注解在实际开发中的两种应用方式。\n\n## 2、注解的两种使用方式\n\n在我开始为我的开源项目 [马克笔记](https://github.com/Shouheng88/MarkNote) 编写数据库的时候，我考虑了使用注解来为数据库对象指定字段的信息，并根据这心信息来拼接出创建数据库表的 SQL 语句。当时也想用反射来动态为每个字段赋值的，但是考虑到反射的性能比较差，最终放弃了这个方案。但是，使用注解处理的方式可以完美的解决我们的问题，即在编译的时候动态生成一堆代码，实际赋值的时候调用这些方法来完成。这前后两种方案就是我们今天要讲的注解的两种使用方式。\n\n### 2.1 基于反射使用注解\n\n这里为了演示基于反射的注解的使用方式，我们写一个小的 Java 程序，要实现的目的是：定义两个个注解，一个应用于方法，一个应用于字段，然后我们使用这两个注解来定义一个类。我们想要在代码中动态地打印出使用了注解的方法和字段的信息和注解信息。\n\n这里我们先定义两个注解，应用于字段的 `@Column` 注解和应用于方法 `@Important` 注解：\n\n```java\n    @Target(value = {ElementType.FIELD})\n    @Retention(RetentionPolicy.RUNTIME)\n    public @interface Column {\n        String name();\n    }\n\n    @Target(value = {ElementType.METHOD})\n    @Retention(RetentionPolicy.RUNTIME)\n    public @interface WrappedMethod {\n        // empty\n    }\n```\n\n然后我们定义了一个Person类，并使用注解为其中的部分方法和字段添加注解：\n\n```java\n    private static class Person {\n\n        @Column(name = \"id\")\n        private int id;\n\n        @Column(name = \"first_name\")\n        private String firstName;\n\n        @Column(name = \"last_name\")\n        private String lastName;\n\n        private int temp;\n\n        @WrappedMethod()\n        public String getInfo() {\n            return id + \" :\" + firstName + \" \" + lastName;\n        }\n\n        public String method() {\n            return \"Nothing\";\n        }\n    }\n```\n\n然后，我们使用Person类来获取该类的字段和方法的信息，并输出具有注解的部分：\n\n```java\n    public static void main(String...args) {\n        Class<?> c = Person.class;\n        Method[] methods = c.getDeclaredMethods();\n        for (Method method : methods) {\n            if (method.getAnnotation(WrappedMethod.class) != null) {\n                System.out.print(method.getName() + \" \");\n            }\n        }\n        System.out.println();\n        Field[] fields = c.getDeclaredFields();\n        for (Field field : fields) {\n            Column column = field.getAnnotation(Column.class);\n            if (column != null) {\n                System.out.print(column.name() + \"-\" + field.getName() + \", \");\n            }\n        }\n    }\n```\n\n输出结果：\n\n    getInfo\n    id-id, first_name-firstName, last_name-lastName, \n\n在上面的代码的执行结果，我们可以看出：使用了注解和反射之后，我们成功的打印出了使用了注解的字段。这里我们需要先获取指定的类的 Class 类型，然后用反射获取它的所有方法和字段信息并进行遍历，通过判断它们的 `getAnnotation()` 方法的结果来确定这个方法和字段是否使用了指定类型的注解。\n\n上面的代码可以解决一些问题，但同时，我们还有一些地方需要注意：\n\n1. **如果指定的方法或者字段名被混淆了怎么办？** 对于一些可以自定义名称的情景，我们可以在注解中加入参数为该字段或者方法指定一个名称；\n2. **上面使用了很多的反射，这会影响程序的性能吗？** 使用注解的方式肯定性能不会高，但是如果注解的使用没有那么频繁，上面方法不会有特别大的性能损耗，比如拼接 SQL 这样的操作，可能只需要执行一次。不过，根本的解决办法是使用注解的第二种使用方式！\n\n### 2.2 基于 annotationProcessor 使用注解\n\n也许你之前已经使用过 ButterKnife 这样的注入框架，不知道你是否记得在 Gradle 中引用它的时候加入了下面这行依赖：\n\n```groovy\n    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'\n```\n\n这里的 annotationProcessor 就是我们这里要讲的**注解处理**。本质上它会在编译的时候，在你调用 `ButterKnife.bind(this);` 方法的那个类所在的包下面生成一些类，当调用 `ButterKnife.bind(this);` 的时候实际上就完成了为使用注解的方法和控件绑定的过程。也就是，本质上还是调用了 `findViewById()`，只是这个过程被隐藏了，不用你来完成了，仅此而已。\n\n下面，我们就使用注解处理的功能来制作一个类似于 ButterKnife 的简单库。不过，在那之前我们还需要做一些准备——一些知识点需要进行说明。即 [Javapoet](https://github.com/square/javapoet)和`AbstractProcessor`。\n\n#### Javapoet & AbstractProcessor\n\nJavapoet 是一个用来生成 `.java` 文件的 Java API，由 Square 开发，你可以在它的 Github 主页中了解它的基本使用方法。它的好处就是对方法、类文件和代码等的拼接进行了封装，有了它，我们就不用再按照字符串的方式去拼接出一段代码了。相比于直接使用字符串的方式，它还可以生成代码的同时直接 `import` 对应的引用，可以说是非常方便、快捷的一个库了。\n\n这里的 `AbstractProcessor` 是用来生成类文件的核心类，它是一个抽象类，一般使用的时候我们只要覆写它的方法中的4个就可以了。下面是这些方法及其定义：\n\n1. `init`：在生成代码之前被调用，可以从它参数 `ProcessingEnvironment` 获取到非常多有用的工具类；\n2. `process`：用于生成代码的 Java 方法，可以从参数 `RoundEnvironment` 中获取使用指定的注解的对象的信息，并包装成一个 `Element` 类型返回；\n3. `getSupportedAnnotationTypes`：用于指定该处理器适用的注解；\n4. `getSupportedSourceVersion`：用来指定你使用的 Java 的版本。\n\n这几个方法中，除了 `process`，其他方法都不是必须覆写的方法。这里的 `getSupportedAnnotationTypes` 和 `getSupportedSourceVersion` 可以使用注 `@SupportedAnnotationTypes` 和 `@SupportedSourceVersion` 来替换，但是不建议这么做。因为前面的注解接收的参数是字符串，如果你使用了混淆可能就比较麻烦，后面的注解只能使用枚举，相对欠缺了灵活性。\n\n另一个我们需要特别说明的地方是，继承 `AbstractProcessor` 并实现了我们自己的处理器之后还要对它进行注册才能使用。一种做法是在与 `java` 同的目录下面创建一个 `resources` 文件夹，并在其中创建 `META-INF/service` 文件夹，然后在其中创建一个名为`javax.annotation.processing.Processor` 的文件，并在其中写上我们的处理器的完整路径。另一种做法是使用谷歌的 `@AutoService` 注解，你只需要在自己的处理器上面加上 `@AutoService(Processor.class)` 一行代码即可。当然，前提是你需要在自己的项目中引入依赖：\n\n```groovy\n    compile 'com.google.auto.service:auto-service:1.0-rc2'\n```\n\n按照后面的这种方式一样会在目录下面生成上面的那个文件，只是这个过程不需要我们来操作了。你可以通过查看buidl出的文件来找到生成的文件。\n\n#### MyKnife 的最终结果\n\n在定制之前，我们先看一下程序的最终执行结果，也许这样会更有助于理解整个过程的原理。我们程序的最终的执行结果是，在编译的时候，在使用我们的工具的类的相同级别的包下面生成一个类。如下图所示：\n\n![程序的执行结果](https://user-gold-cdn.xitu.io/2018/8/26/16574fbbf6dcb617?w=419&h=393&f=png&s=40752)\n\n这里的 `me.shouheng.libraries` 是我们应用 MyKnife 的包，这里我们在它下面生成了一个名为 `MyKnifeActivity$$Injector` 的类，它的定义如下：\n\n```java\n    public class MyKnifeActivity$$Injector implements Injector<MyKnifeActivity> {\n      @Override\n      public void inject(final MyKnifeActivity host, Object source, Finder finder) {\n        host.textView=(TextView)finder.findView(source, 2131230952);\n        View.OnClickListener listener;\n        listener = new View.OnClickListener() {\n          @Override\n          public void onClick(View view) {\n            host.OnClick();\n          }\n        };\n        finder.findView(source, 2131230762).setOnClickListener(listener);\n      }\n    }\n```\n\n因为我们应用 `MyKnife` 的类是 `MyKnifeActivity`，所以这里就生成了名为 `MyKnifeActivity$$Injector` 的类。通过上面的代码，可以看出它实际上调用了 `Finder` 的方法来为我们的控件 `textView` 赋值，然后使用控件的 `setOnClickListener()` 方法为点击事件赋值。这里的 `Finder` 是我们封装的一个对象，用来从指定的源中获取控件的类，本质上还是调用了指定源的 `findViewById()` 方法。\n\n然后，与 ButterKnife 类似的是，在使用我们的工具的时候，也需要在 Activity 的 `onCreate()` 中调用 `bind()` 方法。这里我们看下这个方法做了什么操作：\n\n```java\n    public static void bind(Object host, Object source, Finder finder) {\n        String className = host.getClass().getName();\n        try {\n            Injector injector = FINDER_MAPPER.get(className);\n            if (injector == null) {\n                Class<?> finderClass = Class.forName(className + \"$$Injector\");\n                injector = (Injector) finderClass.newInstance();\n                FINDER_MAPPER.put(className, injector);\n            }\n            injector.inject(host, source, finder);\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n        }\n    }\n```\n\n从上面的代码中可以看出，调用 `bind()` 方法的时候会从 `FINDER_MAPPER` 尝试获取指定 `类名$$Injector` 的文件。所以，如果说我们应用 `bind() `的类是 `MyKnifeActivity`，那么这里获取到的类将会是 `MyKnifeActivity$$Injector`。然后，当我们调用 `inject` 方法的时候就执行了我们上面的注入操作，来完成对控件和点击事件的赋值。这里的 `FINDER_MAPPER` 是一个哈希表，用来缓存指定的 `Injector` 的。所以，从上面也可以看出，这里进行值绑定的时候使用了反射，所以，在应用框架的时候还需要对混淆进行处理。\n\nOK，看完了程序的最终结果，我们来看一下如何生成上面的那个类文件。\n\n#### API 和注解的定义\n\n首先，我们需要定义注解用来提供给用户进行事件和控件的绑定，\n\n```java\n    @Target(ElementType.FIELD)\n    @Retention(RetentionPolicy.CLASS)\n    public @interface BindView {\n        int id();\n    }\n\n    @Target(ElementType.METHOD)\n    @Retention(RetentionPolicy.CLASS)\n    public @interface OnClick {\n        int[] ids();\n    }\n```\n\n如上面的代码所示，可以看出我们分别用了 `ElementType.FIELD` 和 `ElementType.METHOD` 指定它们是应用于字段和方法的，然后用了 `RetentionPolicy.CLASS` 标明它们不会被保留到程序运行时。\n\n然后，我们需要定义 `MyKnife`，它提供了一个 `bind()` 方法，其定义如下：\n\n```java\n    public static void bind(Object host, Object source, Finder finder) {\n        String className = host.getClass().getName();\n        try {\n            Injector injector = FINDER_MAPPER.get(className);\n            if (injector == null) {\n                Class<?> finderClass = Class.forName(className + \"$$Injector\");\n                injector = (Injector) finderClass.newInstance();\n                FINDER_MAPPER.put(className, injector);\n            }\n            injector.inject(host, source, finder);\n        } catch (ClassNotFoundException e) {\n            e.printStackTrace();\n        } catch (IllegalAccessException e) {\n            e.printStackTrace();\n        } catch (InstantiationException e) {\n            e.printStackTrace();\n        }\n    }\n```\n\n这里的三个参数的含义分别是：`host` 是调用绑定方法的类，比如 Activity 等；`source`是从用来获取绑定的值的数据源，一般理解是从 `source` 中获取控件赋值给 `host` 中的字段，通常两者是相同的；最后一个参数 `finder` 是一个接口，是获取数据的方法的一个封装，有两默认的实现，一个是 `ActivityFinder`，一个是 `ViewFinder`，分别用来从 Activity 和 View 中查找控件。\n\n我们之前已经讲过 `bind()` 方法的作用，即使用反射根据类名来获取一个 `Injector`，然后调用它的 `inject()` 方法进行注入。这里的 `Injector` 是一个接口，我们不会写代码去实现它，而是在编译的时候让编译器直接生成它的实现类。\n\n#### 代码的生成过程\n\n在介绍 Javapoet 和 AbstractProcessor 的时候，我们提到过 Element，它封装了应用注解的对象（方法、字段或者类等）的信息。我们可以从 Element 中获取这些信息并将它们封装成一个对象来方便我们调用。于是就产生了 `BindViewField` 和 `OnClickMethod` 两个类。它们分别用来描述使用 `@BindView` 注解和使用 `@OnClick` 注解的对象的信息。此外，还有一个 `AnnotatedClass`，它用来描述使用注解的整个类的信息，并且其中定义了`List<BindViewField>` 和 `List<OnClickMethod>`，分别用来存储该类中应用注解的字段和方法的信息。\n\n与生成文件和获取注解的对象信息相关的几个字段都是从 AbstractProcessor 中获取的。如下面的代码所示，我们可以从 AbstractProcessor 的 `init()` 方法的 `ProcessingEnvironment` 中获取到 `Elements`、`Filer` 和 `Messager`。它们的作用分别是：`Elements` 类似于一个工具类，用来从 `Element` 中获取注解对象的信息；`Filer` 用来支持通过注释处理器创建新文件；`Messager` 提供注释处理器用来报告错误消息、警告和其他通知的方式。\n\n```java\n    @Override\n    public synchronized void init(ProcessingEnvironment processingEnvironment) {\n        super.init(processingEnvironment);\n        elements = processingEnvironment.getElementUtils();\n        messager = processingEnvironment.getMessager();\n        filer = processingEnvironment.getFiler();\n    }\n```\n\n然后在 AbstractProcessor 的 `process()` 方法中的 `RoundEnvironment` 参数中，我们又可以获取到指定注解对应的 `Element` 信息。代码如下所示：\n\n```java\n    private Map<String, AnnotatedClass> map = new HashMap<>();\n\n    @Override\n    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {\n        map.clear();\n        try {\n            // 分别用来处理我们定义的两种注解\n            processBindView(roundEnvironment);\n            processOnClick(roundEnvironment);\n        } catch (IllegalArgumentException e) {\n            return true;\n        }\n\n        try {\n            // 为缓存的各个使用注解的类生成类文件\n            for (AnnotatedClass annotatedClass : map.values()) {\n                annotatedClass.generateFinder().writeTo(filer);\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return true;\n    }\n\n    // 从RoundEnvironment中获取@BindView注解的信息\n    private void processBindView(RoundEnvironment roundEnv) {\n        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {\n            AnnotatedClass annotatedClass = getAnnotatedClass(element);\n            BindViewField field = new BindViewField(element);\n            annotatedClass.addField(field);\n        }\n    }\n\n    // 从RoundEnvironment中获取@OnClick注解的信息\n    private void processOnClick(RoundEnvironment roundEnv) {\n        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {\n            AnnotatedClass annotatedClass = getAnnotatedClass(element);\n            OnClickMethod method = new OnClickMethod(element);\n            annotatedClass.addMethod(method);\n        }\n    }\n\n    // 获取使用注解的类的信息，先尝试从缓存中获取，缓存中没有的话就实例化一个并放进缓存中\n    private AnnotatedClass getAnnotatedClass(Element element) {\n        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();\n        String fullClassName = encloseElement.getQualifiedName().toString();\n        AnnotatedClass annotatedClass = map.get(fullClassName);\n        if (annotatedClass == null) {\n            annotatedClass = new AnnotatedClass(encloseElement, elements);\n            map.put(fullClassName, annotatedClass);\n        }\n        return annotatedClass;\n    }\n```\n\n上面的代码的逻辑是，在调用 `process()` 方法的时候，会根据传入的 `RoundEnvironment` 分别处理两种注解。两个注解的相关信息都会被解析成 `List<BindViewField>` 和 `List<OnClickMethod>`，然后把使用注解的整个类的信息统一放置在 `AnnotatedClass` 中。为了提升程序的效率，这里用了缓存来存储类信息。最后，我们调用了 `annotatedClass.generateFinder()` 获取一个JavaFile，并调用它的 `writeTo(filer)` 方法生成类文件。\n\n上面的代码重点在于解析使用注解的类的信息，至于如何根据类信息生成类文件，我们还需要看下 `AnnotatedClass` 的 `generateFinder()` 方法，其代码如下所示。这里我们用了之前提到的 Javapoet 来帮助我们生成类文件：\n\n```java\n    public JavaFile generateFinder() {\n        // 这里用来定义inject方法的签名\n        MethodSpec.Builder builder = MethodSpec.methodBuilder(\"inject\")\n                .addModifiers(Modifier.PUBLIC)\n                .addAnnotation(Override.class)\n                .addParameter(TypeName.get(typeElement.asType()), \"host\", Modifier.FINAL)\n                .addParameter(TypeName.OBJECT, \"source\")\n                .addParameter(TypeUtils.FINDER, \"finder\");\n        // 这里用来定义inject方法中@BindView注解的绑定过程\n        for (BindViewField field : bindViewFields) {\n            builder.addStatement(\"host.$N=($T)finder.findView(source, $L)\",\n                    field.getFieldName(),\n                    ClassName.get(field.getFieldType()),\n                    field.getViewId());\n        }\n        // 这里用来定义inject方法中@OnClick注解的绑定过程\n        if (onClickMethods.size() > 0) {\n            builder.addStatement(\"$T listener\", TypeUtils.ONCLICK_LISTENER);\n        }\n        for (OnClickMethod method : onClickMethods) {\n            TypeSpec listener = TypeSpec.anonymousClassBuilder(\"\")\n                    .addSuperinterface(TypeUtils.ONCLICK_LISTENER)\n                    .addMethod(MethodSpec.methodBuilder(\"onClick\")\n                            .addAnnotation(Override.class)\n                            .addModifiers(Modifier.PUBLIC)\n                            .returns(TypeName.VOID)\n                            .addParameter(TypeUtils.ANDROID_VIEW, \"view\")\n                            .addStatement(\"host.$N()\", method.getMethodName())\n                            .build())\n                    .build();\n            builder.addStatement(\"listener = $L\", listener);\n            for (int id : method.getIds()) {\n                builder.addStatement(\"finder.findView(source, $L).setOnClickListener(listener)\", id);\n            }\n        }\n        // 这里用来获取要生成的类所在的包的信息\n        String packageName = getPackageName(typeElement);\n        String className = getClassName(typeElement, packageName);\n        ClassName bindClassName = ClassName.get(packageName, className);\n\n        // 用来最终组装成我们要输出的类\n        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + \"$$Injector\")\n                .addModifiers(Modifier.PUBLIC)\n                .addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType())))\n                .addMethod(builder.build())\n                .build();\n        return JavaFile.builder(packageName, finderClass).build();\n    }\n```\n\n上面就是我们用来最终生成类文件的方法，这里用了 Javapoet ，如果对它不是很了解可以到 Github 上面了解一下它的用法。\n\n这样我们就完成了整个方法的定义。\n\n#### 使用 MyKnife\n\n使用我们定义的 MyKnife ，我们只需要在 Gradle 里面引入我们的包即可：\n\n```groovy\n    implementation project(':knife-api')\n    implementation project(':knife-annotation')\n    annotationProcessor project(':knife-compiler')\n```\n\n也许你在有的地方看到过要使用 `android-apt` 引入注解处理器，其实这里的`annotationProcessor` 与之作用是一样的。这里推荐使用 `annotationProcessor`，因为它更加简洁，不需要额外的配置，也是官方推荐的使用方式。\n\n然后，我们只需要在代码中使用它们就可以了：\n\n```java\npublic class MyKnifeActivity extends CommonActivity<ActivityMyKnifeBinding> {\n\n    @BindView(id = R.id.tv)\n    public TextView textView;\n\n    @OnClick(ids = {R.id.btn})\n    public void OnClick() {\n        ToastUtils.makeToast(\"OnClick\");\n    }\n\n    @Override\n    protected int getLayoutResId() {\n        return R.layout.activity_my_knife;\n    }\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        MyKnife.bind(this);\n        textView.setText(\"This is MyKnife demo!\");\n    }\n}\n```\n\n这里有几个地方需要注意：\n\n1. 使用注解的方法和字段需要至少是 `protected`，因为我们使用了直接引用的方式，而生成的文件和上面的类包相同，所以至少应该保证包级别访问权限；\n2. 上面使用注解的方式只能在当前 Module 作为 application 的时候使用，作为 library 的时候无法使用，这是因为只有当 Module 作为 application 的时候，R文件中的 id 是 final 的，作为 library 的时候是非 final 的。\n\n#### 总结\n\n这里我们总结一下按照第二种方式使用注解的时候需要步骤：\n\n1. 首先，我们需要按照自己的需要考虑如何定义注解。\n2. 然后，我们需要实现 AbstractProcessor ，覆写各个方法，注册，并在 process 方法中完成生成类文件的操作。\n\n## 3、总结\n\n以上就是注解的两种比较常见的使用方式。第一种是通过反射来进行的，因为反射本身的效率比较低，所以比较适用于发射比较少的场景；第二种方式是在编译期间通过编译器生成代码来实现的，相比于第一种，它还是可能会用到反射的，但是不必在运行时对类的每个方法和字段进行遍历，因而效率高得多。\n\n以上。\n\n获取源码：[Android-references](https://github.com/Shouheng88/Android-references)"
  },
  {
    "path": "高阶技术/浅谈LiveData的通知过程.md",
    "content": "# 浅谈 LiveData 的通知机制\n\nLiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了，昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以，就这个机会我们也来分析一下 LiveData 通知的实现过程。\n\n1. 关于 ViewModel 的生命周期：[《浅谈 LiveData 的通知机制》](高阶技术/浅谈ViewModel生命周期控制.md);\n2. 关于 MVVM 设计模式的基本应用，你可以参考这篇文章：[《Android 架构设计：MVC、MVP、MVVM和组件化》](高阶技术/探索Android架构设计.md).\n\n## 1、一个 LiveData 的问题\n\n今天所遇到的问题是这样的，\n\n![LiveData的问题](res/live_data.png)\n\n有两个页面 A 和 B，A 是一个 Fragment ，是一个列表的展示页；B 是其他的页面。首先，A 会更新页面，并且为了防止连续更新，再每次更新之前需要检查一个布尔值，只有为 false 的时候才允许从网络加载数据。每次加载数据之前会将该布尔值置为 true，拿到了结果之后置为 false. 这里拿到的结果是借助 LiveData 来通知给页面进行更新的。\n\n现在，A 打开了 B，B 中对列表中的数据进行了更新，然后发了一条类似于广播的消息。此时，A 接收了消息并进行数据加载。过了一段时间，B 准备退出，再退出的时候又对列表中的项目进行了更新，所以此时又发出了一条消息。\n\nB 关闭了，我们回到了 A 页面。但是，此时，我们发现 A 页面中的数据只包含了第一次的数据更新，第二次的数据更新没有体现在列表中。\n\n用代码来描述的话大致是下面这样，\n\n```java\n    // 类 A\n    public class A extends Fragment {\n    \n        private boolean loading = false;\n\n        private MyViewModel vm;\n\n        // ......\n\n        /**\n         * Register load observer.\n         */\n        public void registerObservers() {\n            vm.getData().observe(this, resources -> {\n                loading = false;\n                // ... show in list\n            })\n        }\n\n        /**\n         * Load data from server.\n         */\n        public void loadData() {\n            if (loading) return;\n            loading = true;\n            vm.load();\n        }\n\n        /**\n         * On receive message.\n         */\n        public void onReceive() {\n            loadData();\n        }\n    }\n\n    public class B extends Activity {\n\n        public void doBusiness1() {\n            sendMessage(MSG); // Send message when on foreground.\n        }\n\n        @Override\n        public void onBackpressed() {\n            // ....\n            sendMessage(MSG); // Send message when back\n        }\n    }\n\n    public class MyViewModel extends ViewModel {\n\n        private MutableLiveData<Resoucres<Object>> data;\n\n        public MutableLiveData<Resoucres<Object>> getData() {\n            if (data == null) {\n                data = new MutableLiveData<>();\n            }\n            return data;\n        }\n\n        public void load() {\n            Object result = AsyncGetData.getData(); // Get data\n            if (data != null) {\n                data.setValue(Resouces.success(result));\n            }\n        }\n    }\n```\n\nA 打开了 B 之后，A 处于后台，B 处于前台。此时，B 调用 `doBusiness1()` 发送了一条消息 MSG，A 中在 `onReceive()` 中收到消息，并调用 `loadData()` 加载数据。然后，B 处理完了业务，准备退出的时候发现其他数据发生了变化，所以又发了一条消息，然后 `onReceive()` 中收到消息，并调用 `loadData()`. 但此时发现 loading 为 true. 所以，我们后来对数据的修改没有体现到列表上面。\n\n## 2、问题的原因\n\n如果用上面的示例代码作为例子，那么出现问题的原因就是当 A 处于后台的时候。虽然调用了 `loadData()` 并且从网络中拿到了数据，但是调用 `data.setValue()` 方法的时候无法通知到 A 中。所以，`loading = false` 这一行无法被调用到。第二次发出通知的时候，一样调用到了 `loadData()`，但是因为此时 `loading` 为 true，所以并没有执行加载数据的操作。而当从 B 中完全回到 A 的时候，第一次加载的数据被 A 接收到。所以，列表中的数据是第一次加载时的数据，第二次加载事件丢失了。\n\n解决这个问题的方法当然比较简单，可以当接收到事件的时候使用布尔变量监听，然后回到页面的时候发现数据发生变化再执行数据加载：\n\n```java\n    // 类 A\n    public class A extends Fragment {\n    \n        private boolean dataChanged;\n\n        /**\n         * On receive message.\n         */\n        public void onReceive() {\n            dataChanged = true;\n        }\n\n        @Override\n        public void onResume() {\n            // ...\n            if (dataChanged) {\n                loadData();\n            }\n        }\n    }\n```\n\n对于上面的问题，当我们调用了 `setValue()` 之后将调用到 LiveData 类的 `setValue()` 方法，\n\n```java\n    @MainThread\n    protected void setValue(T value) {\n        assertMainThread(\"setValue\");\n        mVersion++;\n        mData = value;\n        dispatchingValue(null);\n    }\n```\n\n这里表明该方法必须在主线程中被调用，最终事件的分发将会交给 `dispatchingValue()` 方法来执行：\n\n```java\n    private void dispatchingValue(@Nullable ObserverWrapper initiator) {\n        if (mDispatchingValue) {\n            mDispatchInvalidated = true;\n            return;\n        }\n        mDispatchingValue = true;\n        do {\n            mDispatchInvalidated = false;\n            if (initiator != null) {\n                considerNotify(initiator);\n                initiator = null;\n            } else {\n                for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =\n                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {\n                    // 发送事件\n                    considerNotify(iterator.next().getValue());\n                    if (mDispatchInvalidated) {\n                        break;\n                    }\n                }\n            }\n        } while (mDispatchInvalidated);\n        mDispatchingValue = false;\n    }\n```\n\n然后，会调用 `considerNotify()` 方法来最终将事件传递出去，\n\n```java\n    private void considerNotify(ObserverWrapper observer) {\n        // 这里会因为当前的 Fragment 没有处于 active 状态而退出方法\n        if (!observer.mActive) {\n            return;\n        }\n        if (!observer.shouldBeActive()) {\n            observer.activeStateChanged(false);\n            return;\n        }\n        if (observer.mLastVersion >= mVersion) {\n            return;\n        }\n        observer.mLastVersion = mVersion;\n        observer.mObserver.onChanged((T) mData);\n    }\n```\n\n这里会因为当前的 Fragment 没有处于 active 状态而退出 `considerNotify()` 方法，从而消息无法被传递出去。\n\n## 3、LiveData 的通知机制\n\nLiveData 的通知机制并不复杂，它的类主要包含在 `livedata-core` 包下面，总共也就 3 个类。LiveData 是一个抽象类，它有一个默认的实现就是 MutableLiveData. \n\nLiveData 主要依靠内部的变量 `mObservers` 来缓存订阅的对象和订阅信息。其定义如下，使用了一个哈希表进行缓存和映射，\n\n```java\nprivate SafeIterableMap<Observer<T>, ObserverWrapper> mObservers = new SafeIterableMap<>();\n```\n\n每当我们调用一次 `observe()` 方法的时候就会有一个映射关系被加入到哈希表中，\n\n```java\n    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {\n        if (owner.getLifecycle().getCurrentState() == DESTROYED) {\n            // 持有者当前处于被销毁状态，因此可以忽略此次观察\n            return;\n        }\n        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);\n        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);\n        if (existing != null && !existing.isAttachedTo(owner)) {\n            throw new IllegalArgumentException(\"Cannot add the same observer\"\n                    + \" with different lifecycles\");\n        }\n        if (existing != null) {\n            return;\n        }\n        owner.getLifecycle().addObserver(wrapper);\n    }\n```\n\n从上面的代码我们可以看出，添加到映射关系中的类会先被包装成 `LifecycleBoundObserver` 对象。然后使用该对象对 owner 的生命周期进行监听。\n\n这的 `LifecycleBoundObserver` 和 `ObserverWrapper` 两个类的定义如下，\n\n```java\n    class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {\n        @NonNull final LifecycleOwner mOwner;\n\n        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<T> observer) {\n            super(observer);\n            mOwner = owner;\n        }\n\n        @Override\n        boolean shouldBeActive() {\n            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);\n        }\n\n        @Override\n        public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {\n            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {\n                removeObserver(mObserver);\n                return;\n            }\n            activeStateChanged(shouldBeActive());\n        }\n\n        @Override\n        boolean isAttachedTo(LifecycleOwner owner) {\n            return mOwner == owner;\n        }\n\n        @Override\n        void detachObserver() {\n            mOwner.getLifecycle().removeObserver(this);\n        }\n    }\n\n    private abstract class ObserverWrapper {\n        final Observer<T> mObserver;\n        boolean mActive;\n        int mLastVersion = START_VERSION;\n\n        ObserverWrapper(Observer<T> observer) {\n            mObserver = observer;\n        }\n\n        abstract boolean shouldBeActive();\n\n        boolean isAttachedTo(LifecycleOwner owner) {\n            return false;\n        }\n\n        void detachObserver() {}\n\n        void activeStateChanged(boolean newActive) {\n            if (newActive == mActive) {\n                return;\n            }\n            mActive = newActive;\n            boolean wasInactive = LiveData.this.mActiveCount == 0;\n            LiveData.this.mActiveCount += mActive ? 1 : -1;\n            if (wasInactive && mActive) {\n                onActive();\n            }\n            if (LiveData.this.mActiveCount == 0 && !mActive) {\n                onInactive();\n            }\n            if (mActive) {\n                dispatchingValue(this);\n            }\n        }\n    }\n```\n\n上面的类中我们先来关注 `LifecycleBoundObserver` 中的 `onStateChanged()` 方法。该方法继承自 `LifecycleObserver`. 这里的 `Lifecycle.Event` 是一个枚举类型，定义了一些与生命周期相关的枚举值。所以，当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出，该方法内部又调用了基类的 `activeStateChanged()` 方法，该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。\n\n继续看 `activeStateChanged()` 方法，我们可以看出在最后的几行中，它调用了 `dispatchingValue(this)` 方法。所以，当 Fragment 从处于后台切换到前台之后，会将当前缓存的值通知给观察者。\n\n那么值是如何缓存的，以及缓存了多少值呢？回到之前的 `setValue()` 和 `dispatchingValue()` 方法中，我们发现值是以一个单独的变量进行缓存的，\n\n```java\n    private volatile Object mData = NOT_SET;\n```\n\n因此，在我们的示例中，当页面从后台切换到前台的时候，只能将最后一次缓存的结果通知给观察者就真相大白了。\n\n## 总结\n\n从上面的分析中，我们对 LiveData 总结如下，\n\n3. 当调用 `observe()` 方法的时候，我们的观察者将会和 LifecycleOwner (Fragment 或者 Activity) 一起被包装到一个类中，并使用哈希表建立映射关系。同时，还会对 Fragment 或者 Activity 的生命周期方法进行监听，依次来达到监听观察者是否处于 active 状态的目的。\n2. 当 Fragment 或者 Activity 处于后台的时候，其内部的观察者将处于非 active 状态，此时使用 `setValue()` 设置的值会缓存到 LiveData 中。但是这种缓存只能缓存一个值，新的值会替换旧的值。因此，当页面从后台恢复到前台的时候只有最后设置的一个值会被传递给观察者。\n3. 2 中的当 Fragment 或者 Activity 从后台恢复的时候进行通知也是通过监听其生命周期方法实现的。\n4. 调用了 `observe()` 之后，Fragment 或者 Activity 被缓存了起来，不会造成内存泄漏吗？答案是不会的。因为 LiveData 可以对其生命周期进行监听，当其处于销毁状态的时候，该映射关系将被从缓存中移除。\n\n以上。\n\n\n"
  },
  {
    "path": "高阶技术/浅谈ViewModel生命周期控制.md",
    "content": "# 浅谈 ViewModel 的生命周期控制\n\n## 1、从一个 Bug 说起\n\n想必有过一定开发经验的同学对 ViewModel 都不会陌生，它是 Google 推出的 MVVM 架构模式的一部分。这里它的基础使用我们就不介绍了，毕竟这种类型的文章也遍地都是。今天我们着重来探讨一下它的生命周期。\n\n起因是这样的，昨天在修复程序中的 Bug 的时候遇到了一个异常，是从 ViewModel 中获取存储的数据的时候报了空指针。我启用了开发者模式的 “不保留活动” 之后很容易地重现了这个异常。出现错误的原因也很简单，相关的代码如下：\n\n```java\n    private ReceiptViewerViewModel viewModel;\n\n    @Override\n    protected void doCreateView(Bundle savedInstanceState) {\n        viewModel = ViewModelProviders.of(this).get(ReceiptViewerViewModel.class); // 1\n        handleIntent(savedInstanceState);\n        // ... \n    }\n\n    private void handleIntent(Bundle savedInstanceState) {\n        LoadingStatus loadingStatus;\n        if (savedInstanceState == null) {\n            loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);\n        }\n        viewModel.setLoadingStatus(loadingStatus);\n    }\n```\n\n在方法 `doCreateView()` 中我获取了 `viewModel` 实例，然后在 `handleIntent()` 方法中从 `Intent` 中取出传入的参数。当然，还要使用 `viewModel` 的 `getter` 方法从其中取出 `loadingStatus` 并使用。在使用的时候抛了空指针。\n\n显然，一般情况下是不会出现问题的，但是如果 Activity 在后台被销毁了，那么再重建的时候就会出现空指针异常。\n\n解决方法也比较简单，在 `onSaveInstanceState()` 方法中将数据缓存起来即可，即：\n\n```java\n    private void handleIntent(Bundle savedInstanceState) {\n        LoadingStatus loadingStatus;\n        if (savedInstanceState == null) {\n            loadingStatus = (LoadingStatus) getIntent().getSerializableExtra(Router.RECEIPT_VIEWER_LOADING_STATUS);\n        } else {\n            loadingStatus = (LoadingStatus) savedInstanceState.get(Router.RECEIPT_VIEWER_LOADING_STATUS);\n        }\n        viewModel.setLoadingStatus(loadingStatus);\n    }\n\n    @Override\n    protected void onSaveInstanceState(Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putSerializable(Router.RECEIPT_VIEWER_LOADING_STATUS, viewModel.getLoadingStatus());\n    }\n```\n\n现在的问题是 ViewModel 的生命周期问题，有人说在 `doCreateView()` 方法的 1 处得到的不是之前的 ViewModel 吗，数据不是之前已经设置过了吗？所以，这牵扯 ViewModel 是在什么时候被销毁和重建的问题。\n\n## 2、ViewModel 的生命周期\n\n有的人希望使用 ViewModel 缓存 Activity 的信息，然后在 `doCreateView()` 方法的 1 处得到之前的 ViewModel 实例，这样 ViewModel 的数据就是 Activity 销毁之前的数据，这可行吗？我们从源码角度来看下这个问题。\n\n首先，每次获取 `viewmodel` 实例的时候都会调用下面的方法来获取 ViewModel 实例。从下面的 `get()` 方法中可以看出，实例化过的 ViewModel 是从 `mViewModelStore` 中获取的。如果由 `ViewModelStores.of(activity)` 方法得到的 `mViewModelStore` 不是同一个，那么得到的 ViewModel 也不是同一个。\n\n下面方法中的 `get()` 方法中后续的逻辑是如果之前没有缓存过 ViewModel，那么就构建一个新的实例并将其放进 `mViewModelStore` 中。这部分代码逻辑比较简单，我们不继续分析了。\n\n```java\n    // ViewModelProviders#of()\n    public static ViewModelProvider of(@NonNull FragmentActivity activity) {\n        ViewModelProvider.AndroidViewModelFactory factory =\n                ViewModelProvider.AndroidViewModelFactory.getInstance(activity);\n        return new ViewModelProvider(ViewModelStores.of(activity), factory); // 1\n    }\n\n    // ViewModelProvider#get()\n    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {\n        ViewModel viewModel = mViewModelStore.get(key);\n\n        if (modelClass.isInstance(viewModel)) {\n            return (T) viewModel;\n        }\n\n        viewModel = mFactory.create(modelClass);\n        mViewModelStore.put(key, viewModel);\n        return (T) viewModel;\n    }\n```\n\n我们回到上述 `of()` 方法的 1 处，来看下 `ViewModelStores.of()` 方法，其定义如下：\n\n```java\n    // ViewModelStores#of()\n    public static ViewModelStore of(@NonNull FragmentActivity activity) {\n        if (activity instanceof ViewModelStoreOwner) {\n            return ((ViewModelStoreOwner) activity).getViewModelStore();\n        }\n        return holderFragmentFor(activity).getViewModelStore();\n    }\n\n    // HolderFragment#holderFragmentFor()\n    public static HolderFragment holderFragmentFor(FragmentActivity activity) {\n        return sHolderFragmentManager.holderFragmentFor(activity);\n    }\n```\n\n这里会从 `holderFragmentFor()` 方法中获取一个 `HolderFragment` 实例，它是一个 Fragment 的实现类。然后从该实例中获取 `ViewModelStore` 的实例。所以，ViewModel 对生命周期的管理与 Glide 和 RxPermission 等框架的处理方式一致，就是使用一个空的 Fragment 来进行生命周期管理。\n\n对于 `HolderFragment`，其定义如下。从下面的代码我们可以看出，上述用到的 ViewModelStore 实例就是 `HolderFragment` 的一个局部变量。所以，ViewModel 使用空的 Fragment 管理生命周期实锤了。\n\n```java\n    public class HolderFragment extends Fragment implements ViewModelStoreOwner {\n        private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();\n        private ViewModelStore mViewModelStore = new ViewModelStore();\n\n        public HolderFragment() {\n            setRetainInstance(true);\n        }\n\n        // ...\n    }\n```\n\n此外，我们注意到上面的 HolderFragment 的构造方法中还调用了 `setRetainInstance(true)` 这一行代码。我们进入该方法看它的注释：\n\n> Control whether a fragment instance is retained across Activity\n> re-creation (such as from a configuration change).  This can only\n> be used with fragments not in the back stack.  If set, the fragment\n> lifecycle will be slightly different when an activity is recreated:\n\n就是说，当 Activity 被重建的时候该 Fragment 会被保留，然后传递给新创建的 Activity. 但是，这只适用于不处于后台的 Fragment. 所以，如果 Activity 处于后台的时候，Fragment 不会保留，那么它得到的 `ViewModelStore` 实例就不同了。\n\n所以，总结下来，准确地将：**当 Activity 处于前台的时候被销毁了，那么得到的 ViewModel 是之前实例过的 ViewModel；如果 Activity 处于后台时被销毁了，那么得到的 ViewModel 不是同一个。举例说，如果 Activity 因为配置发生变化而被重建了，那么当重建的时候，ViewModel 是之前的实例；如果因为长期处于后台而被销毁了，那么重建的时候，ViewModel 就不是之前的实例了。**\n\n回到之前的 `holderFragmentFor()` 方法，我们看下这里具体做了什么，其定义如下。\n\n```java\n    // HolderFragmentManager#holderFragmentFor()\n    HolderFragment holderFragmentFor(FragmentActivity activity) {\n        // 使用 FragmentManager 获取 HolderFragment\n        FragmentManager fm = activity.getSupportFragmentManager();\n        HolderFragment holder = findHolderFragment(fm);\n        if (holder != null) {\n            return holder;\n        }\n        // 从哈希表中获取 HolderFragment\n        holder = mNotCommittedActivityHolders.get(activity);\n        if (holder != null) {\n            return holder;\n        }\n\n        if (!mActivityCallbacksIsAdded) {\n            mActivityCallbacksIsAdded = true;\n            activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);\n        }\n        holder = createHolderFragment(fm);\n        // 将新的实例放进哈希表中\n        mNotCommittedActivityHolders.put(activity, holder);\n        return holder;\n    }\n```\n\n首先，尝试使用 `FragmentManager` 来获取 `HolderFragment`，如果获取不到就从 `mNotCommittedActivityHolders` 中进行获取。这里的 `mNotCommittedActivityHolders` 是一个哈希表，每次实例化的新的 HolderFragment 会被添加到哈希表中。\n\n另外，上面的方法中还使用了 ActivityLifecycleCallbacks 对 Activity 的生命周期进行监听。其定义如下，\n\n```java\n    private ActivityLifecycleCallbacks mActivityCallbacks =\n            new EmptyActivityLifecycleCallbacks() {\n                @Override\n                public void onActivityDestroyed(Activity activity) {\n                    HolderFragment fragment = mNotCommittedActivityHolders.remove(activity);\n                }\n            };\n```\n\n当 Activity 被销毁的时候会从哈希表中移除映射关系。所以，每次 Activity 被销毁的时候哈希表中的映射关系都不存在了。而之所以 ViewModel 能够实现在 Activity 配置发生变化的时候获取之前的 ViewModel 是通过上面的 `setRetainInstance(true)` 和 `findHolderFragment(fm)` 来实现的。\n\n## 总结\n\n以上就是 ViewModel 的生命周期的总结。我们只是通过对主流程的分析研究了它的生命周期的流程，实际上内部还有许多小细节，逻辑也比较简单，我们就不一一说明了。\n\n其实，从 Google 的官方文档中，我们也能够得到上面的总结，\n\n![viewmodel-lifecycle](res/viewmodel-lifecycle.png)\n\n这里使用了 `Activity rotated`，也就是 Activity 处于前台的时候配置发生变化的情况，而不是处于后台，不知道你之前有没有注意这一点呢？\n\n以上。\n\n"
  }
]