Repository: Shouheng88/Android-notes Branch: master Commit: 416fdc3de0ea Files: 103 Total size: 825.2 KB Directory structure: gitextract_c1rujubr/ ├── API简析/ │ └── LruCache.md ├── Kotlin/ │ └── Kotlin.md ├── README.md ├── 其他/ │ ├── Android知识点随记.md │ ├── MarkNote版本1的.md │ ├── MarkNote版本2.md │ └── 计算机视觉与Android.md ├── 响应式编程/ │ ├── RxJava系列-4:RxJava源码分析.md │ ├── RxJava系列(1):一篇的比较全面的RxJava2方法总结.md │ ├── RxJava系列(2):Flowable和背压.md │ └── RxJava系列(3):用RxJava打造EventBus.md ├── 四大组件/ │ ├── Activity.md │ ├── Broadcast.md │ ├── Fragment.md │ └── Service.md ├── 图片加载/ │ ├── Android相机最佳实践.md │ ├── Glide系列:Glide主流程源码分析.md │ ├── Glide系列:Glide的缓存的实现原理.md │ ├── Glide系列:Glide的配置和使用方式.md │ └── 图片压缩框架封装.md ├── 工作空间/ │ ├── OOM优化.md │ ├── Tinker.md │ ├── URL编码问题.md │ ├── 文章暂时存放.md │ ├── 百度定位API.md │ └── 第三方库整理.md ├── 开发工具/ │ ├── ADB_常见的ADB指令总结.md │ ├── Gradle_常见的指令和配置总结.md │ └── Keytool_常用的指令.md ├── 异步编程/ │ ├── Android多线程编程:IntentService和HandlerThread.md │ └── AsyncTask的使用和源码分析.md ├── 性能优化/ │ ├── Android性能优化-ANR.md │ ├── Android性能优化-内存优化.md │ ├── Android性能优化-启动优化.md │ ├── Android性能优化-布局优化.md │ ├── Android相机Camera1资料.md │ ├── Android相机Camera2资料.md │ └── Android进程保活.md ├── 消息机制/ │ ├── EventBus的源码分析.md │ ├── 线程通信:Handler、MessageQueue和Looper.md │ └── 跨进程通信:Binder机制.md ├── 混合开发/ │ └── ReactNative.md ├── 笔试面试/ │ ├── Android高级软件工程师2017.md │ ├── Android高级面试_10_跨平台开发.md │ ├── Android高级面试_11_JNINDK.md │ ├── Android高级面试_12_各种三方库分析.md │ ├── Android高级面试_12_算法.md │ ├── Android高级面试_12_项目经验梳理.md │ ├── Android高级面试_1_Handler相关.md │ ├── Android高级面试_2_IPC相关.md │ ├── Android高级面试_3_语言相关.md │ ├── Android高级面试_4_虚拟机相关.md │ ├── Android高级面试_5_四大组件、系统源码等.md │ ├── Android高级面试_6_性能优化.md │ ├── Android高级面试_7_网络相关.md │ ├── Android高级面试_8_热修补插件化等.md │ ├── Android高级面试_9_网络基础.md │ ├── README.md │ ├── java/ │ │ ├── ArrayList、LinkedList、Vector.md │ │ ├── Collection包结构,与Collections的区别.md │ │ ├── HashMap和ConcurrentHashMap的区别,HashMap的底层源码.md │ │ ├── HashMap和HashTable的区别.md │ │ ├── Hashcode的作用.md │ │ ├── Java1.7与1.8新特性.md │ │ ├── Java的四种引用,强弱软虚,用到的场景.md │ │ ├── Map、Set、List、Queue、Stack的特点与用法.md │ │ ├── Object有哪些公用方法.md │ │ ├── Override和Overload的含义去区别.md │ │ ├── Static class 与 non static class的区别.md │ │ ├── String、StringBuffer与StringBuilder的区别.md │ │ ├── Switch能否用string做参数.md │ │ ├── TreeMap、HashMap、LindedHashMap.md │ │ ├── equals与==的区别.md │ │ ├── jvm-java 内存模型 以及各个分区具体内容.md │ │ ├── throw和throws有什么区别.md │ │ ├── wait()和sleep()的区别.md │ │ ├── 九种基本数据类型的大小以及他们的封装类.md │ │ ├── 内存溢出和内存泄露的区别.md │ │ └── 解析XML的几种方式的原理与特点:DOM、SAX、PULL.md │ ├── 今日头条Android面试.md │ └── 初级工程师.md ├── 系统架构/ │ ├── Android应用启动过程.md │ ├── Android应用安装过程.md │ ├── Android打包过程.md │ ├── Android系统启动过程.md │ ├── Android系统架构.md │ ├── SurefaceView_and_TextureView.md │ ├── 控件体系/ │ │ ├── RV.md │ │ ├── RV各种效果实现.md │ │ ├── View体系详解:View的工作流程.md │ │ ├── View体系详解:坐标系、滑动事件和分发机制.md │ │ ├── View体系详解:自定义控件.md │ │ └── 动画体系详解.md │ └── 窗口机制/ │ └── Android的Window管理机制.md ├── 网络访问/ │ ├── OKHttp源码阅读.md │ └── Retrofit源码阅读.md └── 高阶技术/ ├── Android插件化.md ├── Dagger从集成到源码.md ├── JNI技术总结.md ├── 探索Android架构设计.md ├── 注解在Android中的应用.md ├── 浅谈LiveData的通知过程.md └── 浅谈ViewModel生命周期控制.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: API简析/LruCache.md ================================================ # Android 内存缓存框架 LruCache 的源码分析 LruCache 是 Android 提供的一种基于内存的缓存框架。LRU 是 **Least Recently Used** 的缩写,即最近最少使用。当一块内存最近很少使用的时候就会被从缓存中移除。在这篇文章中,我们会先简单介绍 LruCache 的使用,然后我们会对它的源码进行分析。 ## 1、基本的使用示例 首先,让我们来简单介绍一下如何使用 LruCache 实现内存缓存。下面是 LruCache 的一个使用示例。 这里我们实现的是对 RecyclerView 的列表的截图的功能。因为我们需要将列表的每个项的 Bitmap 存储下来,然后当所有的列表项的 Bitmap 都拿到的时候,将其按照顺序和位置绘制到一个完整的 Bitmap 上面。如果我们不使用 LruCache 的话,当然也能够是实现这个功能——将所有的列表项的 Bitmap 放置到一个 List 中即可。但是那种方式存在缺点:因为是强引用类型,所以当内存不足的时候会导致 OOM。 在下面的方法中,我们先获取了内存的大小的 8 分之一作为缓存空间的大小,用来初始化 LruCache 对象,然后从 RecyclerView 的适配器中取出所有的 ViewHolder 并获取其对应的 Bitmap,然后按照键值对的方式将其放置到 LruCache 中。当所有的列表项的 Bitmap 都拿到之后,我们再创建最终的 Bitmap 并将之前的 Bitmap 依次绘制到最终的 Bitmap 上面: ```java public static Bitmap shotRecyclerView(RecyclerView view) { RecyclerView.Adapter adapter = view.getAdapter(); Bitmap bigBitmap = null; if (adapter != null) { int size = adapter.getItemCount(); int height = 0; Paint paint = new Paint(); int iHeight = 0; final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用内存的 8 分之一作为该缓存框架的缓存空间 final int cacheSize = maxMemory / 8; LruCache bitmaCache = new LruCache<>(cacheSize); for (int i = 0; i < size; i++) { RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i)); adapter.onBindViewHolder(holder, i); holder.itemView.measure( View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight()); holder.itemView.setDrawingCacheEnabled(true); holder.itemView.buildDrawingCache(); Bitmap drawingCache = holder.itemView.getDrawingCache(); if (drawingCache != null) { bitmaCache.put(String.valueOf(i), drawingCache); } height += holder.itemView.getMeasuredHeight(); } bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888); Canvas bigCanvas = new Canvas(bigBitmap); Drawable lBackground = view.getBackground(); if (lBackground instanceof ColorDrawable) { ColorDrawable lColorDrawable = (ColorDrawable) lBackground; int lColor = lColorDrawable.getColor(); bigCanvas.drawColor(lColor); } for (int i = 0; i < size; i++) { Bitmap bitmap = bitmaCache.get(String.valueOf(i)); bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint); iHeight += bitmap.getHeight(); bitmap.recycle(); } } return bigBitmap; } ``` 因此,我们可以总结出 LruCahce 的基本用法如下: 首先,你要声明一个缓存空间的大小,在这里我们用了运行时内存的 8 分之 1 作为缓存空间的大小 ```java LruCache bitmaCache = new LruCache<>(cacheSize); ``` **但是应该注意的一个问题是缓存空间的单位的问题**。因为 LruCache 的键值对的值可能是任何类型的,所以你传入的类型的大小如何统计需要自己去指定。后面我们在分析它的源码的时候会指出它的单位的问题。LruCahce 的 API 中也已经提供了计算传入的值的大小的方法。我们只需要在实例化一个 LruCache 的时候覆写该方法即可。而这里我们认为一个 Bitmap 对象所占用的内存的大小不超过 1KB. 然后,我们可以像普通的 Map 一样调用它的 `put()` 和 `get()` 方法向缓存中插入和从缓存中取出数据: ```java bitmaCache.put(String.valueOf(i), drawingCache); Bitmap bitmap = bitmaCache.get(String.valueOf(i)); ``` ## 2、LruCahce 源码分析 ### 2.1 分析之前:当我们自己实现一个 LruCache 的时候,我们需要考虑什么 在我们对 LruCache 的源码进行分析之前,我们现来考虑一下当我们自己去实现一个 LruCache 的时候需要考虑哪些东西,以此来带着问题阅读源码。 因为我们需要对数据进行存储,并且又能够根据指定的 id 将数据从缓存中取出,所以我们需要使用哈希表表结构。或者使用两个数组,一个作为键一个作为值,然后使用它们的索引来实现映射也行。但是,后者的效率不如前者高。 此外,我们还要对插入的元素进行排序,因为我们需要移除那些使用频率最小的元素。我们可以使用链表来达到这个目的,每当一个数据被用到的时候,我们可以将其移向链表的头节点。这样当要插入的元素大于缓存的最大空间的时候,我们就将链表末位的元素移除,以在缓存中腾出空间。 综合这两点,我们需要一个既有哈希表功能,又有队列功能的数据结构。在 Java 的集合中,已经为我们提供了 LinkedHashMap 用来实现这个功能。 实际上在 Android 中的 LruCache 也正是使用 LinkedHashMap 来实现的。LinkedHashMap 拓展自 HashMap。如果理解 HashMap 的话,它的源码就不难阅读。LinkedHashMap 仅在 HashMap 的基础之上,又将各个节点放进了一个双向链表中。每次增加和删除一个元素的时候,被操作的元素会被移到到链表的末尾。Android 中的 LruCahce 就是在 LinkedHashMap 基础之上进行了一层拓展,不过 Android 中的 LruCache 的实现具有一些很巧妙的地方值得我们学习。 ### 2.2 LruCache 源代码分析 从上面的分析中我们知道了选择 LinkedHashMap 作为底层数据结构的原因。下面我们分析其中的一些方法。这个类的实现还有许多的细节考虑得非常周到,非常值得我们借鉴和学习。 #### 2.2.1 缓存的最大可用空间 在 LruCache 中有两个字段 size 和 maxSize. maxSize 会在 LruCache 的构造方法中被赋值,用来表示该缓存的最大可用的空间: ```java int cacheSize = 4 * 1024 * 1024; // 4MiB,cacheSize 的单位是 KB LruCache bitmapCache = new LruCache(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }}; ``` 这里我们使用 4MB 来设置缓存空间的大小。我们知道 LruCache 的原理是指定了空间的大小之后,如果继续插入元素时,空间超出了指定的大小就会将那些“可以被移除”的元素移除掉,以此来为新的元素腾出空间。那么,因为插入的类型时不确定的,所以具体被插入的对象如何计算大小就应该交给用户来实现。 在上面的代码中,我们直接使用了 Bitmap 的 `getByteCount()` 方法来获取 Bitmap 的大小。同时,我们也注意到在最初的例子中,我们并没有这样去操作。那样的话一个 Bitmap 将会被当作 1KB 来计算。 这里的 sizeOf() 是一个受保护的方法,显然是希望用户自己去实现计算的逻辑。它的默认值是 1,单位和设置缓存大小指定的 maxSize 的单位相同: ```java protected int sizeOf(K key, V value) { return 1; } ``` 这里我们还需要提及一下:虽然这个方法交给用户来实现,但是在 LruCache 的源码中,不会直接调用这个方法,而是 ```java private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result; } ``` 所以,这里又增加了一个检查,防止参数错误。其实,这个考虑是非常周到的,试想如果传入了一个非法的参数,导致了意外的错误,那么错误的地方就很难跟踪了。如果我们自己想设计 API 给别人用并且提供给他们自己可以覆写的方法的时候,不妨借鉴一下这个设计。 #### 2.2.2 LruCache 的 get() 方法 下面我们分析它的 get() 方法。它用来从 LruCahce 中根据指定的键来获取对应的值: ```java /** * 1). 获取指定 key 对应的元素,如果不存在的话就用 craete() 方法创建一个。 * 2). 当返回一个元素的时候,该元素将被移动到队列的首位; * 3). 如果在缓存中不存在又不能创建,就返回n ull */ public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { // 在这里如果返回不为空的话就会将返回的元素移动到队列头部,这是在 LinkedHashMap 中实现的 mapValue = map.get(key); if (mapValue != null) { // 缓存命中 hitCount++; return mapValue; } // 缓存没有命中,可能是因为这个键值对被移除了 missCount++; } // 这里的创建是单线程的,在创建的时候指定的 key 可能已经被其他的键值对占用 V createdValue = create(key); if (createdValue == null) { return null; } // 这里设计的目的是防止创建的时候,指定的 key 已经被其他的 value 占用,如果冲突就撤销插入 synchronized (this) { createCount++; // 向表中插入一个新的数据的时候会返回该 key 之前对应的值,如果没有的话就返回 null mapValue = map.put(key, createdValue); if (mapValue != null) { // 冲突了,还要撤销之前的插入操作 map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); } } if (mapValue != null) { entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { trimToSize(maxSize); return createdValue; } } ``` 这里获取值的时候对当前的实例进行了加锁以保证线程安全。当用 map 的 get() 方法获取不到数据的时候用了 `create()` 方法。因为当指定的键值对找不到的时候,可能它本来就不存在,可能是因为缓存不足被移除了,所以,我们需要提供这个方法让用户来处理这种情况,该方法默认返回 null. 如果用户覆写了 `create()` 方法,并且返回的值不为 null,那么我们需要将该值插入到哈希表中。 插入的逻辑也在同步代码块中进行。这是因为,创建的操作可能过长而且是非同步的。当我们再次向指定的 key 插入值的时候,它可能已经存在值了。所以当调用 map 的 put() 的时候如果返回不为 null,就表明对应的 key 已经有对应的值了,就需要撤销插入操作。最后,当 mapValue 非 null,还要调用 `entryRemoved()` 方法。每当一个键值对从哈希表中被移除的时候,这个方法将会被回调一次。 最后调用了 `trimToSize()` 方法,用来保证新的值被插入之后缓存的空间大小不会超过我们指定的值。当发现已经使用的缓存超出最大的缓存大小的时候,“最近最少使用” 的项目将会被从哈希表中移除。 那么如何来判断哪个是 “最近最少使用” 的项目呢?我们先来看下 `trimToSize()` 的方法定义: ```java public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize) { break; } // 获取用来移除的 “最近最少使用” 的项目 Map.Entry toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } } ``` 显然,这里是使用了 LinkedHashMap 的 `eldest()` 方法,这个方法的返回值是: ```java public Map.Entry eldest() { return head; } ``` 也就是 LinkedHashMap 的头结点。那么为什么要移除头结点呢?这不符合 LRU 的原则啊,这里分明是直接移除了头结点。实际上不是这样,魔力发生在 `get()` 方法中。在 LruCache 的 get() 方法中,我们调用了 LinkedHashMap 的 `get()` 方法,这个方法中又会在拿到值的时候调用下面的方法: ```java void afterNodeAccess(Node e) { // move node to last LinkedHashMapEntry last; if (accessOrder && (last = tail) != e) { LinkedHashMapEntry p = (LinkedHashMapEntry)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } } ``` 这里的逻辑是把 `get()` 方法中返回的结点移动到双向链表的末尾。所以,最近最少使用的结点必然就是头结点了。 ## 3、总结 以上是我们对 LruCache 的是使用和源码的总结,这里我们实际上只分析了 `get()` 的过程。因为这个方法才是 LruCache 的核心,它包含了插入值和移动最近使用的项目的过程。至于 `put()` 和 `remove()` 两种方法,它们内部实际上直接调用了 LinkedHashMap 的方法。这里我们不再对它们进行分析。 ------ **如果您喜欢我的文章,可以在以下平台关注我:** - 个人主页:[https://shouheng88.github.io/](https://shouheng88.github.io/) - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) ================================================ FILE: Kotlin/Kotlin.md ================================================ # Kotlin 学习笔记-1:基础语法 ## 1、初识 Kotlin ### 1.1 基本特性梳理 1. 本质上是**静态类型语言**,编译期确定类型,**但无需明确指定变量类型**; 2. 对**可空类型的支持**,可以在编译期发现空指针; 3. 支持**函数式编程**,虽然 Java 8 以后都支持了; 4. 类文件的后缀名式 `.kt`,编译之后还是生成 class 文件,只是编译器使用的是 `kotlinc`(对应于 javac),执行 class 的时候还是使用 java; 5. 可以使用转换器将 Java 转换成 kotlin; 6. Kotlin 标准库给 Java 库做了封装,我们可以简化原生 Java 库的调用; ### 1.2 类文件结构 在 Kotlin 中文件名称和文件的内容没有关系(在 Java 中文件名和类名相同),并且文件内部定义的是函数还是类都没关系。比如,下面是定义在目录 `me/shouheng/demo1/FirstDemo.kt` 中的类和函数,这里类和函数处于文件的同一层次。另外,一个文件中还可以定义多个类和多个函数,都是允许的。 ```kotlin package me.shouheng // 包的声明应处于源文件顶部 class Person (age : Int, name : String) // 声明了一个类 class Person2 (val age : Int, val name : String) // 声明了一个类 ``` 类和函数的真实包名是由文件中的 **package** 关键字指定的,与文件结构没有必然的关系。当然,我们建议按照 Java 的规则使其对应起来,因为这样维护起来更好、逻辑更清晰些。 ### 1.3 定义函数 ```kotlin fun doSomething(person: Person) : Int { // 定义了一个函数 // 在字符串中使用 “$+变量名” 的格式进行占位,相当于 "My name is" + persion + "!" println("My name is $person!") } fun sum(a: Int, b: Int) = a + b ``` 1. 函数的定义使用关键字 `fun`,覆写函数的话就在函数名前面加上 `override fun`。 2. 变量和返回值的类型被放在冒号后面,如果返回无意义的类型,可以使用 `Unit`,也可以省略。 3. **字符串模板**:在字符串中使用 `$+变量名` 的格式进行占位(这叫字符串模板),如果希望使用美元符号,前面加上反斜杠即可。 4. 也可以将表达式作为函数体、返回值类型自动推断。 5. 如果需要把一个字符串当作正则表达式,需要显式调用字符串的 `toRegex()` 方法才行。 6. 三重引号中的字符不会做任何转义,即 `"""$"""` 可以直接当作美元。 ### 1.4 定义变量 ```kotlin fun test(args : Array) { val a = Person(10, "Ming") var b: Person b = Person(11, "Xing") doSomething(a) } ``` 1. **数组**:没有专门用来声明数组的,全部都是类。可以像下面这样声明数组 `Array`。另外,可以按照 `args[0]` 的方式获取数组元素。 2. **声明变量有 `var` 和 `val` 两中方式**:系统可以自动推断类型,`var` 声明的变量可以二次赋值,而 `val` 不行,后者相当于 `final` 的。 3. 虽然 `var` 类型的变量可以二次赋值,但是两次赋值的类型必须相同。 4. 可以在声明变量的时候使用冒号指定变量的类型,像上面的 b 一样(大部分情况下可以省略,因为编译器可以自动推断)。 5. 初始化一个类的时候不需要 `new` 关键字(抛出异常的时候自然也一样)。 ### 1.5 使用循环 ```kotlin // 循环 Map val map = HashMap() for (c in 'A'..'F') { // 循环字符串 map[c] = Integer.toHexString(c.toInt()) } for ((k, v) in map) { // 输出结果是 print("<$k,$v> ") } // 循环列表 val items = listOf("apple", "banana", "kiwifruit") for (index in items.indices) { println("item at $index is ${items[index]}") } // while 循环 var index = 0 while (index < items.size) { // 使用 while 循环 println("item at $index is ${items[index]}") index++ } ``` 总结, 1. Kotlin 的 for 循环与 Java 稍有不同,它跟 js 等更相似,即使用 `in` 关键字。 2. 遍历 map 的时候使用上述方式,以键值对的形式遍历即可。 3. 要按照索引的方式进行遍历,需要先使用列表的 `indices` 得到索引列表。 4. while 循环和 Java 中的使用方式基本一致,包括 `while` 和 `do...while` 两种形式。 ### 1.6 when 类似于 Java 中的 switch,但是它的每个条件中默认加入了 break. 另外,它还有一个比较好的地方是,它会检查枚举是否都包含进去了,如果没全部包含,它会提示你全包含或者加入 else 语句。 ```kotlin fun multiple(city: City2) = when(city) { City2.BEIGING -> { 2*10000 2+2 } City2.SHANGHAI,City2.GUANGZHOU->3*10000 else -> 5 } fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long" !is String -> "Not a string" else -> "Unknown" } ``` 另外,从示例 2 中可以看出, 1. 注意每个条件之后需要加上 `->` 才行哦! 2. 当多个类型的逻辑相同的时候,可以把它们放在 when 的一个条件里,然后用逗号分隔开。 3. when 比 switch 的功能更加强大,它还可以使用不同类型的判断条件。(参考示例 2) ### 1.7 控制语句 #### 1.7.1 if 语句 在 Kotlin 中,if是一个表达式,即它会返回一个值。 因此就不需要三元运算符 `? : `,比如 `val max = if (a > b) a else b`。 #### 1.7.2 返回与跳转 Kotlin 中返回与跳转语句也是 return、break 和 continue 三种。它们的基本使用方式与 Java 相同。此外,Kotlin 中还支持标签。标签的格式为标识符后跟 @ 符号,例如:`abc@`、`fooBar@` 都是有效的标签。我们可以使用标签进行流程的控制(用的比较少)。 ### 1.8 异常处理 `try..catch` 语句的基本结构如下,和 Java 基本相似,只是 catch 中声明变量的方式,下面的函数会当小于 0 时返回 -1,否则返回 1. 另外,kotlin 中不分受检异常和非受检异常,不会强制你捕获异常。 ```kotlin fun tryTest(i : Int) = try { if (i < 0) throw IllegalArgumentException("< 0") else 1 } catch (e : Exception) { -1 } ``` ## 2、类与对象 ### 2.1 类声明 Kotlin 中使用关键字 `class` 声明类。类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的;如果一个类没有类体,可以省略花括号。 ```kotlin class MyClass { /*...*/ } class Empty ``` ### 2.2 构造方法 在 Kotlin 中的一个类可以有一个`主构造函数`以及一个或多个`次构造函数`。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 `constructor` 关键字。 ```kotlin class Person constructor(firstName: String) { ... } // 主构造函数 class Person(firstName: String) { ... } // 省略主构造函数 class Person(val firstName: String) { ... } class DontCreateMe private constructor () { ... } // 将构造函数设置成私有的 ``` 注意上述声明方式中的 2 和 3 的区别,后者声明之后有一个局部变量 firstName,而前者没有声明任何变量。可以通过 private 关键字将构造函数设置成私有的。 类也可以声明前缀有 constructor 的次构造函数。果类有一个主构造函数,每个次构造函数需要委托给主构造函数,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可: ```kotlin // 声明了一个次构造函数 class Person { constructor(parent: Person) { parent.children.add(this) } } // 有主构造函数时,次构造函数的声明方式 class Person(val name: String) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } } ``` ### 2.3 初始化代码块 Kotlin 中也有初始化代码块,非静态初始化代码块使用 `init` 关键字即可。 ```kotlin class Person { init { // // ... } } ``` 初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。 静态代码块与静态变量定义的方式一致,略显繁琐,后续说明。 ### 2.4 函数 函数是 Kotlin 中非常重要的概念,Kotlin 提供了许多便利的函数。 ```kotlin // 默认参数 fun MyFun(a: String = "a", b: String) { println("$a $b") } // 为 String 增加函数 fun String.lastChar() : Char = this[length - 1] // 为 String 增加属性 val String.lastChar: Char get() = get(length - 1) // 可变数量的参数 fun varFun(vararg args: String) { for (arg in args) println(arg) } fun main(args: Array) { MyFun(a = "x", b = "y") // 指定参数名称:输出 x y,允许指定参数的名称 MyFun(b = "y") // 指定参数名称:输出 a y,使用默认的参数 val args = arrayOf("A", "B", "C") varFun(*args) // 使用伸展操作符调用可变数量参数的函数 } ``` 1. 允许在调用方法的时候指定参数的名称,并且指定了一个参数之后,后面的参数都要指定名称; 2. 允许为函数的参数指定 `缺省参数`,比如上面的 a 默认是 `a`; 3. 可以为别人的函数添加函数和属性,但是 `拓展函数无法访问私有的或者受保护的成员`。本质上拓展函数将调用它的实例当作第一个参数,这是本质的实现原理,很多问题可以依靠这个理解。拓展函数无法被继承,原因很简单,就是因为它们只相当于调用了一个静态方法而把实例当作参数实现的 4. 可变数量参数函数调用的时候可以使用伸展(spread)操作符(就是在数组前面加 `*`)。缺省参数定义的时候需要使用 `vararg`(也许是因为 `..` 被当作其他用途了),当传入数组的时候的需要解包,也就是数组前面加 `*`。 5. 把函数提升到与类同一层次,这样它就成 `静态函数` 了,把字段提升到与类同一层次,这样它就成 `静态字段` 了。 6. 可以在函数内部定义`局部函数`,并且局部函数可以访问外部函数(即闭包)的局部变量。 7. 导入函数的时候可以使用 `as` 重命名导入以简化使用。 ### 2.5 属性 #### 2.5.1 声明属性 声明类的属性有 var 和 val 两种方式。声明一个属性的格式是, ```kotlin var [: ] [= ] [] [] ``` 示例程序, ```kotlin class Person{ var grade: Int = 0 get() = field + 1 set(value) { field = value + 1 } var age: Int = 0 private set // 修改默认访问权限 } ``` Kotlin 中会将类的局部变量的访问权默认为 pulic 的,所以外部可以直接通过实例获取字段和赋值。可以通过上述方式来修改它的默认方法权限。 可以通过 `get()` 和 `set()` 来重写 getter 和 setter 方法。一般情况下,使用默认的 `get()` 和 `set()` 默认逻辑即可,这也是 Java 规范。如果想增加新的逻辑,可以增加一个新的方法。注意,在覆写的时候,如果要修改属性的值,需要通过 field 来完成。field 标识符只能用在属性的访问器内,也被称为幕后字段。 #### 2.5.2 编译期常量 const ```kotlin // 定义在类顶层 const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated" // 定义在类内部,可以用来为类添加静态常量 class MyClass { companion object { const val EXTRA_LAUNCH_TYPE = "__extra_launch_type" } // 外部访问方式是:MyClass.EXTRA_LAUNCH_TYPE } ``` 使用 `const` 修饰符标记为编译期常量。 这些属性需要满足以下要求: 1. 位于`顶层`或者是 `object` 声明或 `companion object` 的一个成员; 2. 以 String 或原生类型值初始化; 3. 没有自定义 getter。 #### 2.5.3 延迟初始化 lateinit 属性与变量 ```kotlin lateinit var subject: TestSubject ``` 一般地,属性声明为非空类型必须在构造函数中初始化。当无法在构造器中对属性初始化时,可以用 `lateinit` 修饰该属性。该修饰符只能用于在类体中的属性,而自 Kotlin 1.2 起,也用于顶层属性与局部变量。该属性或变量必须为`非空`类型,并且是`非原生类型`。 在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。自 1.2 起,可以该属性的引用上使用 `.isInitialized` 检测一个 lateinit var 是否已初始化。 ### 2.6 嵌套类与内部类 #### 2.6.1 内部类 下面是 Kotlin 中内部类的使用示例。在这个例子中,声明的内部类类似于 Java 中的非静态内部类,因此进行实例化的时候需要先获取到外部类的实例。 ```kotlin class Outer { private val bar: Int = 1 class Nested { fun foo() = 2 } inner class Inner { fun foo() = bar // 可以访问外部类变量 } } val demo = Outer.Nested().foo() // == 2 ``` 类可以标记为 `inner` 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用。使用 `inner` 修饰的类属于内部类,没有使用的属于嵌套类。所以,上面的 Nested 属于嵌套类,Inner 属于内部类。但是注意嵌套类和内部类的区别:嵌套类不是内部类,不包含对外部类的引用。所以,比如 Android 中常见的内存泄漏的问题就可以避免了。 #### 2.6.2 匿名内部类 匿名内部类也是我们开发过程中比较常用的定义方式,比如设置点击事件的回调的时候。匿名类的定义又分成下面两种方式: ```kotlin // 定义一个类 open class A(x: Int) { public open val y: Int = x } // 使用匿名内部类 window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { …… } override fun mouseEntered(e: MouseEvent) { …… } }) // 对函数式接口使用匿名内部类 val listener = ActionListener { println("clicked") } // 匿名内部类有多个超类的情况 val val ab: A = object : A(1), MouseAdapter { override val y = 15 } // 不适用任何类创建匿名类实例 val adHoc = object { var x: Int = 0 var y: Int = 0 } ``` 第一种方式适用于类中包含多个方法的情形,如上面的 MouseAdapter 的匿名类;另一种方式适用于函数式接口,即只有一个方法的接口,如 ActionListener。 如果一个类的超类型有一个构造函数,则必须传递适当的构造函数参数给它。多个超类型可以由跟在冒号后面的逗号分隔的列表指定,如 ab 的定义。 如果不想要明确创建哪种类型,而只是想创建一个匿名类实例,可以按按上面的 `adHoc` 那样定义。 #### 2.6.3 对象表达式 上面是 object 定义匿名类的几个示例,除此之外,它还可以用来定义单例类, ```kotlin object DataProviderManager { // 单例的方法 fun registerDataProvider(provider: DataProvider) { // …… } // 单例类的属性 val allDataProviders: Collection get() = // …… } // 调用单例类的方法 DataProviderManager.registerDataProvider(……) ``` 这种形式定义的单例在初始化的时候是线程安全的,它调用的时候有点类似于 Java 中静态类的方法和属性的调用。这些对象也可以有父类,它的实现方式与普通的类的继承并无二致。 注意:对象声明不能在局部作用域(即直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。 #### 2.6.4 伴生对象 类内部的对象声明可以用 companion 关键字标记的对象是伴生对象,它的使用效果类似于 Java 中的静态字段和静态方法。伴生对象也是可以实现基类和接口的。比如, ```kotlin class MyClass { companion object : Factory { override fun create(): MyClass = MyClass() } } // 调用的方式是:MyClass.create() ``` ### 2.7 继承 在 Kotlin 中所有类都有一个共同的超类 Any,类似于 Java 中的 Object,但是两者不同。 Kotlin 中的声明默认都是 `public final` 的,即公共且无法继承,如果希望一个类可以被继承,可以使用 open 关键字进行修饰。覆写函数的时候需要使用 override 关键字进行修饰,并且是必须的。属性的覆盖与函数的覆盖类似,都是使用 override 进行修饰。 ```kotlin // 基类,使用 open 关键字修饰 open class Base(p: Int) // 继承的时候调用基类的构造器 class Derived(p: Int) : Base(p) // 当基类有多个构造器的时候 class MyView : View { constructor(ctx: Context) : super(ctx) constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } ``` 如果派生类有一个主构造函数,其基类型可以(并且必须) 用基类的主构造函数参数就地初始化。 如果派生类没有主构造函数,那么每个次构造函数必须使用 super 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数。 另外, 1. 当在派生类的函数中调用父类函数的时候使用 `super.函数名` 即可,与 Java 一致。 2. 如果实现了多个接口,想要调某个父接口的实现,需要按照 `super<接口>.函数名` 的形式调用。 3. `abstract` 关键字的用法和 Java 一样,它同时具有 open 的语义。 4. **可见性修饰符**:总共有四个,即 private、 protected、 internal 和 public。修饰符 `internal` 表示模块内可见;`protected` 表示子类可见;`private` 表示类内可见,并且子类可见并不代表模块内可见,两个之间没有关系;public 表示没有任何限制,并且是默认级别。 5. 非静态内部类可以使用 `this@外部类名称` 访问外部类的方法和变量。 ### 2.8 接口 Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含实现: ```kotlin interface IClickable { fun onClick() fun defaulFun() { // 默认函数,不需要任何声明 println("I'm defaulFun().") } fun defaulFun2() { println("I'm defaulFun2().") } } ``` ### 2.9 数据类 专门用来存储数据的类,在普通类的基础之上使用 data 关键字修饰。系统会自动为我们的数据类生成:equals()、hashCode()、toString()、componentN() 和 copy() 函数的实现。 ```kotlin data class User(val name: String, val age: Int) ``` 数据类要求: 1. 主构造函数需要至少有一个参数 2. 主构造函数的所有参数需要标记为 val 或 var 3. 数据类不能是抽象、开放、密封或者内部的 4. (在1.1之前)数据类只能实现接口 上面的 `copy()` 函数类似于 Java 中的 `clone()` 函数,用来实现函数的克隆。 ### 2.10 密封类 密封类有点类似于枚举,要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。密封类不允许有非-private 构造函数(其构造函数默认为 private)。 ```kotlin sealed class Expr data class Const(val number: Double) : Expr() data class Sum(val e1: Expr, val e2: Expr) : Expr() object NotANumber : Expr() ``` 理解上,密封类的作用是类似于枚举,但是对类的位置进行了限制。这是为了让运用于 when 的子类能够更容易被发现。 ### 2.11 枚举类 声明枚举类的时候需要使用 enum 关键字,也可以给枚举增加一些属性,其定义方式基本同 Java. ```kotlin enum class City { BEIGING, SHANGHAI, GUANGZHOU } enum class City2(level:Int) { BEIGING(1), SHANGHAI(2), GUANGZHOU(3) } ``` ## 3、高阶特性 ### 3.1 Lambda 表达式 #### 3.1.1 Lambda 表达式的基本示例 Lambda 表达式的格式是:`{ x: Int, y: Int -> x + y }`。它的使用比较简单,通常用来定义函数式接口。如果变量的含义明确,它还可以进一步简化,比如 `{ it * it}` 也是可以的。 #### 3.1.2 集合与 Lambda 以下面的程序为例,我们可以在集合中使用 Lambda 表达式。在 Java 8 中,我们可以使用 Stream 进行编程,而 Android 中要求 API 24 以上才能使用 Stream,所以 Kotlin 可以帮助我们解决这个遗憾。 ```kotlin listOf(1,2,3,4).filter { it > 2 }.map { it.toString() }.all { it.length > 2 } ``` 它支持的操作符包括:`filter`, `map`, `all`, `any`, `count` 和 `find`, `groupBy`, `flatMap` 和 `flatten`。它们的用法和效果与 Stream 或者 RxJava 中的操作符的含义一致。 ### 3.1.3 with 与 apply `with` 表示以某个类作为开始,对其进行操作,最后返回。`apply` 对应于 with,表示对某个实例进行某种操作;(省去了声明一个实例的过程,仅此而已,但是新添加一个语法……) 它们的效果有点类似于在 Java 中的这种写法。 ```java new LinedList{ { add("A"); add("B"); } } ``` 也就是可以为声明的对象增加一些操作,但是这些过程都被包含在了 `with` 和 `apply` 中。参考下面的程序: ```kotlin // with fun getString() : String = with(LinkedList()) { for (i in 1..10) { this.add(i) // 这里的 this 就是上面传入的列表 } this.toString() } // apply fun getString2() : String = LinkedList().apply { for (i in 1..10) { add(i) } }.toString() ``` ### 3.2 区间 与区间相关的几个操作符是 `..`、`in`、`!in`、`until`、`downTo` 以及 `until`。其含义如下,Kotlin 中的区间默认是闭区间的: ```kotlin val nums = 1..10 // 输出结果是 1..10 println(nums) // 输出结果是 12345678910 for (num in nums) { print(num) } // 输出结果是 1086,10 递减到 5,步进 2 for (i in 10 downTo 5 step 2) { print(i) } // 输出结果是 12345,1 递增到 6,步进 1 for (i in 1 until 6) { print(i) } ``` ### 3.3 集合 Kotlin 中的集合比 Java 中的集合,增加了可变和不可变的概念。不可变集合的好处在于它的线程安全性(估计这个又是从 Guava 中借鉴来的概念)。在创建集合的时候,我们无需按照 Java 中使用 new 的方式来创建。在使用的时候还是要注意区分。下面我们来列举些这些集合, ![不可变集合的创建方法](res/QQ截图20190303112453.png) ![可变集合的创建方法](res/QQ截图20190303112506.png) Kotlin 中的不可变集合的一个好处是,它本身就不会提供插入和删除的方法,所以无需担心因为该方法没有实现而出现的运行时异常。 上面也说过,Kotlin 中的集合支持 Stream 的一些操作,除了上面的那些,它还支持许多其他的操作,这里就不一一列举了。 ### 3.4 类型系统 Kotlin 对空类型的处理比较好:默认所有的参数都是非空的,除非显式声明其可以为空。而 Java 中默认全部都是可空的。这可以有效帮助我们减少程序中的 NPE. ```kotlin fun testFun1(param : String) { print(param.length) } // 如果一个类是可空的那么必须显式声明,所以下面的程序编译器提示错误 fun testFun2(param : String?) { // print(param.length) } fun main(args : Array) { // testFun1(null) // 编译器提示错误 testFun2(null) val str : String? = null println(str?.length) // null println(str ?: "B") // B val b = "AA".let { it + "A" } // AAA println(b) } ``` 1. 使用 `?` 在类型的后面则说明这个变量是可空的。 2. 安全调用运算符 `?.`,以 `a?.method()` 为例,当 a 不为 null 则整个表达式的结果是 `a.method()` 否则是 null; 3. Elvis 运算符 `?:`,以 `a ?: "A"` 为例,当 a 不为 null 则整个表达式的结果是 a,否则是 "A"; 4. 安全转换运算符 `as?`,以 `foo as? Type` 为例,当 foo 是 Type 类型则将 foo 转换成 Type 类型的实例,否则返回 null; 5. 非空断言 `!!`,用在某个变量后面表示断言其非空,如 `a!!`; 6. `let` 表示对调用 let 的实例进行某种运算,如 `val b = "AA".let { it + "A" }` 返回 "AAA"。如果使用 let 的某个对象是可空的,那么只有当该对象非空的时候才会执行 let。 7. Kotlin 中进行类型之间的转换的时候必须显式进行,需要调用 `toXX()` 方法; 8. `Any` 和 `Any?` 分别是所有非空和空类型的超类; 9. `Unit` 相当于 Java 中的 void,返回 Unit 就相当于返回 void; ## 4、协程 常规的线程使用时,上下文切换会带来额外的性能开销。线程适用于 CPU 密集型的程序,而协程适合 Android 这种 IO 密集型的程序。从执行效果上面看,协程和线程达到的效果基本一致。它们的区别主要有以下几点: 1. 协程不需要进行同步控制; 2. 可以开大量的协程,但是线程数量是有限的,不然会影响程序的运行时性能; 3. 使用 GlobalScope 启动的协程像守护线程,当程序中的所有线程都结束的时候,整个程序结束,没有执行完毕的协程不会继续执行; 协程配置等相关信息:[kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) **挂起函数**:使用 suspend 修饰的函数,挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数能挂起协程。挂起函数挂起协程时,不会阻塞协程所在的线程,挂起函数执行完成后会恢复协程。所以,挂起函数只能在协程中或其他挂起函数中调用。 **CoroutineScope 和 CoroutineContext**:CoroutineScope 时协程本身,包含了 CoroutineContext。CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。 **CoroutineDispatcher**:协程调度器,决定协程所在的线程或线程池。指定协程运行于特定的一个线程、一个线程池或者不指定任何线程。有三种标准实现 Dispatchers.Default、Dispatchers.IO,Dispatchers.Main和Dispatchers.Unconfined,Unconfined 就是不指定线程。 **构建协程**:`CoroutineScope.launch {}` 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。`runBlocking {}`:创建一个新的协程同时阻塞当前线程,直到协程结束。`withContext {}` 不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。`async {}` 在后台创建一个新协程,跟 `CoroutineScope.launch {}` 的区别在于它有返回值。 ================================================ FILE: README.md ================================================ # Android [DEPRECATED] ## 1、目录 ### 基础开发 - 基础回顾 - [Android 基础回顾:Activity 基础](四大组件/Activity.md) - [Android 基础回顾:Fragment 基础](四大组件/Fragment.md) - [Android 基础回顾:Service 基础](四大组件/Service.md) - [Android 基础回顾:Broadcast 基础](四大组件/Broadcast.md) - 开发语言 - [Java 注解在 Android 中的应用](注解和依赖注入/注解在Android中的应用.md) - [Kotlin 基础知识梳理](Kotlin/Kotlin.md) - [在 Android 中使用 JNI 的总结](高阶技术/JNI技术总结.md) - 架构设计 - [Android 应用架构设计探索:MVC、MVP、MVVM和组件化](结构设计/探索Android架构设计.md) - [浅谈 ViewModel 的生命周期控制](高阶技术/浅谈ViewModel生命周期控制.md) - [浅谈 LiveData 的通知机制](高阶技术/浅谈LiveData的通知过程.md) - 性能优化 - [ANR](性能优化/Android性能优化-ANR.md) - [布局优化](性能优化/Android性能优化-布局优化.md) - [进程保活](性能优化/Android进程保活.md) - [启动优化](性能优化/Android性能优化-启动优化.md) - [内存优化](性能优化/Android性能优化-内存优化.md) - 开发环境 - [常见的 ADB 指令总结](开发工具/ADB_常见的ADB指令总结.md) - [常见的 Gradle 指令和配置总结](开发工具/Gradle_常见的指令和配置总结.md) - [常见的 Keytool 指令总结](开发工具/Keytool_常用的指令.md) ### 系统源码 - 核心流程 - [Android 系统架构](系统架构/Android系统架构.md) - [Android 系统启动流程源码分析](系统架构/Android系统启动过程.md) - [Android 应用打包过程](系统架构/Android打包过程.md) - [Android 应用安装过程](系统架构/Android应用安装过程.md) - 消息机制 - [Android 消息机制:Handler、MessageQueue 和 Looper](消息机制/线程通信:Handler、MessageQueue和Looper.md.md) - [Android IPC 机制:Binder 机制](消息机制/跨进程通信:Binder机制.md) - 异步编程 - [AsyncTask 的使用和源码分析](异步编程/AsyncTask源码分析.md) - [Android 多线程编程:IntentService 和 HandlerThread](异步编程/Android多线程编程:IntentService和HandlerThread.md) - 窗口机制 - [Android 的窗口管理机制](系统架构/窗口机制/Android的Window管理机制.md)(编辑中) - 控件体系 - [View 体系详解:View的工作流程](系统架构/控件体系/View体系详解:View的工作流程.md) - [View 体系详解:坐标系、滑动事件和分发机制](系统架构/控件体系/View体系详解:坐标系、滑动事件和分发机制.md) - [Android 动画体系详解](系统架构/控件体系/动画体系详解.md) - [SurfaceView 与 TextureView 的区别](系统架构/SurefaceView_and_TextureView.md) - 部分 API 源码 - [LruCache 的使用和源码分析](API简析/LruCache.md) ### 三方库源码 - 网络框架 - [网络框架 OkHttp 源码解析](网络访问/OKHttp源码阅读.md) - [网络框架 Retrofit 源码解析](网络访问/Retrofit源码阅读.md) - 图片加载框架 - [Glide 系列-1:预热、Glide 的常用配置方式及其原理](图片加载/Glide系列:Glide的配置和使用方式.md) - [Glide 系列-2:主流程源码分析](图片加载/Glide系列:Glide主流程源码分析.md) - [Glide 系列-3:Glide 缓存的实现原理](图片加载/Glide系列:Glide的缓存的实现原理.md) - RxJava - [RxJava2 系列-1:一篇的比较全面的 RxJava2 方法总结](响应式编程/RxJava2系列·_一篇的比较全面的RxJava2方法总结.md) - [RxJava2 系列-2:Flowable 和背压](响应式编程/Flowable和背压.md) - [RxJava2 系列-3:使用 Subject](响应式编程/用RxJava打造EventBus.md) - [RxJava2 系列-4:RxJava 源码分析](响应式编程/RxJava系列-4:RxJava源码分析.md) - 其他框架 - [消息机制 EventBus 源码解析](消息机制/EventBus的源码分析.md) - [Dagger 从集成到源码带你理解依赖注入框架](高阶技术/Dagger从集成到源码.md) ### Java 相关 - 并发编程 - [Java 并发编程:ThreadLocal 的使用及其源码实现](https://blog.csdn.net/github_35186068/article/details/83858944) - 设计模式 - [观察者模式](https://blog.csdn.net/github_35186068/article/details/83754026) - 虚拟机 - [内存管理](https://juejin.im/post/5b475e976fb9a04fa8671a45) - [虚拟机执行子系统](https://juejin.im/post/5b4a1fb7e51d4519213fd374) - [虚拟机内存模型与高效并发](https://juejin.im/post/5b4f48e75188251b1b448aa0) - 三方库 - [时间库 JodaTime](https://blog.csdn.net/github_35186068/article/details/83754146) ### UI 相关 - [自定义控件](系统架构/控件体系/View体系详解:自定义控件.md)(编辑中) ### 编程基础 - 数据库 - [MySQL 基础知识(全)](https://juejin.im/post/5a12d62bf265da431d3c4a01) ### 面试题 > 通过面试题梳理知识点细节 - [Android高级面试_1_Handler相关](笔试面试/Android高级面试_1_Handler相关.md) - [Android高级面试_2_IPC相关](笔试面试/Android高级面试_2_IPC相关.md) - [Android高级面试_3_语言相关](笔试面试/Android高级面试_3_语言相关.md) - [Android高级面试_4_虚拟机相关](笔试面试/Android高级面试_4_虚拟机相关.md) - [Android高级面试_5_四大组件、系统源码等](笔试面试/Android高级面试_5_四大组件、系统源码等.md) - [Android高级面试_6_性能优化](笔试面试/Android高级面试_6_性能优化.md) - [Android高级面试_7_三方库相关](笔试面试/Android高级面试_7_三方库相关.md) - [Android高级面试_8_热修补插件化等](笔试面试/Android高级面试_8_热修补插件化等.md) - [Android高级面试_9_网络基础](笔试面试/Android高级面试_9_网络基础.md) - [Android高级面试_10_跨平台开发](笔试面试/Android高级面试_10_跨平台开发.md) - [Android高级面试_11_JNINDK](笔试面试/Android高级面试_11_JNINDK.md) - [Android高级面试_12_项目经验梳理](笔试面试/Android高级面试_12_项目经验梳理.md) - [Android 中高级工程师面试题总结](笔试面试/Android高级软件工程师2017.md) ### 其他 - [马克笔记—Android 端开源的 Markdown 笔记应用](其他/MarkNote版本1的.md) - [承上启下:Markdown 笔记应用 MarkNote 的重构之路](其他/MarkNote版本2.md) ## 2、资源整理 ================================================ FILE: 其他/Android知识点随记.md ================================================ ## 异常处理 对于未捕获的异常,借助 Thread 的静态方法来进行处理 ```java public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { defaultUncaughtExceptionHandler = eh; } ``` ## multidex 这是因为安装应用时,有一步是使用 DexOpt 对 Dex 进行优化。这个过程会生成一个 ODex 文件,执行 ODex 的效率会比直接执行 Dex 文件的效率要高很多。在早期的 Android 系统中,DexOpt 把每一个类的方法 id 检索起来,存在一个链表结构里面。但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536 个。尽管在新版本的 Android 系统中,修复了 DexOpt 的这个问题,但是我们仍然需要对低版本的 Android 系统做兼容。 为了解决方法数超限的问题,需要启用 multidex 将该 dex文 件拆成多个。 ## 动态布局 就是指服务端使用 API 下发数据信息,然后客户端根据下发的信息进行动态布局。 另一层含义可能是在代码中进行动态布局而不是使用 XML 的方式。 ================================================ FILE: 其他/MarkNote版本1的.md ================================================ # 马克笔记—Android 端开源的 Markdown 笔记应用 ![App 导引](https://github.com/Shouheng88/MarkNote/blob/master/resources/images/app.png?raw=true) > 马克笔记是运行在Android设备上面的一款开源的Markdown笔记,它的功能开发得已经比较完善,已经能够满足大部分用户的需求。现在将其开源到Github上面,用来交流和学习。当然,更希望你能够参与到项目的开发当中,帮助马克笔记变得更加有用。 ## 1、关于马克笔记 马克笔记是一款开源的Markdown笔记应用,它的界面设计采用了Google最新的Material Design风格。该笔记现在的功能已经比较完善,能够满足用户大多数场景的需求。开源该软件的目的是希望与更多的人交流和学习,同时也希望能够有人参与到项目的开发中,一起帮助马克笔记,让它变得更加有用。 你可以通过加入[Google+社区](https://plus.google.com/u/1/communities/102252970668657211916)来关注该软件开发的最新动态,并且可以参与Beta测试。 马克笔记现在已经发布到了[酷安网](https://www.coolapk.com/apk/178276)上面,也欢迎你下载和使用该软件。另外,笔者还开发了一款清单应用[多功能清单](https://www.coolapk.com/apk/185660),感兴趣的同学也可以了解一下。 ## 2、应用展示图 这里是该应用的一些截图通过Photoshop调整之后得到的展示图,通过展示图,你大概可以了解一下该软件的主要功能和开发状态。在接下来的行文中,我会向你更详细地介绍它使用到的一些技术以及现在开发完成的一些功能和特性。 ## 3、功能和特性 我把该软件当前已经支持的功能列了一个清单: |编号|功能| |:-:|:-| |1|基本的**添加、修改、归档、放进垃圾箱、彻底删除**操作| |2|基本的Markdown语法,外加**MathJax**等高级特性| |3|特色的**时间线**功能,通过类似于AOP的操作记录用户的操作信息| |4|多种形式的媒体数据,包括**文件、视频、音频、图片、手写和位置信息**等| |5|**多主题**,支持**夜间主题**,并且有多种可选的**主题色和强调色**| |6|多彩的**图表**用于统计用户的数据信息| |7|三种形式的**桌面小控件**,并且可以为每个笔记添加快捷方式| |8|允许你为笔记指定多个多彩的标签| |9|使用“树结构”模拟文件夹操作,支持**多层文件夹**,并可以进行层级的搜索| |10|允许将笔记**导出为PDF、TXT、MD格式的文本、HTML和图片**| |11|使用**应用独立锁**,加强数据安全| |12|允许用户**备份数据到外部存储空间和OneDrive**| |13|图片**自动压缩**,节省本地的数据存储空间| 将来希望开发和完善的功能: |编号|功能描述| |:-:|:-| |1|数据同步,本地的文件管理容易导致多平台的不一致,增加同步服务,能够实现多平台操作| |2|文件服务器,用于获取图片和文件的链接| |3|富文本编辑,即时的编辑预览| |4|允许添加闹钟,并且复选框可以编辑| |5|添加地图来展示用户的位置信息的变更| 你可以从[更新日志](app/src/main/res/raw/changelog.xml)中获取到软件的更新信息。 ## 4、依赖和用到的一些技术 马克笔记用到了MVVM的设计模式,还用到了DataBinding等一系列技术。下面的表格中列出了用到的具体的依赖和简要的描述。在此,还要感谢这些开源项目的作者: |编号|依赖|描述| |:-:|:-|:-| |1|[arch.lifecycle]()|使用ViewModel+LiveData实现Model和View的解耦| |2|[Stetho](https://github.com/facebook/stetho)|Facebook开源的安卓调试框架| |3|[Fabric]()|错误跟踪,用户数据收集| |4|[RxBinding](https://github.com/JakeWharton/RxBinding)|| |5|[RxJava](https://github.com/ReactiveX/RxJava)|| |6|[RxAndroid](https://github.com/ReactiveX/RxAndroid)|| |7|[OkHttp](https://github.com/square/okhttp)|| |8|[Retrofit](https://github.com/square/retrofit)|| |9|[Glide](https://github.com/bumptech/glide)|| |10|[BRVAH](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|非常好用的Recycler适配器| |11|[Gson](https://github.com/google/gson)|| |12|[Joda-Time](https://github.com/JodaOrg/joda-time)|Java时间库| |13|[Apache IO](http://commons.apache.org/io/)|文件操作库| |14|[Material dialogs](https://github.com/afollestad/material-dialogs)|| |15|[PhotoView](https://github.com/chrisbanes/PhotoView)|| |16|[Hello charts](https://github.com/lecho/hellocharts-android)|| |17|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)|| |18|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)|| |19|[CircleImageView](https://github.com/hdodenhof/CircleImageView)|| |20|[Changeloglib](https://github.com/gabrielemariotti/changeloglib)|日志信息| |21|[PinLockView](https://github.com/aritraroy/PinLockView)|锁控件| |22|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)|底部弹出的对话框| |23|[Luban](https://github.com/Curzibn/Luban)|图片压缩| |24|[Flexmark](https://github.com/vsch/flexmark-java)|基于Java的Markdown文本解析| |25|[PrettyTime](https://github.com/ocpsoft/prettytime)|时间格式美化| 特别需要说明的一点是,马克笔记是在开发了一段时间之后重新引入的ViewModel,因为作者本人水平有限,或者对ViewModel理解不够深入,设计难免有不足的地方,还请批评指正。 ### 数据库操作 对于数据库部分,笔者自己设计了一套数据的访问逻辑,这里使用到了模板和单例等设计模式。它的好处在于,当你想要向程序中添加一个数据库实体的时候,只需要很少的配置即可,可以省去很多的样板代码。而且,由于该项目的一些特殊需求,比如要记录统计信息等,所以就自己设计了一下。当然,可能性能上仍然有许多值得提升的地方,但笔者认为仍不失为一个简单的学习材料。 ### Markdown解析 对于Markdown解析,可以使用js在webview里面解析,也可以像本项目一样在程序种用java进行解析。笔者认为使用Flexmark在java种解析的好处是更方便地对解析的功能进行拓展。如该软件中的MathJax的解析就是在Flexmark的基础上进行的拓展。 ## 5、参与项目 正如一开始提及的那样,马克笔记仍然有许多不足,我希望可以有更多的人帮助马克笔记继续完善它的功能。当然,这并不勉强。如果你希望对该项目贡献代码,你可以fork该项目,并向该项目提交请求。你可以在[waffle.io](https://waffle.io/Shouheng88/NotePal)上面跟踪issue的开发状态。或者,你发现了该软件中存在的一些问题,你可以在issue中向开发者报告。如果有其他的需求,可以直接通过[邮箱](mailto:shouheng2015@gmail.com)邮件开发者。 ## 6、项目地址 因为这篇文章是从Github的Readme文件中拷贝出来的,所以忘记加上Github地址了,抱歉。现在补上:[Github](https://github.com/Shouheng88/MarkNote) ================================================ FILE: 其他/MarkNote版本2.md ================================================ # 承上启下:Markdown 笔记应用 MarkNote 的重构之路 ## 1、关于项目 **MarkNote** 是一款 Android 端的笔记应用,它支持非常多的 Markdown 基础语法,还包括了 MathJax, Html 等各种特性。此外,你还可以从相机或者相册中选择图象并将其添加到自己的笔记中。这很酷!因为你可以将自己的游记或者其他图片拍摄下来并将其作为自己笔记的一部分。这也是笔者开发这款软件的目的——希望 MarkNote 能够成为一款帮助用户记录自己生活的笔记应用。 下面是我自己制作的一张部分功能预览图。这里仅仅列举了其中的部分页面,当然,你可以在酷安网或者 Google Play Store 上面获取到这个应用程序,并进一步了解它的全部功能,也可以在 Github 上得到最新版的应用的全部源代码。 ![预览图](https://camo.githubusercontent.com/bea0cac9b6a352211c658024615419a785bc7fe6/68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a553137333943454b3259667241726a376651546230672e6a706567) 项目相关的**链接**: 1. [酷安网下载链接:https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276) 2. [Google Play Store 下载:https://play.google.com/store/apps/details?id=me.shouheng.notepal](https://play.google.com/store/apps/details?id=me.shouheng.notepal) 3. [Github 项目链接:https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote) **最后**,之所以把这次重构称为 “承上启下” 的一个很重要的原因是:这次重构代码其实是为了后续功能的开发铺路。在未来,我会为这个应用增加更多有趣的功能。如果你对该项目感兴趣的话,可以 **Star** 或者 **Fork** 该项目,并为项目贡献代码。我们欢迎任何的、即使很小的贡献 :) ## 2、关于重构 在之前的版本中,MarkNote 在功能、界面和代码方面都存在一些不足,所以,前些日子我又专门抽了些时间对这些不足的地方进行了一些优化,时间大概从 11 月中旬直到 12 月中旬。这次重构也进行了大量的代码优化。经过这次重构,项目增加了大概 100 多次 commit. 下面我们列举一下本次重构所涉及的部分,其实也是这段时间以来学习到的东西的一些总结。 ### 2.1 项目结构优化 #### 2.1.1 包结构优化 首先,在之前笔者已经对项目的整个结构做了一次调整,主要是将项目中各个模块的位置进行了调整。这部分内容主要是项目中的 Gradle 配置和项目文件的路径的修改。在 `settings.gradle` 里面,我按照下面的方式指定了依赖的各个模块的路径: include ':app', ':commons', ':data', ':pinlockview', ':fingerprint' project(':commons').projectDir = new File('../commons') project(':data').projectDir = new File('../data') project(':pinlockview').projectDir = new File('../pinlockview') project(':fingerprint').projectDir = new File('../fingerprint') 这种方式最大的好处就是,项目中的 `app`, `commons`, `data` 等模块的文件路径处于相同的层次中,即: --MarkNote |----client |----commons |----data .... 这个调整当然是为了组件化开发做准备啦,当然这样的结构相比于将各个模块全部放置在 `client` 下面清晰得多。 其次,我将项目中已经比较成熟的部分打包成了 `aar`,并直接引用该包,而不是继续将其作为一个依赖的形式。这样又进一步简化了项目的结构。 最后是项目中的功能模块的拆分。在之前的项目中,Markdown 编辑器和解析、渲染相关的代码都被我放置在项目所引用的一个模块中。而这次,我直接将这个部分拆成了一个单独的项目并将其开源到了 Github. ![EasyMark](res/easymark.png) 这么做的主要目的是: 1. 将核心的功能模块从项目中独立出来单独开发,以实现更多的功能并提升该部分的性能; 2. 开源,希望能够帮助想实现一个 Markdown 笔记的开发者快速集成这个功能; 3. 开源,希望能够有开发者参与进行以提升这部分的功能。 关于 Markdown 处理的部分被开源到了 Github,其地址是:https://github.com/Shouheng88/EasyMark ,该项目中同时还包含了一个非常好用的编辑器菜单控件,感兴趣的同学可以关注一下这个项目。 #### 2.1.2 MVVM 调整 在该项目中,我们一直使用的是最新的 MVVM 设计模式,只是可惜的是在之前的版本中,笔者对 MVVM 的理解不够深入,所以导致程序的结构更像是 MVP. 本次,我们对这个部分做了优化,使其更符合 MVVM 设计原则。 以笔记列表界面为例,当我们获取了对应于 Fragment 的 ViewModel 之后,我们统一在 `addSubscriptions()` 方法中对其通知进行订阅: viewModel.getMutableLiveData().observe(this, resources -> { assert resources != null; switch (resources.status) { case SUCCESS: adapter.setNewData(resources.data); getBinding().ivEmpty.showEmptyIcon(); break; case LOADING: getBinding().ivEmpty.showProgressBar(); break; case FAILED: ToastUtils.makeToast(R.string.text_failed); getBinding().ivEmpty.showEmptyIcon(); break; } }); 这里返回的 resources,是封装的 `Resource` 的实例,是用来向观察者传递程序执行结果的包装类。然后,我们会使用 ViewModel 的 `fetchMultiItems()` 方法来根据之前传入的页面的状态信息拉取笔记记录: public Disposable fetchMultiItems() { if (mutableLiveData != null) { mutableLiveData.setValue(Resource.loading(null)); } return Observable.create((ObservableOnSubscribe>) emitter -> { List multiItems = new LinkedList<>(); List list; if (category != null) { switch (status) { case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(category);break; case TRASHED: list = TrashHelper.getNotebooksAndNotes(category);break; default: list = NotebookHelper.getNotesAndNotebooks(category); } } else { switch (status) { case ARCHIVED: list = ArchiveHelper.getNotebooksAndNotes(notebook);break; case TRASHED: list = TrashHelper.getNotebooksAndNotes(notebook);break; default: list = NotebookHelper.getNotesAndNotebooks(notebook); } } for (Object obj : list) { if (obj instanceof Note) { multiItems.add(new NotesAdapter.MultiItem((Note) obj)); } else if (obj instanceof Notebook) { multiItems.add(new NotesAdapter.MultiItem((Notebook) obj)); } } emitter.onNext(multiItems); }).observeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(multiItems -> { if (mutableLiveData != null) { mutableLiveData.setValue(Resource.success(multiItems)); } }); } 从上面也可以看出,我们将从数据库中获取到数据的许多逻辑放在了 ViewModel 中,并且每当想要拉取数据的时候调用一下 `fetchMultiItems()` 方法即可。这样,我们可以大大地减少 View 层的代码量。 View 层的逻辑也因此变得清晰得多。 ### 2.2 界面优化:更纯粹的质感设计 记得在 Material Design 刚推出的时候,笔者和许多其他开发者一样兴奋。不过,在实际的开发过程中我却总是感觉不得要领,总觉少了一些什么。不过,经过前段时间的学习,我对在应用中实现质感设计有了更多的认识。 #### 2.2.1 Toolbar 的阴影效果 在之前的版本中,为了实现工具栏下面的阴影效果,我使用了在 Toolbar 下面增加一个高度为 `5dp` 的控件并为设置一个渐变背景的实现方式。这种实现方式可以完美兼容 Android 系统的各个版本。但是,这种实现的效果没有系统自带的显得那么自然。在新的版本中,我使用了下面的方式来实现阴影的效果: ... 这里的 `SupportAppBarLayout` 继承自支持包的 `AppBarLayout`,主要用来实现日夜间主题的兼容。这样 Toolbar 下面就会带有一个漂亮的阴影,但是在比较低版本的手机上面是没有效果的,所以,为了兼容低版本的手机还要使用之前的那种使用控件填充的方式。(在新版本中暂时没有做这个处理) #### 2.2.2 日夜间主题兼容 在之前的项目中,支持 20 多种主题颜色和强调色,不过最近随着 Google 在自己的项目中逐渐采用纯白色的设计,我也抛弃了之前的逻辑。现在整个项目中只支持三种主题: 1. 白色的主题 + 蓝色的强调色 2. 白色的主题 + 粉红的强调色 3. 黑色的主题 + 蓝色的强调色 ![主题](res/themes.jpg) 对于主题的支持,我依然延续了之前的实现方式——通过重建 Activity 来实现主题的切换。同时,为了达到某些控件随着主题自适应调整的目的,我定义了一些自定义控件,并在其中根据当前的设置选择使用的颜色。而对于其他可以直接使用项目中的强调色或者主题色的部分,我们可以直接使用当前的主题的值,比如下面的 Toolbar 的背景颜色会使用当前主题中的 `主题色`: #### 2.2.3 启动页优化 之前的版本中在第一次打开程序的时候会有一个启动页来展示程序的功能,新版本中直接移除了这个功能。取而代之的是使用启动页来进行优化,首秀定义一个主题。这个主题只应用于第一次打开的 Activity。 这里,我们将界面的背景更换成我们自己的项目的图标,因为项目图标中使用的颜色与状态栏的颜色不一致,所以,这里又重写了 `colorPrimaryDark` 属性以将状态栏的颜色和启动页的颜色设置成相同的效果: 这种实现方式的效果是,在程序打开的时候不会存在白屏。之前的白屏会被我们指定的启动页替换掉(因为这个启动页是该 Activity 的窗口的背景)。当然,当页面打开完毕之后你还要在程序中将启动页背景替换掉。这样优化之后程序打开的时候显得更加自然、流畅。 #### 2.2.4 动画优化 因为时间的原因,在当前的版本中,我并没有加入太多的动画,而只是对程序中的一些地方增加了动画的效果。 在笔记的列表中,我使用了下面的动画效果。这样当打开列表界面的时候各个条目会存在自底向上的进入动画。 private int lastPosition = -1; @Override protected void convert(BaseViewHolder helper, MultiItem item) { // ... /* Animations */ if (PalmUtils.isLollipop()) { setAnimation(helper.itemView, helper.getAdapterPosition()); } else { if (helper.getAdapterPosition() > 10) { setAnimation(helper.itemView, helper.getAdapterPosition()); } } } private void setAnimation(View viewToAnimate, int position) { if (position > lastPosition) { Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.anim_slide_in_bottom); viewToAnimate.startAnimation(animation); lastPosition = position; } } 不过,这种方式实现的并不是最理想的效果,因为当打开页面的时候,多条记录会以一个整体的形式进入到页面中。这也是以后的一个优化的地方。 ### 2.3 使用 RxJava 重构 在之前的项目中,当进行异步的操作的时候,需要定义一个 `AsyncTask`. 这种实现方式存在一个明显的问题,当需要执行的异步任务比较多,又无法进行复用的时候,你需要定义大量的 `AsyncTask`。另外,在各个页面之间进行数据传递的时候,如果单纯地使用 `onActivityResult()` 或者进行接口回调(Fragment 和 Activity 之间)会使得代码繁琐、难以阅读。针对这些问题,我们可以使用 RxJava 来进行很好的优化。 首先是异步操作的问题,我们可以使用 RxJava 来实现线程的切换。以下面的这段代码为例,它被用来实现保存`快速笔记`的结果到文件系统和数据库中。在这段代码中,我们使用了 RxJava 的 `create()` 方法,并在其中进行逻辑的处理,然后使用 `subscribeOn()` 方法指定处理的线程是 IO 线程,并使用 `observeOn()` 方法指定最终处理的结果在主线程中进行处理: public Disposable saveQuickNote(@NonNull Note note, QuickNote quickNote, @Nullable Attachment attachment) { return Observable.create((ObservableOnSubscribe) emitter -> { /* Prepare note content. */ String content = quickNote.getContent(); if (attachment != null) { attachment.setModelCode(note.getCode()); attachment.setModelType(ModelType.NOTE); AttachmentsStore.getInstance().saveModel(attachment); if (Constants.MIME_TYPE_IMAGE.equalsIgnoreCase(attachment.getMineType()) || Constants.MIME_TYPE_SKETCH.equalsIgnoreCase(attachment.getMineType())) { content = content + "![](" + quickNote.getPicture() + ")"; } else { content = content + "[](" + quickNote.getPicture() + ")"; } } note.setContent(content); note.setTitle(NoteManager.getTitle(quickNote.getContent(), quickNote.getContent())); note.setPreviewImage(quickNote.getPicture()); note.setPreviewContent(NoteManager.getPreview(note.getContent())); /* Save note to the file system. */ String extension = UserPreferences.getInstance().getNoteFileExtension(); File noteFile = FileManager.createNewAttachmentFile(PalmApp.getContext(), extension); try { Attachment atFile = ModelFactory.getAttachment(); FileUtils.writeStringToFile(noteFile, note.getContent(), Constants.NOTE_FILE_ENCODING); atFile.setUri(FileManager.getUriFromFile(PalmApp.getContext(), noteFile)); atFile.setSize(FileUtils.sizeOf(noteFile)); atFile.setPath(noteFile.getPath()); atFile.setName(noteFile.getName()); atFile.setModelType(ModelType.NOTE); atFile.setModelCode(note.getCode()); AttachmentsStore.getInstance().saveModel(atFile); note.setContentCode(atFile.getCode()); } catch (IOException e) { emitter.onError(e); } /* Save note. */ NotesStore.getInstance().saveModel(note); emitter.onNext(note); }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(note1 -> { if (saveNoteLiveData != null) { saveNoteLiveData.setValue(Resource.success(note1)); } }); } 另外是界面之间的结果传递的问题。对于 `onActivityResult()` 的执行结果,我们使用自定义的 `RxBus` 来传递信息,它的作用类似于 `EventBus`。然后,我们为此而封装了一个 `RxMessage` 对象来包装返回的结果。但是在程序中,我们尽量来简化和减少这种代码,因为过多的全局消息会让代码调试变得更加困难。我们希望代码逻辑更加简单、清晰。 RxJava 除了能够完成线程切换的任务之外,对代码的可读性的提升效果也是非常明显的。另外,它还非常适用于局部的优化,比如,我们可以很轻易地改变自己的代码来将某个耗时逻辑放在异步线程中执行来提升界面的响应速度。 ### 2.4 增加新功能 #### 2.4.1 桌面快捷方式 桌面快捷方式并不是所有的 Android 桌面都支持的,我们在程序中有两个地方使用它。如下图所示,第一种方式是在笔记内部点击创建快捷方式的时候在桌面创建应用的快捷方式,我们可以通过点击快捷方式来快速打开笔记;第二种方式是长按应用图标的时候弹出一个菜单选项。 ![快捷方式](res/shortcuts.jpg) 首先,第一种实现方式是在 7.0 之后加入的,之前我们也是可以创建快捷方式的,只是实现的方式与现在的方式不同而已。如下面这段代码所示,当 7.0 之后,我们使用 ShortcutManager 来创建快捷方式。之前,我们可以使用 "com.android.launcher.action.INSTALL_SHORTCUT" 这个 ACTION 并指定参数来创建快捷方式: public static void createShortcut(Context context, @NonNull Note note) { Context mContext = context.getApplicationContext(); Intent shortcutIntent = new Intent(mContext, MainActivity.class); shortcutIntent.putExtra(SHORTCUT_EXTRA_NOTE_CODE, note.getCode()); shortcutIntent.setAction(SHORTCUT_ACTION_VIEW_NOTE); if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) { ShortcutManager mShortcutManager = context.getSystemService(ShortcutManager.class); if (mShortcutManager != null && VERSION.SDK_INT >= VERSION_CODES.O) { if (mShortcutManager.isRequestPinShortcutSupported()) { ShortcutInfo pinShortcutInfo = new Builder(context, String.valueOf(note.getCode())) .setShortLabel(note.getTitle()) .setLongLabel(note.getTitle()) .setIntent(shortcutIntent) .setIcon(Icon.createWithResource(context, R.drawable.ic_launcher_round)) .build(); Intent pinnedShortcutCallbackIntent = mShortcutManager.createShortcutResultIntent(pinShortcutInfo); PendingIntent successCallback = PendingIntent.getBroadcast(context, /* request code */ 0, pinnedShortcutCallbackIntent, /* flags */ 0); mShortcutManager.requestPinShortcut(pinShortcutInfo, successCallback.getIntentSender()); } } else { createShortcutOld(context, shortcutIntent, note); } } else { createShortcutOld(context, shortcutIntent, note); } } private static void createShortcutOld(Context context, Intent shortcutIntent, Note note) { Intent addIntent = new Intent(); addIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); addIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, note.getTitle()); addIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(context, R.drawable.ic_launcher_round)); addIntent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); context.sendBroadcast(addIntent); } 对于第二种实现方式,我们可以在 Manifest 文件中进行注册,并为其指定 ACTION 和启动类来实现各个选项被点击之后发送的事件。然后,我们在指定的 Activity 中对各个 ACTION 进行处理即可,具体可以参考源代码。另外,这里的快速创建笔记还是比较有意思的,可以打开一个背景透明的 Activity 并在其中弹出一个自定义对话框来快速编辑笔记。可以帮助我们快速地记录自己的笔记。 #### 2.4.2 指纹解锁 当然,这部分功能,我们直接使用了一个开源的三方库。毕竟人家为还为各个系统的指纹解锁的支持做了处理,所以这里我们直接奉行拿来主义了。这个项目的地址是:https://github.com/uccmawei/FingerprintIdentify. #### 2.4.3 打开网页的各种问题 打开网页当然不难实现,我们使用一个自定义的 WebView 即可实现。不过,在这个项目的重构版本中,我们采用了一个开源的库 AgentWeb,它可以满足我们非常多场景的应用。 另外,因为在我们的新的重构版本中,将支持包和 targetApi 都提升到了 28,所以出现了一个问题:使用 `http` 的网页无法打开。为了解决这个问题,我们需要在 Manifest 文件中指定网络配置文件的地址: android:networkSecurityConfig="@xml/network_security_config" 然后,在该配置文件中指定我们可以访问的 http 白名单: mikecrm.com m.weibo.cn 在这里我们还发现了一个其他的问题:我们打开网页的时候设置的 Weibo 的链接是 https 的,但是因为我们在移动设备上面使用,所以又被重定向到了 `http://m.weibo.cn`,导致我们的网页无法打开。解决的方式即按照上面那样,将重定向之后的地址添加到白名单之中即可。 #### 2.4.4 其他 1. 在新的版本中,为了帮助我们进一步优化程序,我们使用了友盟进行埋点。 2. 不注册支付宝和微信支付账号进行打赏; 3. 分享相关的逻辑等; 4. 其他:新版本中我们还增加了许多其他的逻辑,如果你感兴趣的话可以查看下代码。 ## 3、总结 上面我们介绍了项目的一些内容和新版本重构时加入的新功能等。这些新加入的东西也算是这段时间以来学习成果的一个小集合。当然,因为毕竟业余时间有限,代码中可能仍然存在一些不足和设计不良的地方,如果你发现了这些不愉快的问题,可以在 Github 上面为项目提 issue,很乐意与你沟通和学习! 最后,重申一下项目相关的链接: 1. [酷安网下载链接:https://www.coolapk.com/apk/178276](https://www.coolapk.com/apk/178276) 2. [Google Play Store 下载:https://play.google.com/store/apps/details?id=me.shouheng.notepal](https://play.google.com/store/apps/details?id=me.shouheng.notepal) 3. [Github 项目链接:https://github.com/Shouheng88/MarkNote](https://github.com/Shouheng88/MarkNote) ------ **如果您喜欢我的文章,可以在以下平台关注我:** - 博客:[https://shouheng88.github.io/](https://shouheng88.github.io/) - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) 更多文章:[Gihub: Android-notes](https://github.com/Shouheng88/Android-notes) ================================================ FILE: 其他/计算机视觉与Android.md ================================================ # Android 与计算机视觉 不管你是否从事计算机视觉相关的工作,了解这方面的内容总是好的,因为即使你现在的工作与 AI 无关,采用一些开放的 API 仍然有可能让你的应用做得更好。比如,百度开发平台就提供了许多 AI 相关的 API,像当下比较受欢迎的“白描”等应用,其实就是使用了百度的 API。所以,你也可以考虑一下能否借助一些语音和文字识别等功能来赋能自己的应用。 因为我们所做的计算机视觉的东西更多的是对图片进行处理,这就涉及到 OpenCV 和 Tensorflow 在 Android 端的应用,以及相机和 Android 端的其他图片处理逻辑。这不可避免地要用到 JNI 和 NDK 当中的一些内容。因此,在本篇文章中,我们想要讨论的内容主要包括以下几个方面的: 1. **Android 端图片压缩库封装** 2. **Android 端相机库封装和性能优化** 3. **JNI 和 NDK 调用,以及 CMake 在 Android 中的应用** 4. **OpenCV 在 Android 中的集成和应用** 5. **Tensorflow 在 Android 端的集成和应用** 其实之前的文章中我们也提到过一些今天我们想讨论的内容。所以在这里相关的技术底层的知识能带过的就直接带过。我们会给出相关的技术文章的链接,如果感兴趣的可以到指定的文章下面查看更具体的知识。 ## 1、Android 端图片压缩库封装 为什么要做图像压缩呢?因为太大的图片上传速度比较慢,会影响程序的用户体验;而过分的压缩图片会导致程序识别出来的效率比较低。识别的效率每提高 1 个百分点,标注团队可能就要多标注几万张图片。经过测试发现,把图片的短边控制在 1100 左右是最合适的,那么此时我们就需要制定一个自己的压缩策略。 这个在之前的文章中我们已经讨论过,并且对 Android 端 Bitmap 相关的压缩的知识都做了介绍。您可以到下面的文章下面了解下我们是如何对图片压缩库进行封装的,以及 Android 中图片压缩的底层原理: [开源一个 Android 图片压缩框架](https://juejin.im/post/5c87d01f6fb9a049b7813784) 当然,上面的文章在介绍的这方面的东西的时候,基于的是我们库的第一个版本,那个版本可以满足基本的功能。在后来的版本中,我们又对自己的库做了完善,增加了更多的 feature。这里我们主要介绍下新的框架相关的 API 以及后来我们如何做了兼容性的设计,以在第一个版本的基础之上进行了功能性的拓展。 在实际的使用过程中,我们发现更多的时候你需要对 Bitmap 进行处理而不是 File. 在这个时候,第一个版本的库就应用不上了。想了想,我们希望能够对自己的库进行拓展以支持更多的应用场景。这包括: 1. **在当前线程中直接获取压缩的 Bitmap 而不是通过 Callback 或者 RxJava 的形式传递结果**:因为我们有一部分代码本身就是在 RxJava 中异步执行的,回调或者使用 RxJava 会影响我们程序的逻辑结构。 2. **直接使用 Bitmap 或者 `byte[]` 作为参数进行压缩而不是先写入到 File 中,然后对文件进行读取和压缩**:这个是存在具体的应用场景,比如当你从相机当中获取数据的时候,实际获取到的是 `byte[]`,在连续拍摄的情况下,不断写入到文件再读取并进行压缩非常影响程序性能。 3. **支持直接返回 Bitmap 而不是只能返回 File 类型**:有时候我们需要对程序的局部做优化,比如图片处理结果的预览,此时,如果我们返回的是 File 的话,一样会影响我们程序的性能和逻辑结构。 最初,我们希望能够像 Glide 那样支持对自定义的数据结构进行压缩,然后自定义图片获取的逻辑,然而考虑到时间和兼容的问题,直接放弃了这个想法,转而采用更加简单、直接的方式: 1. **对入参这一块,直接使用重载函数接受不同的参数类型**; 2. **压缩的过程可以直接使用 `get()` 方法把压缩的中间结果返回给调用者**; 3. **增加 `asBitmap()` 方法,转换输出参数类型为 Bitmap 而不是 File 类型**。 因此,在后来的版本中,你可以像下面这样直接获取到 Bitmap 的结果: ```java Bitmap result = Compress.with(this, bytes) .setQuality(80) .strategy(Strategies.compressor()) .setScaleMode(ScaleMode.SCALE_SMALLER) // 指定放缩的边 .setMaxHeight(1100f) .setMaxWidth(1100f) .asBitmap() // 调用这个方法表示期望的返回类型是 Bitmap 而不是 File .asFlowable() .subscribe(b -> { // do somthing }) ``` 这里关于 `asBitmap()` 方法的设计可以简单说明一下。 ![Compressor 设计图](res/Compressor.png) > 图片链接:[https://www.processon.com/view/link/5cdfb769e4b00528648784b7](https://www.processon.com/view/link/5cdfb769e4b00528648784b7]) 在第一个版本中,我们采用的上图中第一个图的设计。在这里两种压缩策略均继承自 AbstractStrategy,其实上述的 `strategy()` 方法,你可以把它理解成**拐了一个弯**,也就是它返回的具体的策略,你下面能调用的方法都局限在具体的策略中。 在后来的设计中要在 `asBitmap()` 方法处返回一个具体的构建者继续按照返回 Bitmap 的逻辑进行处理。此时我们直接返回的是第二张图中的 BitmapBuilder 对象,而 Abstrategy 则依然按照返回 File 类型的逻辑走。这样我们可以轻易地在原来的基础上,通过**拐一个弯**的形式把后续的构建逻辑转移到了 BitmapBuilder 对象上面。同时,为了达到代码复用的目的,我们引入了泛型的 `RequestBuilder`。这样 AbstractStrategy 和 BitmapBuilder 只需要实现它,并指定各自的资源类型即可。又因为按照之前的逻辑,我们一直在构建的都是 AbstractStrategy 对象,因此,我们只需要把 AbstractStrategy 作为参数传入到具体的 RequestBuilder 里面就可以从它上面直接获取 Bitmap 了。(Bitmap 是在之间串联的“通用货币”。)这样我们既复用了大量的代码又在兼容原始版本的基础上进行了功能的拓展,妙极! ## 2、Android 相机库封装和性能优化 对于一个 ToC 的应用来说,用户体验直观重要。按照我们的业务场景,如果使用拍照识别的效率比人工操作的效率还要低的话,那么人工智能似乎就没有存在的必要了。毕竟我们的目标是提升别人的工作效率,所以在相机这块就必须做到快速响应。 在项目的初期我们使用的是 Google 开源的 [CameraView](https://github.com/google/cameraview). 然而在实际的应用过程中,我们逐渐发现这个库存在大量不好的设计,影响了我们程序的应用性能: ![相机启动 TraceView 分析](https://user-gold-cdn.xitu.io/2019/4/23/16a4ac1ac0d8a697?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) > 配图是我们使用 TraceView 对程序执行过程进行的性能分析 1. **没必要的数据结构构建,影响相机启动速率**:首先,当从相机的属性当中读取相机支持的尺寸的时候它会使用这些参数构建一个尺寸的宽高比到尺寸列表的哈希表结构。然后具体的运算的时候从这个哈希表中读取尺寸再进行计算。这样设计很不好!因为当需要计算尺寸的时候遍历一遍尺寸列表可能并不会占用太多的时间,并且构建的哈希表结构使用并不频繁,而在相机启动阶段进行不必要的计算反而影响了相机的启动速率。 2. **打开相机的操作在主线程当中执行,影响界面响应速率**:前提是界面能够快速响应用户,即使打开的是一个黑色的等待界面也比按下没有响应更容易接受。通过 TraceView 我们发现相机 `open()` 的过程大概占用了相机启动速率的 25%。因此把这个方法的调用放在主线程中是不太合适的。 3. **相机不支持视频拍摄和预览**:这个库是不支持相机的视频拍摄和预览的。毕竟作为计算机视觉的一部分的实时处理也是很重要的一部分。就算当前的项目中没有这方面的功能,我们也应该考虑对这方面的功能进行支持。(这方面的内容基本就是 OpenGL + Camera) 于是乎,我们自己开发的一款相机库就诞生了。当然当初开发的一个原因也是希望能够支持 OpenGL。只是时间太有限,暂时还没有太多时间去关注这些问题: ![相机库设计 UML 建模图](https://user-gold-cdn.xitu.io/2019/4/23/16a4aae65580a62c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) > 图片链接:[https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049](https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049) 关于这个库,我只是把它所有的逻辑实现了一遍,并且在我的手机上面调试没有什么问题。如果具体应用到生存环境当中还需要更多的测试和验证。关于 Android 相机开发的知识,主要覆盖 Camera1 和 Camera2 两块内容。一个方法的实现逻辑看懂了,其他方法的实现与之类似,具体的内容可以参考项目的源码。因为本人当前时间和精力有限,所以暂时无法详细讲解相机 API 的使用。 我们可以简单概括一下这份设计图当中的一些内容: 1. **三种主要设计模式**: 1. **门面模式**:考虑到兼容 Camera1 和 Camera2 的问题,我们需要对外提供统一的 API 调用,所以,我们考虑了使用门面模式来做一个统一的封装。这里定义了 Camera1Manager 和 Camera2Manager 两个实现,分别对应于两种不同的相机 API. 它们统一继承自 CameraManager 这个门面接口。这种设计模式的好处是对外是统一的,这样结合具体的工厂+策略模式,我们可以让用户在 Camera1 和 Camera2 之间自由选择。 2. **策略模式+工厂模式**:因为考虑到各种不同应用场景的兼容,我们希望能够用户提供最大的自由度。所以,我们采用了策略的方式,对外提供了接口给用户来计算最终想要得到的相机尺寸等参数。所以这里我们定义了一个名为 `ConfigurationProvider` 的类,它是一个单例的类,除了负责获取相机参数的计算策略,同时肩负着内存缓存的责任。这样对于很多参数的计算,包括预览尺寸、照片尺寸、视频尺寸等可以让用户自由来指定具体的大小。 2. **三个主要的优化点**: 1. **内存缓存优化**:实际上作为相机属性的相机所支持尺寸等信息是不变的,使用内存缓存缓存这些数据之后下次就无需再次获取并进行处理,这样可以在下次相机启动的时候显著提升程序响应的速率。 2. **延迟初始化,不使用不计算**:为了提升程序的响应速率,我们甚至对数字的计算也进行了优化,当然这个优化点可能效果没有那么明显,但是如果你愿意精益求精的话,这也可以当作一个优化点。目前程序里面还是使用了浮点数进行计算,在早期对于作为哈希表映射的键的字段,我们甚至直接使用移位预算。当然这种优化的效果还要取决于整体的数据量,并且数据量越大的时候优化效果越明显。 3. **异步线程优化**:在早期的版本中,我们使用的是私有锁进行线程优化。因为要把线程的 `open()` 和设置参数放在两个线程当中进行,因此不可避免地要遇到线程安全问题。而所谓的私有锁其实就类似于 `Collections.syncXXX()` 所返回的同步容器。其实就是对容器的每个方法进行加锁。这样虽然可以解决问题,但是程序的结构不好看。所以,后来的版本中,我们直接使用 HandlerThread 来进行异步的调用。所谓的 HandlerThread,顾名思义,就是 Handler 和 Thread 的结合。 (更多的内容可以参考:[Android 相机库开发实践](https://juejin.im/post/5cbf2667f265da037875980a)) ## 3、JNI 和 NDK 调用,以及 CMake 在 Android 中的应用 在之前我们想在在 Java 或者 Android 中调用 C++ 代码是比较复杂的。这需要进行动态或者静态的注册。对于静态注册的方式,你需要一步一步地进行编译;对于动态注册的方式,你需要把方法一个个地在 native 层进行注册。不过后来有了 CMake 之后一切都变得简单了。当然对于 CMake,如果做过 native 的同学肯定不会陌生。对于一般应用层开发的同学,其实也可以了解下它。因为,有了它之后你可以很容易地把你的一部分实现逻辑放在 native 层里,而 native 层相对于 Java 层比较安全,而且借助 C++ 和 NDK 你可以做出更多有趣的东西。 要在 Android 端使用 CMake 而是很简单的,你需要首先在 AS 里面安装下相关的 SDK 工具: ![CMake 开发环境搭建](https://user-gold-cdn.xitu.io/2019/3/2/1693c6828071d2a6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 然后,你需要在 Gradle 里面做简单的配置: ![CMake gradle 配置](https://user-gold-cdn.xitu.io/2019/3/2/1693c83b5585c77b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 当然,虽然我们这样将其来容易,但是进行配置的时候可能需要很多的相关的专业知识。 这里面会配置到一个 CMake path,它就是指向的 CMake 的配置文件 `CMakeLists.txt`。通常我们程序中要用到的一些三方的 native 库就需要在这个地方进行配置。比如下面的就是之前项目里面的 CMake 的配置。这里面配置了一些 OpenCV 的库以及我们自己的代码所在的位置,并且引用了 NDK 里面的一些相关的库: ```cmake # 设置要求的 CMake 的最低版本 cmake_minimum_required(VERSION 3.4.1) # 指定头文件的目录 include_directories(opencv/jni/include src/main/cpp/include ../../common) add_library(opencv_calib3d STATIC IMPORTED) add_library(opencv_core STATIC IMPORTED) # .... #if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a) # add_library(tbb STATIC IMPORTED) #endif() set_target_properties(opencv_calib3d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_calib3d.a) set_target_properties(opencv_core PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_core.a) set_target_properties(opencv_features2d PROPERTIES IMPORTED_LOCATION ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libopencv_features2d.a) # .... add_library(everlens SHARED src/main/cpp/img_proc.cpp src/main/cpp/img_cropper.cpp src/main/cpp/android_utils.cpp ../../common/EdgeFinder.cpp ../../common/ImageFilter.cpp) find_library(log-lib log) find_library(jnigraphics-lib jnigraphics) if(EXISTS ${PROJECT_SOURCE_DIR}/opencv/libs/${ANDROID_ABI}/libtbb.a) target_link_libraries( my_library opencv_stitching opencv_features2d # .... ${log-lib} ${jnigraphics-lib}) else() target_link_libraries( my_library opencv_stitching opencv_features2d # .... ${log-lib} ${jnigraphics-lib}) endif() ``` 对于 CMake 的一些指令,之前也进行过一些总结,并且对指定了官方文档的地址。如果想要了解的话,可以到文章下面了解更多的内容: [常用的 CMake 指令总结](https://blog.csdn.net/github_35186068/article/details/88639757) 使用 CMake 的好处主要是 AS 支持得比较好: 1. 可以根据 native 层代码到 Java 层代码之间的关系,鼠标左键 + Ctrl 即可直接完成 native 层方法和 Java 层方法之间的跳转; 2. 无需进行繁琐的动态注册和静态注册,只需要在 CMake 和 Gradle 当中进行配置,可以把注意力更多地放在自己的代码的逻辑实现上。 当然,就算使用了 CMake 有时候还是需要了解一些 JNI 中动态注册的内容,因为有时候当你在 native 层中从 Java 层传入的对象上面获取信息的时候还是需要进行动态注册。比如, ```C++ #include #include #include // 定义一个结构体及实例 gPointInfo static struct { jclass jClassPoint; jmethodID jMethodInit; jfieldID jFieldIDX; jfieldID jFieldIDY; } gPointInfo; // 初始化 Class 信息,注意下映射关系是如何表达的,其实就类似于反编译之后的注释 static void initClassInfo(JNIEnv *env) { gPointInfo.jClassPoint = reinterpret_cast(env -> NewGlobalRef(env -> FindClass("android/graphics/Point"))); gPointInfo.jMethodInit = env -> GetMethodID(gPointInfo.jClassPoint, "", "(II)V"); gPointInfo.jFieldIDX = env -> GetFieldID(gPointInfo.jClassPoint, "x", "I"); gPointInfo.jFieldIDY = env -> GetFieldID(gPointInfo.jClassPoint, "y", "I"); } // 动态注册,在这里初始化 extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv *env = NULL; if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) { return JNI_FALSE; } initClassInfo(env); return JNI_VERSION_1_4; } ``` (更多的内容可以参考:[在 Android 中使用 JNI 的总结](https://juejin.im/post/5c79f5d0518825347a56275f)) ## 4、OpenCV 在 Android 中的集成和应用:图片裁剪以及透视变换 ### 4.1 关于 OpenCV 的集成 当然,不引用 OpenCV 的 C++ 库,直接使用别人封装好的 Java 库也是可以的,这取决于具体的应用场景。比如,如果你不需要实现特别复杂的功能,只需要简单的图像处理即可,那么别人包装过的 Java 库已经可以完全满足你的需求。但如果你像我们一样,本身需要包装和编译来自算法同学的 C++ 算法,甚至还需要使用 OpenCV 的拓展库,那么使用 Java 包装后的库可能无法满足你的需求。 下面是 OpenCV 及其拓展库的 Github 地址: - [OpenCV](https://github.com/opencv/opencv) - [OpenCV-contrib](https://github.com/opencv/opencv_contrib) 有了这些库你还是无法直接将其应用到程序当中的。因为上述项目得到的是 OpenCV 的源码,主要是源代码以及一些头文件,还需要对它们进行编译然后再应用到自己的项目当中。 [Build OpenCV 3.3 Android SDK on Mac OSX](https://chaoyang.nz/post/build-opencv-android-sdk/) 当然也有一些已经编译完成的 OpenCV 及其拓展库,我们可以在 CMake 中配置之后直接将其引用到我们的项目中: [opencv3-android-sdk-with-contrib](https://github.com/chaoyangnz/opencv3-android-sdk-with-contrib) 所以最终项目的结构如下: ![OpenCV 集成之后的项目结构](res/QQ截图20190718000949.png) 左边圈出的部分是 OpenCV 及 CMake 的一些配置,右边是封装之后的 Java 方法。 ### 4.2 关于 OpenCV 的应用 OpenCV 可以用来处理做很多图片处理的工作,很多工作是使用 Android 原生的 Bitmap 无法完成的。比如,图片不规则裁剪之后的透视变换、灰度化处理等。其实,不论你在 native 层如何对图片进行处理,在 Android 当中,对 native 层的输入和 native 层的输出都是 Bitmap. 而 `OpenCV::Mat` 就像是 native 层图片处理的通用货币。所以,一个完整的图片处理的流程大致是: 1. Step1: Java 层的 Bitmap 转换成 native 层的 Mat; 2. Step2: 使用 Mat 进行图片处理; 3. Step3: 将 native 层的 Mat 转换成 Java 层的 Bitmap 并返回。 将 Java 层的 Bitmap 转换成 native 层的 Mat 你可以使用下面的方法: ```C++ #include #include #include "android_utils.h" void bitmap_to_mat(JNIEnv *env, jobject &srcBitmap, Mat &srcMat) { void *srcPixels = 0; AndroidBitmapInfo srcBitmapInfo; try { // 调用 AndroidBitmap 中的方法获取 bitmap 信息 AndroidBitmap_getInfo(env, srcBitmap, &srcBitmapInfo); AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels); uint32_t srcHeight = srcBitmapInfo.height; uint32_t srcWidth = srcBitmapInfo.width; srcMat.create(srcHeight, srcWidth, CV_8UC4); // 根据 bitmap 的格式构建不同通道的 Mat if (srcBitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) { Mat tmp(srcHeight, srcWidth, CV_8UC4, srcPixels); tmp.copyTo(srcMat); cvtColor(tmp, srcMat, COLOR_RGBA2RGB); } else { Mat tmp = Mat(srcHeight, srcWidth, CV_8UC2, srcPixels); cvtColor(tmp, srcMat, COLOR_BGR5652RGBA); } AndroidBitmap_unlockPixels(env, srcBitmap); return; } catch (cv::Exception &e) { AndroidBitmap_unlockPixels(env, srcBitmap); // 构建一个 Java 层的异常并将其抛出 jclass je = env->FindClass("java/lang/Exception"); env -> ThrowNew(je, e.what()); return; } catch (...) { AndroidBitmap_unlockPixels(env, srcBitmap); jclass je = env->FindClass("java/lang/Exception"); env -> ThrowNew(je, "unknown"); return; } } ``` 这里主要是先从 Bitmap 中获取图片具体的信息,这里调用了 NDK 里面的图像相关的一些方法。然后利用得到的图片尺寸信息和颜色信息构建 OpenCV 里面的 Mat. Mat 就类似于 MATLAB 里面的矩阵,它包含了图像的像素等信息,并且也提供了类似于 `eye()`, `zeros()` 等类似的方法用来构建特殊的矩阵。 将 Bitmap 转换成 Mat 之后就是如何使用它们了。下面是一份用来对图片进行裁剪和透视变换的算法: ```C++ // 将 Java 层的顶点转换成 native 层的 Point 对象 static std::vector pointsToNative(JNIEnv *env, jobjectArray points_) { int arrayLength = env->GetArrayLength(points_); std::vector result; for(int i = 0; i < arrayLength; i++) { jobject point_ = env -> GetObjectArrayElement(points_, i); int pX = env -> GetIntField(point_, gPointInfo.jFieldIDX); int pY = env -> GetIntField(point_, gPointInfo.jFieldIDY); result.push_back(Point(pX, pY)); } return result; } // 裁剪并且透视变化 extern "C" JNIEXPORT void JNICALL Java_xxxx_MyCropper_nativeCrop(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray points_, jobject outBitmap) { std::vector points = pointsToNative(env, points_); if (points.size() != 4) { return; } // 取出四个顶点 Point leftTop = points[0], rightTop = points[1], rightBottom = points[2], leftBottom = points[3]; // 获取源图和结果图对应的 Mat Mat srcBitmapMat, dstBitmapMat; bitmap_to_mat(env, srcBitmap, srcBitmapMat); AndroidBitmapInfo outBitmapInfo; AndroidBitmap_getInfo(env, outBitmap, &outBitmapInfo); int newHeight = outBitmapInfo.height, newWidth = outBitmapInfo.width; dstBitmapMat = Mat::zeros(newHeight, newWidth, srcBitmapMat.type()); // 将图片的顶点放进集合当中,用来调用透视的方法 std::vector srcTriangle, dstTriangle; srcTriangle.push_back(Point2f(leftTop.x, leftTop.y)); srcTriangle.push_back(Point2f(rightTop.x, rightTop.y)); srcTriangle.push_back(Point2f(leftBottom.x, leftBottom.y)); srcTriangle.push_back(Point2f(rightBottom.x, rightBottom.y)); dstTriangle.push_back(Point2f(0, 0)); dstTriangle.push_back(Point2f(newWidth, 0)); dstTriangle.push_back(Point2f(0, newHeight)); dstTriangle.push_back(Point2f(newWidth, newHeight)); // 获取一个映射的转换矩阵 Mat transform = getPerspectiveTransform(srcTriangle, dstTriangle); warpPerspective(srcBitmapMat, dstBitmapMat, transform, dstBitmapMat.size()); // 将 Mat 转换成 Bitmap 输出到 Java 层 mat_to_bitmap(env, dstBitmapMat, outBitmap); } ``` 算法最终的输出结果:
透视变化和图像切割
## 5、Tensorflow 在 Android 端的集成和应用:图片边缘检测 在之前对图片的边缘进行检测的时候,因为发现 OpenCV 算法效果不太理性,所以后来选择使用 TensorFlow 对图片进行边缘检测。这就涉及到在 Android 端集成 Tensorflow Lite。前段时间也看到爱奇艺的 SmartVR 的介绍。借助一些官方的资料,在 Android 端使用 TF 并不难。在 Tensorflow 的开源仓库中已经有一些 Sample 可供参考: - [Tensorflow github repository](https://github.com/tensorflow/tensorflow) - [Tensorflow lite offical](https://www.tensorflow.org/lite) 做边缘检测当然有些大材小用的味道,但是对于我们 Android 开发者来说,借这个机会了解如何在 Android 端集成一些 Tensorflow 也可以拓展一下。毕竟这种东西属于当下比较热门的东西,说不定哪天心血来潮自己训练一个模型呢 :) 在 Android 端引入 Tensorflow 并不复杂,只需要添加相关的仓库以及依赖: ```groovy allprojects { repositories { jcenter() maven { url 'https://google.bintray.com/tensorflow' } } } dependencies { // ... // tensorflow api 'org.tensorflow:tensorflow-lite:0.0.0-nightly' } ``` 困难的地方在于如何对训练的模型的输入和输出进行处理。因为所谓的模型,你可以将其理解成锻炼出来的一个函数,你只需要按照要求的输入,它就可以按照固定的格式给你返回一个输出。所以,具体的输入和输出是什么样的还要取决于锻炼模型的同学。 在我们之前开发的时候,最初是训练模型的同学使用 Python 代码调用的模型。使用 Python 虽然代码简洁,但是对于客户端开发简直就是噩梦。因为像 [NumPy](https://numpy.org/) 和 [Pillow](https://pillow.readthedocs.io/en/stable/) 这种函数库,一行代码的任务,你可能要“翻译”很久。后来,我们使用的是 `C++ + OpenCV` 的形式。对于 iOS 开发,因为他们可以使用 Object-C 与 C++ 混编,所以比较轻松。对于 Android 开发则需要做一些处理。 下面是加载模型以及在调用 Tensorflow 之前在 Java 层所做的一些处理: ```java public class TFManager { private static TFManager instance; private static final float IMAGE_MEAN = 128.0f; private static final float IMAGE_STD = 128.0f; private Interpreter interpreter; private int[] intValues; private ByteBuffer imgData; private int inputSize = 256; public static TFManager create(Context context) { // DCL if (instance == null) { synchronized (TFManager.class) { if (instance == null) { instance = new TFManager(context); } } } return instance; } // 从 Assets 中加载模型,用来初始化 TF private static MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename) throws IOException { AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename); FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor()); FileChannel fileChannel = inputStream.getChannel(); long startOffset = fileDescriptor.getStartOffset(); long declaredLength = fileDescriptor.getDeclaredLength(); return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength); } // 初始化 TF private TFManager(Context context) { try { interpreter = new Interpreter(loadModelFile(context.getAssets(), "Model.tflite")); interpreter.setNumThreads(1); interpreter.resizeInput(0, new int[]{1, 256, 256, 3}); intValues = new int[inputSize * inputSize]; imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3); imgData.order(ByteOrder.nativeOrder()); } catch (IOException e) { e.printStackTrace(); } } // 边缘识别 public EdgePoint[] recognize(Bitmap bitmap) { long timeStart = System.currentTimeMillis(); imgData.rewind(); Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true); scaledBitmap.getPixels(intValues, 0, scaledBitmap.getWidth(), 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight()); for (int i = 0; i < inputSize; ++i) { for (int j = 0; j < inputSize; ++j) { int pixelValue = intValues[i * inputSize + j]; // 取 RBG 做归一化处理,结果在 -1 到 1 之间 imgData.putFloat((((pixelValue >> 16) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // R imgData.putFloat((((pixelValue >> 8) & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // G imgData.putFloat(((pixelValue & 0xFF) - IMAGE_MEAN) / IMAGE_STD); // B } } LogUtils.d("----------TFManager prepare imgData cost : " + (System.currentTimeMillis() - timeStart)); timeStart = System.currentTimeMillis(); Map outputMap = new HashMap<>(); outputMap.put(0, new float[1][256][256][5]); Object[] inputArray = {imgData}; // 调用 TF 进行识别 interpreter.runForMultipleInputsOutputs(inputArray, outputMap); // 对识别的结果进行处理,主要是对图片的像素进行处理 float[][][][] arr = (float[][][][]) outputMap.get(0); int[][] colors = new int[5][256 * 256]; for (int i=0; i<5; i++) { for (int j=0; j<256; j++) { for (int k=0; k<256; k++) { colors[i][j*256 + k] = (int) (arr[0][j][k][i] * 256); } } } LogUtils.d("----------TFManager handle TF result cost : " + (System.currentTimeMillis() - timeStart)); timeStart = System.currentTimeMillis(); // 将得到的图片像素按照固定的格式交给 native 层继续进行边缘识别 EdgePoint[] points = ImgProc.findEdges(bitmap, colors); LogUtils.d("----------TFManager" + Arrays.toString(points)); LogUtils.d("----------TFManager find edges cost : " + (System.currentTimeMillis() - timeStart)); return points; } } ``` 这里程序的主要执行流程是: 1. 从 Assets 的模型文件中获取打开输入流,然后从输入流中打开一个管道,这里用到了 NIO 中的一些类。然后,从管道中获取一个字节缓存区。文件读写的时候管道直接与缓冲区进行交互。除了作为一个缓存区,这个缓存区还具有内存映射的功能。类似于 mmap 吧,主要是为了提升文件读写的效率。 2. 初始化并配置 Tensorflow,上面的一些参数用来设置线程数量等信息,比较简单。后面等一些参数主要用来按照模型等要求对 TF 进行调整。比如,我们使用模型来判断图片的顶点的时候使用的是只包含 RGB 三个纬度的 256 * 256 的图片。所以,这里使用了下面几行代码来进行设置: ```java // inputSize = 256; interpreter.resizeInput(0, new int[]{1, 256, 256, 3}); intValues = new int[inputSize * inputSize]; // 256 * 256 的图片,3 个纬度,四张图 imgData = ByteBuffer.allocateDirect(inputSize * inputSize * 4 * 3); ``` 3. 对要识别对图片进行处理。这里需要先对图片进行放缩,将其控制到 256 * 256 的大小。然后,使用 Bitmap 的方法获取图片的像素。下面的几行代码是对图片对 RBG 三种色彩提取,并分别对其进行归一化处理。处理之后对结果统一写入到 imgData 当中,作为模型对输入。 4. 按照模型对输出对文件的格式构建一个 Java 对象作为模型的输出参数。调用模型的方法进行识别。 5. 对模型对输出结果进行处理。根据我们上述定义对模型输出 `new float[1][256][256][5]`,这里实际对含义是 256 * 256 的 5 张图片。因为模型输出的数据并不是原始的像素信息,所以需要乘以 256 来得到真正的图片的像素。最后就是使用这些像素以及 Bitmap 的方法来得到最终的 Bitmap. 上面调用完了模型但是整个流程还没有结束。因为只是调用模型得到了五张识别之后的图片。这五张图片就是只留下了图片边缘的边框,所以想要得到图片的顶点还需要继续对这五张图进行处理。这部分需要一些算法,虽然在 Java 层去判断也是可以的,但是在 native 层,借助 OpenCV 的一些库可以使整个过程更加简单。因此,这里又要涉及一个 JNI 调用: ```C++ extern "C" JNIEXPORT void JNICALL Java_com_xxx_ImgProc_nativeFindEdges(JNIEnv *env, jclass type, jintArray mask1_, jintArray mask2_, jintArray mask3_, jintArray mask4_, jintArray mask5_, jobject origin, jobjectArray points) { // 从 Java 中传入的数组元素 jint *mask1 = env->GetIntArrayElements(mask1_, NULL); jint *mask2 = env->GetIntArrayElements(mask2_, NULL); jint *mask3 = env->GetIntArrayElements(mask3_, NULL); jint *mask4 = env->GetIntArrayElements(mask4_, NULL); jint *mask5 = env->GetIntArrayElements(mask5_, NULL); // 原图转换成 Native 层的 mat Mat originMat; bitmap_to_mat(env, origin, originMat); // 构建一个集合 std::vector jints; jints.push_back(mask1); jints.push_back(mask2); jints.push_back(mask3); jints.push_back(mask4); jints.push_back(mask5); // 从像素点中得到对应的 Mat 并将其放到一个集合当中 std::vector masks; for (int k = 0; k < 5; ++k) { Mat mask(256, 256, CV_8UC1); for (int i = 0; i < 256; ++i) { for (int j = 0; j < 256; ++j) { mask.at(i, j) = (char)(*(jints[k] + i * 256 + j)); } } masks.push_back(mask); } try { // 调用算法进行边缘检测 EdgeFinder finder = ImageEngine::EdgeFinder( originMat, masks[0], masks[1], masks[2], masks[3], masks[4]); vector retPoints = finder.FindBorderCrossPoint(); // 将得到的“点”转换成 Java 层的对象 jclass class_point = env->FindClass("com/xxx/EdgePoint"); jmethodID method_point = env->GetMethodID(class_point, "", "(FF)V"); // 将顶点组成一个 Java 数组返回 for (int i = 0; i < 4; ++i) { jobject point = env->NewObject(class_point, method_point, retPoints[i].x, retPoints[i].y); env->SetObjectArrayElement(points, i, point); } } catch (cv::Exception &e) { jclass je = env->FindClass("java/lang/Exception"); env -> ThrowNew(je, e.what()); return; } catch (...) { jclass je = env->FindClass("java/lang/Exception"); env -> ThrowNew(je, "unknown"); return; } // 释放资源 env->ReleaseIntArrayElements(mask1_, mask1, 0); env->ReleaseIntArrayElements(mask2_, mask2, 0); env->ReleaseIntArrayElements(mask3_, mask3, 0); env->ReleaseIntArrayElements(mask4_, mask4, 0); env->ReleaseIntArrayElements(mask5_, mask5, 0); } ``` 这里的主要逻辑是把之前得到的像素以及原始图片的 Bitmap 统一传入到 native 层,然后这些像素中得到 OpenCV 对应的 Mat,再一起作为算法的参数调用算法来得到顶点信息。最后把得到的顶点信息映射成 Java 层的类,并将其放在数组中返回即可。 从上面的流程中也可以看出,整个调用流程实际上进行了多次的 JNI 调用: 1. 调用 Bitmap 的方法本身就是一次 JNI 调用,调用了 Android 底层的 Skia 的库来实现图片处理; 2. TF-Lite 本身就是对 native 层方法的一个封装,调用其方法也涉及到 JNI 调用; 3. 最后是对模型识别的结果进行处理,这也涉及 JNI 调用。 JNI 调用的时候需要进行额外的转换操作,需要在函数开始的时候把 Java 层的对象转换成 native 层的对象,在算法调用完毕之后再将 native 层的对象转换成 Java 层的对象。这是我们以后可以优化的一个地方。 ## 总结 以上就是计算机视觉在 Android 中的应用,主要涉及 JNI 的一些内容,以及 OpenCV 和 Tensorflow 的一些应用。再这之前介绍了图片压缩和相机库的一些封装,如果你的程序中需要一些图片处理的功能的话,我想这些东西肯定是对你有用的 :D ------- 关注作者获取更多知识: - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) - 博客:[https://shouheng88.github.io/](https://shouheng88.github.io/) 更多知识请参考 [Github, Android-notes](https://github.com/Shouheng88/Android-notes)。 ================================================ FILE: 响应式编程/RxJava系列-4:RxJava源码分析.md ================================================ # RxJava 系列-4:RxJava 源码分析 在之前的文章中我们介绍了 RxJava 2 的常用的 API 的方法总结、背压的概念以及 RxJava 2 在项目中的实际应用。在本节中,我们将要对 RxJava 2 的源码进行分析。下面是之前文章的一些链接,如果对 RxJava 2 的使用比较感兴趣,你可以通过下面的文章进行学习: - [RxJava2 系列-1:一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd) - [RxJava2 系列-2:背压和 Flowable](https://juejin.im/post/5b759b9cf265da283719d187) - [RxJava2 系列-3:使用 Subject](https://juejin.im/post/5b801dfa51882542cb409905) 下面我们就从 RxJava 2 的一个简单的示例来分析下 RxJava 2 是的主流程、设计模式以及 RxJava 2 是如何实现线程切换的。 ## 1、RxJava 的主流程源码分析 下面是 RxJava 的一个非常典型的使用示例,在该示例中,我们在 IO 线程中执行业务逻辑,在主线程中对执行的结果进行后续的处理。 ```java Disposable disposable = Observable.create(new ObservableOnSubscribe() { @Override public void subscribe(ObservableEmitter emitter) throws Exception { // 在这里执行业务逻辑 emitter.onNext(new Object()); } }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Consumer() { @Override public void accept(Object o) throws Exception { // 在主线程中进行后续的处理 } }); disposable.dispose(); ``` 我们将这段程序分成四个阶段来进行分析:1). 调用 `create()` 方法的执行过程;2). 调用 `subscribeOn(Schedulers.io())` 和 `observeOn(AndroidSchedulers.mainThread())` 实现线程切换的过程;3). 使用 `subscribe()` 进行订阅的工程;4). 调用 `dispose()` 方法取消订阅的过程。 下面先来看第一个阶段的执行过程。 ### 1.1 create() 和 subscribe() 方法的执行过程 下面是调用了 `create()` 方法之后的执行过程,在下面的代码中,我们省略了 null 的检测相关的逻辑。在当前的小节中,我们假设没有指定线程切换相关的逻辑。也就是调用了 `create()` 之后,紧接着调用了 `subscribe()` 方法。 对于 RxJavaPlugins 的静态方法,比如 `onAssembly()` 等,暂时我们先不考虑它的用途。你可以将其看作直接将传入的参数的值返回。比如下面的 `create()` 方法将返回 `ObservableCreate` 的实例。 ```java public static Observable create(ObservableOnSubscribe source) { // 看作直接返回了 new ObservableCreate(source) 即可 return RxJavaPlugins.onAssembly(new ObservableCreate(source)); } public final class ObservableCreate extends Observable { final ObservableOnSubscribe source; public ObservableCreate(ObservableOnSubscribe source) { this.source = source; } @Override protected void subscribeActual(Observer observer) { // 对传入的观察者进行包装 CreateEmitter parent = new CreateEmitter(observer); // 调用观察者的订阅回调方法 observer.onSubscribe(parent); try { // 真正执行订阅的地方 source.subscribe(parent); } catch (Throwable ex) { Exceptions.throwIfFatal(ex); parent.onError(ex); } } static final class CreateEmitter extends AtomicReference implements ObservableEmitter, Disposable { final Observer observer; CreateEmitter(Observer observer) { this.observer = observer; } @Override public void onNext(T t) { if (t == null) { onError(new NullPointerException("onNext ...")); return; } if (!isDisposed()) { // 调用传入的观察者的 onNext() 方法 observer.onNext(t); } } @Override public void dispose() { // 取消订阅 DisposableHelper.dispose(this); } // ... } // ... } ``` 上面是第一个阶段的执行过程。这里我们省去了一些代码,只保留了比较具有代表性的一些方法。也许你现在还对这部分代码看得云里雾里,没关系,看了下面的内容你会慢慢理解。 接下来我们看下当调用了 `subscribe()` 方法之后的处理。 ```java public final Disposable subscribe(Consumer onNext, Consumer onError, Action onComplete, Consumer onSubscribe) { // 将三种类型的观察者回调统一包装到 LambdaObserver 方法中 LambdaObserver ls = new LambdaObserver(onNext, onError, onComplete, onSubscribe); subscribe(ls); return ls; } public final void subscribe(Observer observer) { try { // 看作直接返回 observer 即可 observer = RxJavaPlugins.onSubscribe(this, observer); // 调用了 subscribeActual() 方法 subscribeActual(observer); } catch (NullPointerException e) { throw e; } catch (Throwable e) { Exceptions.throwIfFatal(e); RxJavaPlugins.onError(e); throw new NullPointerException("Actually not, but can't throw other exceptions due to RS"); } } ``` 上面的这些方法都定义在 Observable 中,区别只在于调用的对象。所以,为了更清晰地分析这个过程,我们使用大写字母来进行分析: 首先,整体的执行过程是, ```java D = Observalbe.create(S).subscribe(X,Y,Z); ``` 它可以被拆解成下面的两个步骤来分析(下面是一份伪代码,只是按照时间的调用顺序来排序的): ```java A = Observable.create(S); D = A.subscribe(X,Y,Z); ``` 然后,调用 A 的 `subscribe()` 方法的时候,实际上会调用到 Observable 的 `subscribe()` 方法(就是上面的代码)。所以,按照调用的过程,上面的伪代码将变成下面这个样子, ```java A = Observable.create(S) O = LambdaObserver(X,Y,Z) D = A.subscribe(O) A.subscribeActual(O) ``` 于是我们可以得知,当调用了 `subscribe()` 方法的时候,实际上调用了 A 的 `subscribeActual()` 方法,并将 B 作为参数传入。B 是 LambdaObserver,由我们调用 `subscribe()` 的时候传入的三个参数组成。那么 A 呢?回到之前的 `create()` 代码中,我们得知它就是 `ObservableCreate` 的实例。这里会调用到它的 `subscribeActual()` 方法。按照字母表示的方式,该方法将会成为下面这个样子, ```java @Override protected void subscribeActual(O) { P = new CreateEmitter(O); O.onSubscribe(P); S.subscribe(P); } ``` 这里的 S 是由 ObservableCreate 的构造方法传入的,也就是我们在 `create()` 方法中传入的对象。首先,这里会将 O 作为构造方法的参数传入到 `CreateEmitter` 实例中。然后,回调 O 的 `onSubscribe()` 方法并将 P 传出。这是我们常用的 RxJava 的回调方法之一。第三步中,我们调用了 S 的 `subscribe()` 方法并将 P 传出。所以,当我们按照示例代码的方式调用下面这行代码的时候, ```java emitter.onNext(new Object()); ``` 实际上是调用了这里的 P 的方法。那么,我们来看 P 的 `onNext()` 方法, ```java @Override public void onNext(T t) { O.onNext(t); } ``` 它通过调用 O 的 `onNext()` 方法实现。所以,到头来,其实还是回调了我们的在 `subscribe()` 方法中传入的 Consumer 的方法。这样就通过回调的方式把我们发送的值,传递给了我们的观察方法。 ### 1.2 `dispose()` 方法的执行过程 上面分析了 `create()` 和 `subscribe()` 方法的主流程。那么 `dispose()` 方法呢? 按照上面给出的代码,它的定义如下。也就是通过 `DisposableHelper` 的 `dispose()` 方法来最终完成取消订阅。 ```java @Override public void dispose() { DisposableHelper.dispose(this); } ``` `DisposableHelper` 的 `dispose()` 方法的定义如下。按照上面的分析,`dispose()` 的时候传入的 this 就是 CreateEmitter. 并且它是继承了 `AtomicReference` 的。 ```java public static boolean dispose(AtomicReference field) { Disposable current = field.get(); Disposable d = DISPOSED; if (current != d) { current = field.getAndSet(d); if (current != d) { if (current != null) { current.dispose(); } return true; } } return false; } ``` 对 AtomicReference,相比大家都不陌生,它是一个原子类型的引用。这里正式通过对该原子类型引用的赋值来完成取消订阅的——通过一个原子操作将其设置为 DISPOSED. ### 1.3 RxJava 执行过程的总结 上面我们总结了 RxJava 的 Observable 从 `create()` 到 `subscribe()` 到 `dispose()` 方法的执行过程。虽然,我们依靠自己的逻辑能够把整个流程梳理下来,但是这太笨拙了。除了掌握了整个流程,我想我们更应该分析下它使用的设计思想。 一开始,当我们分析到上面的流程的时候,我也是云里雾里,但是当我继续分析了 `subscribeOn()` 的时候才恍然大悟——它整体的设计使用的设计模式和 Java 中的流是一致的。在真正分析 `subscribeOn()` 之前,我们先来看下它的代码, ```java public final Observable subscribeOn(Scheduler scheduler) { return RxJavaPlugins.onAssembly(new ObservableSubscribeOn(this, scheduler)); } public final class ObservableSubscribeOn extends AbstractObservableWithUpstream { final Scheduler scheduler; public ObservableSubscribeOn(ObservableSource source, Scheduler scheduler) { super(source); this.scheduler = scheduler; } @Override public void subscribeActual(final Observer s) { final SubscribeOnObserver parent = new SubscribeOnObserver(s); s.onSubscribe(parent); parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent))); } // ... } ``` 对比一下 `subscribeOn()` 方法和 `create()` 方法,我们可以很容易地发现,它们的逻辑几乎是一致的。都是传入了一个 ObservableSource 之后对其进行包装,然后在 `subscribeActual()` 方法中,得到一个 parent,然后调用 `onSubscribe()` 继而进行后续处理……也就是它和 Java 的 IO 体系一样,都使用了**装饰者设计模式**。 在 Java 的 IO 体系中,我们经常可见下面的代码。 ```java InputStream is = new FileInputStream(fileToCopy); BufferedInputStream bis = new BufferedInputStream(is, 15 * 1024); DataInputStream dis = new DataInputStream(bis); ``` 这里的 FileInputStream 是节点流,用来打开磁盘上面的输入流。后续的 BufferedInputStream 和 DataInputStream 都用来对节点流进行修饰。它们各自只需要完成自己的功能,前者主要负责缓存以提升读取速率,后者用来将得到的流转换成我们需要的数据类型。如果我们由其他的需求只需要在这个链的基础上实现一个自定义的装饰器即可。 回想一下我们在实际的开发过程中是不是经常使用链式来调用一大串,中间的各个环节分别来实现自己的功能,比如转换、过滤、统计等等。使用了装饰者模式之后,链的每个环节只需要实现自己的功能,使用者可以根据自己的需求在链上面增加环节。所以,类似于转换、过滤、统计等等,每个类的责任变得单一了,从整个调用链上面解耦出来。真是不得不佩服 RxJava 的这种设计! 知道了 RxJava 的整体使用的是装饰者设计模式,我们理解其它的一些特性来就容易得多。按照装饰者设计模式的思路,RxJava 的包装过程和调用 `subscribe()` 方法之后的回调过程将如下所示: ![RxJava 的包装和回调的过程](res/RxJava.png) 所以,为什么 RxJava 为人诟病其调用栈太长,就是因为当我们使用一个个的装饰器套起来的时候,导致整个调用的栈变得很长。 另外,捎带说一下所谓的线程切换的问题。假如我们在上述调用过程中的 4 处使用了 `subscribeOn()` 方法,并指定处理的线程为 A;在 5 处同样调用该方法,但是指定的线程为 B,那么之前的 1~3 的过程会被包装成一个对象,放在 4 指定的线程中执行;然后 4 又被包装成一个对象放在 5 所在的线程。因此,如果我们在 2 中获取当前线程,那么肯定得到的是 4 所在的线程。也就是当使用两个 `subscribeOn()` 的时候,通常会被认为只有第一个有效的原因。其实两个都有效,只是 A 是在 B 中执行的,而 1~3 又是在 A 中执行的。所以,所谓的线程切换到奥秘啊,就是依靠这层包裹的关系实现的。一个线程里面把任务执行完了,自然就切换到另一个线程里了。(`subscribeOn()` 和 `observeOn()` 实现线程的时候稍有区别,详情看下文。) ### 1.4 RxJava 的线程切换的执行过程 上面我们也提到过 `subscribeOn()` 和 `observeOn()` 实现线程切换的方式有所不同。所以,在下面的文章中,我们分成两种情况来分别对其进行分析。 当调用 `subscribeOn()` 方法的时候,上流传入的 Observable 将会被进一步装饰成 ObservableSubscribeOn 对象。按照我们上面的分析,当最终调用 `subscribe()` 方法的时候,将会沿着装饰器构成的链,直到 ObservableSubscribeOn 的 `subscribeActual()` 方法中。下面就是该方法的定义, ```java @Override public void subscribeActual(final Observer s) { final SubscribeOnObserver parent = new SubscribeOnObserver(s); s.onSubscribe(parent); parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent))); } ``` 除了上面分析的内容,这里多了一个 scheduler,它就是我们调用 `subscribeOn()` 方法时指定的线程。这里会直接调用它的 `scheduleDirect()` 方法将任务添加到线程池当中执行。这里传入的是 SubscribeTask 对象,它实现了 `Runnable` 接口,并且会在覆写的 `run()` 方法中调用传入的 parent 的 `subscribe()` 方法。因此,它可以被放入任何线程池当中执行,并且当被执行的时候会调用传入的 Observable 的 `subscribe()` 方法来让上流的任务在该线程池当中执行。 下面是 RxJava 中异步任务执行的流程图, ![RxJava 任务调度](res/RxJava_Scheduler.png) 这里的传入的 Schduler 是一个顶层的类,当我们调用 `Schedulers.io()` 等方法的时候,会获取其实现类的实例,比如 IOScheduler. 上面调用 `scheduleDirect()` 方法之后会先使用 Scheduler 的模板方法 `createWorker()` 中获取到一个 Worker. 这个类用来对 RxJava 的任务进行管理。它会进一步调用自己的 `schedule()` 方法来进一步安排任务的执行。图中的 Worker 也是一个抽象类,上面用到的 NewThreadWorker 是它的一个实现。NewThreadWorker 中维护了一个线程池,当调用了它的 `scheduler()` 方法的时候,它就会进一步把该任务放进线程池当中执行。因此,我们的异步任务就在该线程池当中被执行了。 然后,我们再来看下 `observeOn()` 方法是如何进行任务调度的。 当我们调用 `observeOn()` 方法的时候,该任务会被包装成 ObservableObserveOn 的实例。同样,我们来看它的 `subscribeActual()` 方法, ```java @Override protected void subscribeActual(Observer observer) { if (scheduler instanceof TrampolineScheduler) { source.subscribe(observer); } else { Scheduler.Worker w = scheduler.createWorker(); source.subscribe(new ObserveOnObserver(observer, w, delayError, bufferSize)); } } ``` 它会直接调用 Scheduler 的模板方法得到 Worker,然后将 Worker 和传入的 Observer 一起包装到 ObserveOnObserver 中。它会被继续向上传递到 ObservableCreate 中,然后它的 `onNext()` 等方法将会被顶层的类触发。接下来,我们就看下 ObserveOnObserver 的定义,这里我们仍然只以 `onNext()` 为例,其方法源码如下, ```java @Override public void onNext(T t) { if (done) { return; } if (sourceMode != QueueDisposable.ASYNC) { queue.offer(t); } schedule(); } void schedule() { if (getAndIncrement() == 0) { worker.schedule(this); } } ``` 因此,可以看出,在 `observeOn()` 方法中也是通过将任务放进某个 Worker 中执行来实现的,只是具体的线程将取决于 Scheduler 和 Worker 的具体实现。 而 Android 中的将任务放进主线程当中去执行就是通过向主线程的 Handler 发送消息来实现的。如果按照 `subscribeOn()` 的解释,那么当 A 线程启动 B 线程执行任务,那么 B 执行完自然就到了 A 了。那么为什么 Android 中还需要向主线程中发送消息呢?我们使用下面的图来解释。 ![RxJava 线程切换](res/RxJava_Switch2.png) `subscribeOn()` 是一个向上回调的过程,当 A 线程启动 B 线程执行任务,那么 B 执行完自然就到了 A 了,没有问题。但 `observeOn()` 是一个向下调用的过程,从上面的代码中也可以看出,它直接在线程池当中调用 `onNext()` 的时候会沿着回调相反的路线从上往下执行,因此 `observeOn()` 之后所有的逻辑在它指定的线程中执行。 ## 2、总结 在本篇文章中,我们总结了 RxJava 2 的源码。虽然 RxJava 的功能非常强大,但是其核心的实现却仅仅依赖两个设计模式,一个是观察者模式,另一个是装饰器模式。它采用了类似于 Java 的流的设计,每个装饰器负责自己一种任务,这复合单一责任原则;各个装饰器之间相互协作,来完成复杂的功能。从上面的源码分析过程中我们也可以看出,RxJava 的缺点也是非常明显的,大量的自定义类,在完成一个功能的时候各装饰器之间不断包装,导致调用的栈非常长。至于线程的切换,它依赖于自己的装饰器模式,因为一个装饰器可以决定其上游的 Observable 在哪些线程当中执行;两个装饰器处于不同的线程的时候,从一个线程中执行完毕自然进入到另一个线程中执行就完成了线程切换的过程。 以上就是 RxJava 的源码分析,如有疑问,欢迎评论区交流:) ================================================ FILE: 响应式编程/RxJava系列(1):一篇的比较全面的RxJava2方法总结.md ================================================ # RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结 看了许多讲解RxJava的文章,有些文章讲解的内容是基于第一个版本的,有些文章的讲解是通过比较常用的一些API和基础的概念进行讲解的。 但是每次看到RxJava的类中的几十个方法的时候,总是感觉心里没底。所以,我打算自己去专门写篇文章来从API的角度系统地梳理一下RxJava的各种方法和用法。 ## 1、RxJava 基本 ### 1.1 RxJava 简介 RxJava是一个在Java VM上使用可观测的序列来组成异步的、基于事件的程序的库。 虽然,在Android中,我们可以使用AsyncTask来完成异步任务操作,但是当任务的梳理比较多的时候,我们要为每个任务定义一个AsyncTask就变得非常繁琐。 RxJava能帮助我们在实现异步执行的前提下保持代码的清晰。 它的原理就是创建一个`Observable`来完成异步任务,组合使用各种不同的链式操作,来实现各种复杂的操作,最终将任务的执行结果发射给`Observer`进行处理。 当然,RxJava不仅适用于Android,也适用于服务端等各种场景。 我们总结以下RxJava的用途: 1. 简化异步程序的流程; 2. 使用近似于Java8的流的操作进行编程:因为想要在Android中使用Java8的流编程有诸多的限制,所以我们可以使用RxJava来实现这个目的。 在使用RxJava之前,我们需要先在自己的项目中添加如下的依赖: compile 'io.reactivex.rxjava2:rxjava:2.2.0' compile 'io.reactivex.rxjava2:rxandroid:2.0.2' 这里我们使用的是RxJava2,它与RxJava的第一个版本有些许不同。在本文中,我们所有的关于RxJava的示例都将基于RxJava2. 注:如果想了解关于Java8的流编程的内容的内容,可以参考我之前写过的文章[五分钟学习Java8的流编程](https://juejin.im/post/5b07f4536fb9a07ac90da4e5)。 ### 1.2 概要 下面是RxJava的一个基本的用例,这里我们定义了一个`Observable`,然后在它内部使用`emitter`发射了一些数据和信息(其实就相当于调用了被观察对象内部的方法,通知所有的观察者)。 然后,我们用`Consumer`接口的实例作为`subscribe()`方法的参数来观察发射的结果。(这里的接口的方法都已经被使用Lambda简化过,应该学着适应它。) Observable observable = Observable.create(emitter -> { emitter.onNext(1); emitter.onNext(2); emitter.onNext(3); }); observable.subscribe(System.out::println); 这样,我们就完成了一个基本的RxJava的示例。从上面的例子中,你或许没法看出`Observable`中隐藏的流的概念,看下面的例子: Observable.range(0, 10).map(String::valueOf).forEach(System.out::println); 这里我们先用`Observable.range()`方法产生一个序列,然后用`map`方法将该整数序列映射成一个字符序列,最后将得到的序列输出来。从上面看出,这种操作和Java8里面的Stream编程很像。但是两者之间是有区别的: 1. 所谓的“推”和“拉”的区别:Stream中是通过从流中读取数据来实现链式操作,而RxJava除了Stream中的功能之外,还可以通过“发射”数据,来实现通知的功能,即RxJava在Stream之上又多了一个观察者的功能。 2. Java8中的Stream可以通过`parall()`来实现并行,即基于分治算法将任务分解并计算得到结果之后将结果合并起来;而RxJava只能通过`subscribeOn()`方法将所有的操作切换到某个线程中去。 3. Stream只能被消费一次,但是`Observable`可以被多次进行订阅; RxJava除了为我们提供了`Observable`之外,在新的RxJava中还提供了适用于其他场景的基础类,它们之间的功能和主要区别如下: 1. `Flowable`: 多个流,响应式流和背压 2. `Observable`: 多个流,无背压 3. `Single`: 只有一个元素或者错误的流 4. `Completable`: 没有任何元素,只有一个完成和错误信号的流 5. `Maybe`: 没有任何元素或者只有一个元素或者只有一个错误的流 除了上面的几个基础类之外,还有一个`Disposable`。当我们监听某个流的时候,就能获取到一个`Disposable`对象。它提供了两个方法,一个是`isDisposed`,可以被用来判断是否停止了观察指定的流;另一个是`dispose`方法,用来放弃观察指定的流,我们可以使用它在任意的时刻停止观察操作。 ### 1.3 总结 上面我们介绍了了关于RxJava的基本的概念和使用方式,在下面的文章中我们会按照以上定义的顺序从API的角度来讲解以下RxJava各个模块的使用方法。 ## 2、RxJava 的使用 ### 2.1 Observable 从上面的文章中我们可以得知,`Observable`和后面3种操作功能近似,区别在于`Flowable`加入了背压的概念,`Observable`的大部分方法也适用于其他3个操作和`Flowable`。 因此,我们这里先从`Observable`开始梳理,然后我们再专门对`Flowable`和背压的进行介绍。 `Observable`为我们提供了一些静态的构造方法来创建一个`Observable`对象,还有许多链式的方法来完成各种复杂的功能。 这里我们按照功能将它的这些方法分成各个类别并依次进行相关的说明。 #### 2.1.1 创建操作 1.interval & intervalRange 下面的操作可以每个3秒的时间发送一个整数,整数从0开始: Observable.interval(3, TimeUnit.SECONDS).subscribe(System.out::println); 如果想要设置从指定的数字开始也是可以的,实际上`interval`提供了许多重载方法供我们是使用。下面我们连同与之功能相近的`intervalRange`方法也一同给出: 1. `public static Observable interval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler)` 2. `public static Observable interval(long period, TimeUnit unit, Scheduler scheduler)` 3. `public static Observable intervalRange(long start, long count, long initialDelay, long period, TimeUnit unit, Scheduler scheduler)` 这里的`initialDelay`参数用来指示开始发射第一个整数的之前要停顿的时间,时间的单位与`peroid`一样,都是通过`unit`参数来指定的;`period`参数用来表示每个发射之间停顿多少时间;`unit`表示时间的单位,是`TimeUnit`类型的;`scheduler`参数指定数据发射和等待时所在的线程。 `intervalRange`方法可以用来将发射的整数序列限制在一个范围之内,这里的`start`用来表示发射的数据的起始值,`count`表示总共要发射几个数字,其他参数与上面的`interval`方法一致。 2.range & rangeLong 下面的操作可以产生一个从5开始的连续10个整数构成的序列: Observable.range(5, 10).subscribe(i -> System.out.println("1: " + i)); 该方法需要传入两个参数,与之有相同功能的方法还有`rangeLong`: 1. `public static Observable range(final int start, final int count)` 2. `public static Observable rangeLong(long start, long count)` 这里的两个参数`start`用来指定用于生成的序列的开始值,`count`用来指示要生成的序列总共包含多少个数字,上面的两个方法的主要区别在于一个是用来生成int型整数的,一个是用来生成long型整数的。 3.create `create`方法用于从头开始创建一个`Observable`,像下面显示的那样,你需要使用`create`方法并传一个发射器作为参数,在该发射器内部调用`onNext`、`onComplete`和`onError`方法就可以将数据发送给监听者。 Observable.create((ObservableOnSubscribe) observableEmitter -> { observableEmitter.onNext(1); observableEmitter.onNext(2); observableEmitter.onComplete(); }).subscribe(System.out::println); 4.defer `defer`直到有观察者订阅时才创建Observable,并且为每个观察者创建一个新的Observable。`defer`操作符会一直等待直到有观察者订阅它,然后它使用Observable工厂方法生成一个Observable。比如下面的代码两个订阅输出的结果是不一致的: Observable observable = Observable.defer((Callable>) () -> Observable.just(System.currentTimeMillis())); observable.subscribe(System.out::print); System.out.println(); observable.subscribe(System.out::print); 下面是该方法的定义,它接受一个Callable对象,可以在该对象中返回一个Observable的实例: `public static Observable defer(Callable> supplier)` 5.empty & never & error 1. `public static Observable empty()`:创建一个不发射任何数据但是正常终止的Observable; 2. `public static Observable never()`:创建一个不发射数据也不终止的Observable; 3. `public static Observable error(Throwable exception)`:创建一个不发射数据以一个错误终止的Observable,它有几个重载版本,这里给出其中的一个。 测试代码: Observable.empty().subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete")); Observable.never().subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete")); Observable.error(new Exception()).subscribe(i->System.out.print("next"),i->System.out.print("error"),()->System.out.print("complete")); 输出结果:`completeerror` 6.from 系列 `from`系列的方法用来从指定的数据源中获取一个Observable: 1. `public static Observable fromArray(T... items)`:从数组中获取; 2. `public static Observable fromCallable(Callable supplier)`:从Callable中获取; 3. `public static Observable fromFuture(Future future)`:从Future中获取,有多个重载版本,可以用来指定线程和超时等信息; 4. `public static Observable fromIterable(Iterable source)`:从Iterable中获取; 5. `public static Observable fromPublisher(Publisher publisher)`:从Publisher中获取。 7.just 系列 just系列的方法的一个参数的版本为下面的形式:`public static Observable just(T item)`,它还有许多个重载的版本,区别在于接受的参数的个数不同,最少1个,最多10个。 8.repeat 该方法用来表示指定的序列要发射多少次,下面的方法会将该序列无限次进行发送: Observable.range(5, 10).repeat().subscribe(i -> System.out.println(i)); `repeat`方法有以下几个相似方法: 1. `public final Observable repeat()` 2. `public final Observable repeat(long times)` 3. `public final Observable repeatUntil(BooleanSupplier stop)` 4. `public final Observable repeatWhen(Function, ? extends ObservableSource> handler)` 第1个无参的方法会无限次地发送指定的序列(实际上内部调用了第2个方法并传入了Long.MAX_VALUE),第2个方法会将指定的序列重复发射指定的次数;第3个方法会在满足指定的要求的时候停止重复发送,否则会一直发送。 9.timer timer操作符创建一个在给定的时间段之后返回一个特殊值的Observable,它在延迟一段给定的时间后发射一个简单的数字0。比如下面的程序会在500毫秒之后输出一个数字`0`。 Observable.timer(500, TimeUnit.MILLISECONDS).subscribe(System.out::print); 下面是该方法及其重载方法的定义,重载方法还可以指定一个调度器: 1. `public static Observable timer(long delay, TimeUnit unit)` 2. `public static Observable timer(long delay, TimeUnit unit, Scheduler scheduler)` #### 2.1.2 变换操作 1.map & cast 1. `map`操作符对原始Observable发射的每一项数据应用一个你选择的函数,然后返回一个发射这些结果的Observable。默认不在任何特定的调度器上执行。 2. `cast`操作符将原始Observable发射的每一项数据都强制转换为一个指定的类型(多态),然后再发射数据,它是map的一个特殊版本: 下面的第一段代码用于将生成的整数序列转换成一个字符串序列之后并输出;第二段代码用于将Date类型转换成Object类型并进行输出,这里如果前面的Class无法转换成第二个Class就会出现异常: Observable.range(1, 5).map(String::valueOf).subscribe(System.out::println); Observable.just(new Date()).cast(Object.class).subscribe(System.out::print); 这两个方法的定义如下: 1. `public final Observable map(Function mapper)` 2. `public final Observable cast(Class clazz)` 这里的`mapper`函数接受两个泛型,一个表示原始的数据类型,一个表示要转换之后的数据类型,转换的逻辑写在该接口实现的方法中即可。 2.flatMap & contactMap `flatMap`将一个发送事件的上游Observable变换为多个发送事件的Observables,然后将它们发射的事件合并后放进一个单独的Observable里。需要注意的是, flatMap并不保证事件的顺序,也就是说转换之后的Observables的顺序不必与转换之前的序列的顺序一致。比如下面的代码用于将一个序列构成的整数转换成多个单个的`Observable`,然后组成一个`OBservable`,并被订阅。下面输出的结果仍将是一个字符串数字序列,只是顺序不一定是增序的。 Observable.range(1, 5) .flatMap((Function>) i -> Observable.just(String.valueOf(i))) .subscribe(System.out::println); 与`flatMap`对应的方法是`contactMap`,后者能够保证最终输出的顺序与上游发送的顺序一致。下面是这两个方法的定义: 1. `public final Observable flatMap(Function> mapper)` 2. `public final Observable concatMap(Function> mapper)` `flatMap`的重载方法数量过多,它们在数据源方面略有不同,有的支持错误等可选参数,具体可以参考源代码。 3.flatMapIterable `flatMapIterable`可以用来将上流的任意一个元素转换成一个`Iterable`对象,然后我们可以对其进行消费。在下面的代码中,我们先生成一个整数的序列,然后将每个整数映射成一个`Iterable`类型,最后,我们对其进行订阅和消费: Observable.range(1, 5) .flatMapIterable((Function>) integer -> Collections.singletonList(String.valueOf(integer))) .subscribe(s -> System.out.println("flatMapIterable : " + s)); 下面是该方法及其重载方法的定义: 1. `public final Observable flatMapIterable(Function> mapper)` 2. `public final Observable flatMapIterable(Function> mapper, BiFunction resultSelector)` 4.buffer 该方法用于将整个流进行分组。以下面的程序为例,我们会先生成一个7个整数构成的流,然后使用`buffer`之后,这些整数会被3个作为一组进行输出,所以当我们订阅了`buffer`转换之后的`Observable`之后得到的是一个列表构成的`OBservable`: Observable.range(1, 7).buffer(3) .subscribe(integers -> System.out.println(Arrays.toString(integers.toArray()))); 下面是这个方法及其重载方法的定义,它的重载方法太多,这里我们只给出其中的两个,其他的可以参考RxJava的源码。这里的buffer应该理解为一个缓冲区,当缓冲区满了或者剩余的数据不够一个缓冲区的时候就将数据发射出去。 1. `public final Observable> buffer(int count)` 2. `public final Observable> buffer(int count, int skip)` 3. ... 5.groupBy `groupBy`用于分组元素,它可以被用来根据指定的条件将元素分成若干组。它将得到一个`Observable>`类型的`Observable`。如下面的程序所示,这里我们使用`concat`方法先将两个`Observable`拼接成一个`Observable`,然后对其元素进行分组。这里我们的分组依据是整数的值,这样我们将得到一个`Observable>`类型的`Observable`。然后,我们再将得到的序列拼接成一个并进行订阅输出: Observable> observable = Observable.concat( Observable.range(1,4), Observable.range(1,6)).groupBy(integer -> integer); Observable.concat(observable).subscribe(integer -> System.out.println("groupBy : " + integer)); 该方法有多个重载版本,这里我们用到的一个的定义是: `public final Observable> groupBy(Function keySelector)` 6.scan `scan`操作符对原始Observable发射的第一项数据应用一个函数,然后将那个函数的结果作为自己的第一项数据发射。它将函数的结果同第二项数据一起填充给这个函数来产生它自己的第二项数据。它持续进行这个过程来产生剩余的数据序列。这个操作符在某些情况下被叫做accumulator。 以下面的程序为例,该程序的输结果是`2 6 24 120 720`,可以看出这里的计算规则是,我们把传入到`scan`中的函数记为`f`,序列记为`x`,生成的序列记为`y`,那么这里的计算公式是`y(0)=x(0); y(i)=f(y(i-1), x(i)), i>0`: Observable.range(2, 5).scan((i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + " ")); 除了上面的这种形式,`scan`方法还有一个重载的版本,我们可以使用这个版本的方法来在生成序列的时候指定一个初始值。以下面的程序为例,它的输出结果是`3 6 18 72 360 2160 `,可以看出它的输出比上面的形式多了1个,这是因为当指定了初始值之后,生成的第一个数字就是那个初始值,剩下的按照我们上面的规则进行的。所以,用同样的函数语言来描述的话,那么它就应该是下面的这种形式:`y(0)=initialValue; y(i)=f(y(i-1), x(i)), i>0`。 Observable.range(2, 5).scan(3, (i1, i2) -> i1 * i2).subscribe(i -> System.out.print(i + " ")); 以上方法的定义是: 1. `public final Observable scan(BiFunction accumulator)` 2. `public final Observable scan(R initialValue, BiFunction accumulator)` 7.window `window`Window和Buffer类似,但不是发射来自原始Observable的数据包,它发射的是Observable,这些Observables中的每一个都发射原始Observable数据的一个子集,最后发射一个onCompleted通知。 以下面的程序为例,这里我们首先生成了一个由10个数字组成的整数序列,然后使用`window`函数将它们每3个作为一组,每组会返回一个对应的Observable对象。 这里我们对该返回的结果进行订阅并进行消费,因为10个数字,所以会被分成4个组,每个对应一个Observable: Observable.range(1, 10).window(3).subscribe( observable -> observable.subscribe(integer -> System.out.println(observable.hashCode() + " : " + integer))); 除了对数据包进行分组,我们还可以根据时间来对发射的数据进行分组。该方法有多个重载的版本,这里我们给出其中的比较具有代表性的几个: 1. `public final Observable> window(long count)` 2. `public final Observable> window(long timespan, long timeskip, TimeUnit unit)` 3. `public final Observable> window(ObservableSource boundary)` 4. `public final Observable> window(Callable> boundary)` #### 2.1.3 过滤操作 1.filter `filter`用来根据指定的规则对源进行过滤,比如下面的程序用来过滤整数1到10中所有大于5的数字: Observable.range(1,10).filter(i -> i > 5).subscribe(System.out::println); 下面是该方法的定义: 1. `public final Observable filter(Predicate predicate)` 2.elementAt & firstElement & lastElement `elementAt`用来获取源中指定位置的数据,它有几个重载方法,这里我们介绍一下最简单的一个方法的用法。下面是`elementAt`的一个示例,它将获取源数据中索引为1的元素并交给观察者订阅。下面的程序将输出`1` Observable.range(1, 10).elementAt(0).subscribe(System.out::print); 这里我们给出`elementAt`及其相关的方法的定义,它们的使用相似。注意一下这里的返回类型: 1. `public final Maybe elementAt(long index)` 2. `public final Single elementAt(long index, T defaultItem)` 3. `public final Single elementAtOrError(long index)` 除了获取指定索引的元素的方法之外,RxJava中还有可以用来直接获取第一个和最后一个元素的方法,这里我们直接给出方法的定义: 1. `public final Maybe firstElement()` 2. `public final Single first(T defaultItem)` 3. `public final Single firstOrError()` 4. `public final Maybe lastElement()` 5. `public final Single last(T defaultItem)` 6. `public final Single lastOrError()` 3.distinct & distinctUntilChanged `distinct`用来对源中的数据进行过滤,以下面的程序为例,这里会把重复的数字7过滤掉: Observable.just(1,2,3,4,5,6,7,7).distinct().subscribe(System.out::print); 与之类似的还有`distinctUntilChanged`方法,与`distinct`不同的是,它只当相邻的两个元素相同的时候才会将它们过滤掉。比如下面的程序会过滤掉其中的2和5,所以最终的输出结果是`12345676`: Observable.just(1,2,2,3,4,5,5,6,7,6).distinctUntilChanged().subscribe(System.out::print); 该方法也有几个功能相似的方法,这里给出它们的定义如下: 1. `public final Observable distinct()` 2. `public final Observable distinct(Function keySelector)` 3. `public final Observable distinct(Function keySelector, Callable> collectionSupplier)` 4. `public final Observable distinctUntilChanged()` 5. `public final Observable distinctUntilChanged(Function keySelector)` 6. `public final Observable distinctUntilChanged(BiPredicate comparer)` 4.skip & skipLast & skipUntil & skipWhile `skip`方法用于过滤掉数据的前n项,比如下面的程序将会过滤掉前2项,因此输出结果是`345`: Observable.range(1, 5).skip(2).subscribe(System.out::print); 与`skip`方法对应的是`take`方法,它用来表示只选择数据源的前n项,该方法的示例就不给出了。这里,我们说一下与之类功能类似的重载方法。`skip`还有一个重载方法接受两个参数,用来表示跳过指定的时间,也就是在指定的时间之后才开始进行订阅和消费。下面的程序会在3秒之后才开始不断地输出数字: Observable.range(1,5).repeat().skip(3, TimeUnit.SECONDS).subscribe(System.out::print); 与`skip`功能相反的方法的还有`skipLast`,它用来表示过滤掉后面的几项,以及最后的一段时间不进行发射等。比如下面的方法,我们会在程序开始之前进行计时,然后会不断重复输出数字,直到5秒之后结束。然后,我们用`skipLast`方法表示最后的2秒不再进行发射。所以下面的程序会先不断输出数字3秒,3秒结束后停止输出,并在2秒之后结束程序: long current = System.currentTimeMillis(); Observable.range(1,5) .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5)) .skipLast(2, TimeUnit.SECONDS).subscribe(System.out::print); 与上面的这些方法类似的还有一些,这里我们不再一一列举。因为这些方法的重载方法比较多,下面我们给出其中的具有代表性的一部分: 1. `public final Observable skip(long count)` 2. `public final Observable skip(long time, TimeUnit unit, Scheduler scheduler)` 3. `public final Observable skipLast(int count)` 4. `public final Observable skipLast(long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)` 5. `public final Observable skipUntil(ObservableSource other)` 6. `public final Observable skipWhile(Predicate predicate)` 5.take & takeLast & takeUntil & takeWhile 与`skip`方法对应的是`take`方法,它表示按照某种规则进行选择操作。我们以下面的程序为例,这里第一段程序表示只发射序列中的前2个数据: Observable.range(1, 5).take(2).subscribe(System.out::print); 下面的程序表示只选择最后2秒中输出的数据: long current = System.currentTimeMillis(); Observable.range(1,5) .repeatUntil(() -> System.currentTimeMillis() - current > TimeUnit.SECONDS.toMillis(5)) .takeLast(2, TimeUnit.SECONDS).subscribe(System.out::print); 下面是以上相关的方法的定义,同样的,我们只选择其中比较有代表性的几个: 1. `public final Observable take(long count)` 2. `public final Observable takeLast(long count, long time, TimeUnit unit, Scheduler scheduler, boolean delayError, int bufferSize)` 3. `public final Observable takeUntil(ObservableSource other)` 4. `public final Observable takeUntil(Predicate stopPredicate)` 5. `public final Observable takeWhile(Predicate predicate)` 6.ignoreElements 该方法用来过滤所有源Observable产生的结果,只会把Observable的onComplete和onError事件通知给订阅者。下面是该方法的定义: 1. `public final Completable ignoreElements()` 7.throttleFirst & throttleLast & throttleLatest & throttleWithTimeout 这些方法用来对输出的数据进行限制,它们是通过时间的”窗口“来进行限制的,你可以理解成按照指定的参数对时间进行分片,然后根据各个方法的要求选择第一个、最后一个、最近的等进行发射。下面是`throttleLast`方法的用法示例,它会输出每个500毫秒之间的数字中最后一个数字: Observable.interval(80, TimeUnit.MILLISECONDS) .throttleLast(500, TimeUnit.MILLISECONDS) .subscribe(i -> System.out.print(i + " ")); 其他的几个方法的功能大致列举如下: 1. `throttleFirst`只会发射指定的Observable在指定的事件范围内发射出来的第一个数据; 2. `throttleLast`只会发射指定的Observable在指定的事件范围内发射出来的最后一个数据; 3. `throttleLatest`用来发射距离指定的时间分片最近的那个数据; 5. `throttleWithTimeout`仅在过了一段指定的时间还没发射数据时才发射一个数据,如果在一个时间片达到之前,发射的数据之后又紧跟着发射了一个数据,那么这个时间片之内之前发射的数据会被丢掉,该方法底层是使用`debounce`方法实现的。如果数据发射的频率总是快过这里的`timeout`参数指定的时间,那么将不会再发射出数据来。 下面是这些方法及其重载方法的定义(选择其中一部分): 1. `public final Observable throttleFirst(long skipDuration, TimeUnit unit, Scheduler scheduler)` 2. `public final Observable throttleLast(long intervalDuration, TimeUnit unit, Scheduler scheduler)` 3. `public final Observable throttleLatest(long timeout, TimeUnit unit, Scheduler scheduler, boolean emitLast)` 4. `public final Observable throttleWithTimeout(long timeout, TimeUnit unit, Scheduler scheduler)` 8.debounce `debounce`也是用来限制发射频率过快的,它仅在过了一段指定的时间还没发射数据时才发射一个数据。我们通过下面的图来说明这个问题: ![debounce](https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.png) 这里红、绿、蓝三个球发射出来的原因都是因为当反射了这个球之后的一定的时间内没有其他的球发射出来,这个时间是我们可以通过参数来指定的。 该方法的用法与`throttle`之类的方法类似,上面也说过`throttle`那些方法底层用了`debounce`实现,所以,这里我们不再为该方法专门编写相关的测试代码。 9.sample 实际上`throttleLast`的实现中内部调用的就是`sample`。 #### 2.1.4 组合操作 1.startWith & startWithArray `startWith`方法可以用来在指定的数据源的之前插入几个数据,它的功能类似的方法有`startWithArray`,另外还有几个重载方法。这里我们给出一个基本的用法示例,下面的程序会在原始的数字流1-5的前面加上0,所以最终的输出结果是`012345`: Observable.range(1,5).startWith(0).subscribe(System.out::print); 下面是`startWith`及其几个功能相关的方法的定义: 1. `public final Observable startWith(Iterable items)` 2. `public final Observable startWith(ObservableSource other)` 3. `public final Observable startWith(T item)` 4. `public final Observable startWithArray(T... items)` 2.merge & mergeArray `merge`可以让多个数据源的数据合并起来进行发射,当然它可能会让`merge`之后的数据交错发射。下面是一个示例,这个例子中,我们使用`merge`方法将两个`Observable`合并到了一起进行监听: Observable.merge(Observable.range(1,5), Observable.range(6,5)).subscribe(System.out::print); 鉴于`merge`方法及其功能类似的方法太多,我们这里挑选几个比较有代表性的方法,具体的可以查看RxJava的源代码: 1. `public static Observable merge(Iterable> sources)` 2. `public static Observable mergeArray(ObservableSource... sources)` 3. `public static Observable mergeDelayError(Iterable> sources)` 4. `public static Observable mergeArrayDelayError(ObservableSource... sources)` 这里的`mergeError`方法与`merge`方法的表现一致,只是在处理由`onError`触发的错误的时候有所不同。`mergeError`方法会等待所有的数据发射完毕之后才把错误发射出来,即使多个错误被触发,该方法也只会发射出一个错误信息。而如果使用`merger`方法,那么当有错误被触发的时候,该错误会直接被抛出来,并结束发射操作。下面是该方法的一个使用的示例,这里我们主线程停顿4秒,然后所有`merge`的Observable中的一个会在线程开始的第2秒的时候触发一个错误,该错误最终会在所有的数据发射完毕之后被发射出来: Observable.mergeDelayError(Observable.range(1,5), Observable.range(1,5).repeat(2), Observable.create((ObservableOnSubscribe) observableEmitter -> { Thread.sleep(2000); observableEmitter.onError(new Exception("error")); }) ).subscribe(System.out::print, System.out::print); Thread.sleep(4000); 3.concat & concatArray & concatEager 该方法也是用来将多个Observable拼接起来,但是它会严格按照传入的Observable的顺序进行发射,一个Observable没有发射完毕之前不会发射另一个Observable里面的数据。下面是一个程序示例,这里传入了两个Observable,会按照顺序输出`12345678910`: Observable.concat(Observable.range(1, 5), Observable.range(6, 5)).subscribe(System.out::print); 下面是该方法的定义,鉴于该方法及其重载方法太多,这里我们选择几个比较有代表性的说明: 1. `public static Observable concat(Iterable> sources)` 2. `public static Observable concatDelayError(Iterable> sources)` 3. `public static Observable concatArray(ObservableSource... sources)` 4. `public static Observable concatArrayDelayError(ObservableSource... sources)` 5. `public static Observable concatEager(ObservableSource> sources)` 6. `public static Observable concatArrayEager(ObservableSource... sources)` 对于`concat`方法,我们之前已经介绍过它的用法;这里的`conactArray`的功能与之类似;对于`concatEager`方法,当一个观察者订阅了它的结果,那么就相当于订阅了它拼接的所有`ObservableSource`,并且会先缓存这些ObservableSource发射的数据,然后再按照顺序将它们发射出来。而对于这里的`concatDelayError`方法的作用和前面的`mergeDelayError`类似,只有当所有的数据都发射完毕才会处理异常。 4.zip & zipArray & zipIterable `zip`操作用来将多个数据项进行合并,可以通过一个函数指定这些数据项的合并规则。比如下面的程序的输出结果是`6 14 24 36 50 `,显然这里的合并的规则是相同索引的两个数据的乘积。不过仔细看下这里的输出结果,可以看出,如果一个数据项指定的位置没有对应的值的时候,它是不会参与这个变换过程的: Observable.zip(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2) .subscribe(i -> System.out.print(i + " ")); `zip`方法有多个重载的版本,同时也有功能近似的方法,这里我们挑选有代表性的几个进行说明: 1. `public static Observable zip(Iterable> sources, Function zipper)` 2. `ublic static Observable zipArray(Function zipper, boolean delayError, int bufferSize, ObservableSource... sources)` 3. `public static Observable zipIterable(Iterable> sources, Function zipper, boolean delayError, int bufferSize)` 实际上上面几个方法的用法和功能基本类似,区别在于传入的`ObservableSource`的参数的形式。 5.combineLastest 与`zip`操作类似,但是这个操作的输出结果与`zip`截然不同,以下面的程序为例,它的输出结果是`36 42 48 54 60`: Observable.combineLatest(Observable.range(1, 6), Observable.range(6, 5), (integer, integer2) -> integer * integer2) .subscribe(i -> System.out.print(i + " ")); 利用下面的这张图可以比较容易来说明这个问题: ![combineLastest](https://github.com/Shouheng88/Awesome-Android/blob/master/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/res/combineLatest.png?raw=true) 上图中的上面的两条横线代表用于拼接的两个数据项,下面的一条横线是拼接之后的结果。`combineLatest`的作用是拼接最新发射的两个数据。下面我们用上图的过程来说明该方法是如何执行的:开始第一条只有1的时候无法拼接,;当第二条出现A的时候,此时最新的数据是1和A,故组合成一个1A;第二个数据项发射了B,此时最新的数据是1和B,故组合成1B;第一条横线发射了2,此时最新的数据是2和B,因此得到了2B,依次类推。然后再回到我们上面的问题,第一个数据项连续发射了5个数据的时候,第二个数据项一个都没有发射出来,因此没有任何输出;然后第二个数据项开始发射数据,当第二个数据项发射了6的时候,此时最新的数据组合是6和6,故得36;然后,第二个数据项发射了7,此时最新的数据组合是6和7,故得42,依次类推。 该方法也有对应的`combineLatestDelayError`方法,用途也是只有当所有的数据都发射完毕的时候才去处理错误逻辑。 #### 2.1.5 辅助操作 1.delay `delay`方法用于在发射数据之前停顿指定的时间,比如下面的程序会在真正地发射数据之前停顿1秒: Observable.range(1, 5).delay(1000, TimeUnit.MILLISECONDS).subscribe(System.out::print); Thread.sleep(1500); 同样`delay`方法也有几个重载的方法,可以供我们用来指定触发的线程等信息,这里给出其中的两个,其他的可以参考源码和文档: 1. `public final Observable delay(long delay, TimeUnit unit)` 2. `public final Observable delay(long delay, TimeUnit unit, Scheduler scheduler)` 2.do系列 RxJava中还有一系列的方法可以供我们使用,它们共同的特点是都是以`do`开头,下面我们列举一下这些方法并简要说明一下它们各自的用途: 1. `public final Observable doAfterNext(Consumer onAfterNext)`,会在`onNext`方法之后触发; 2. `public final Observable doAfterTerminate(Action onFinally)`,会在Observable终止之后触发; 3. `public final Observable doFinally(Action onFinally)`,当`onComplete`或者`onError`的时候触发; 4. `public final Observable doOnDispose(Action onDispose)`,当被dispose的时候触发; 5. `public final Observable doOnComplete(Action onComplete)`,当complete的时候触发; 6. `public final Observable doOnEach(final Observer observer)`,当每个`onNext`调用的时候触发; 7. `public final Observable doOnError(Consumer onError)`,当调用`onError`的时候触发; 8. `public final Observable doOnLifecycle(final Consumer onSubscribe, final Action onDispose)` 9. `public final Observable doOnNext(Consumer onNext)`,,会在`onNext`的时候触发; 9. `public final Observable doOnSubscribe(Consumer onSubscribe)`,会在订阅的时候触发; 10. `public final Observable doOnTerminate(final Action onTerminate)`,当终止之前触发。 这些方法可以看作是对操作执行过程的一个监听,当指定的操作被触发的时候会同时触发这些监听方法: Observable.range(1, 5) .doOnEach(integerNotification -> System.out.println("Each : " + integerNotification.getValue())) .doOnComplete(() -> System.out.println("complete")) .doFinally(() -> System.out.println("finally")) .doAfterNext(i -> System.out.println("after next : " + i)) .doOnSubscribe(disposable -> System.out.println("subscribe")) .doOnTerminate(() -> System.out.println("terminal")) .subscribe(i -> System.out.println("subscribe : " + i)); 3.subscribeOn & observeOn `subscribeOn`用于指定Observable自身运行的线程,`observeOn`用于指定发射数据所处的线程,比如Android中的异步任务需要用`observeOn`指定发射数据所在的线程是非主线程,然后执行完毕之后将结果发送给主线程,就需要用`subscribeOn`来指定。比如下面的程序,我们用这两个方法来指定所在的线程: Observable.create((ObservableOnSubscribe) observableEmitter -> { System.out.println(Thread.currentThread()); observableEmitter.onNext(0); }).observeOn(Schedulers.newThread()).subscribeOn(Schedulers.computation()) .subscribe(integer -> System.out.println(Thread.currentThread())); 最终的输出结果如下所示: Thread[RxComputationThreadPool-1,5,main] Thread[RxNewThreadScheduler-1,5,main] 4.timeout 用来设置一个超时时间,如果指定的时间之内没有任何数据被发射出来,那么就会执行我们指定的数据项。如下面的程序所示,我们先为设置了一个间隔200毫秒的数字产生器,开始发射数据之前要停顿1秒钟,因为我们设置的超时时间是500毫秒,因而在第500毫秒的时候会执行我们传入的数据项: Observable.interval(1000, 200, TimeUnit.MILLISECONDS) .timeout(500, TimeUnit.MILLISECONDS, Observable.rangeLong(1, 5)) .subscribe(System.out::print); Thread.sleep(2000); `timeout`方法有多个重载方法,可以为其指定线程等参数,可以参考源码或者文档了解详情。 #### 2.1.6 错误处理操作符 错误处理操作符主要用来提供给Observable,用来对错误信息做统一的处理,常用的两个是`catch`和`retry`。 1.catch catch操作会拦截原始的Observable的`onError`通知,将它替换为其他数据项或者数据序列,让产生的Observable能够正常终止或者根本不终止。在RxJava中该操作有3终类型: 1. `onErrorReturn`:这种操作会在onError触发的时候返回一个特殊的项替换错误,并调用观察者的onCompleted方法,而不会将错误传递给观察者; 2. `onErrorResumeNext`:会在onError触发的时候发射备用的数据项给观察者; 3. `onExceptionResumeNext`:如果onError触发的时候onError收到的Throwable不是Exception,它会将错误传递给观察者的onError方法,不会使用备用的Observable。 下面是`onErrorReturn`和`onErrorResumeNext`的程序示例,这里第一段代码会在出现错误的时候输出`666`,而第二段会在出现错误的时候发射数字`12345`: Observable.create((ObservableOnSubscribe) observableEmitter -> { observableEmitter.onError(null); observableEmitter.onNext(0); }).onErrorReturn(throwable -> 666).subscribe(System.out::print); Observable.create((ObservableOnSubscribe) observableEmitter -> { observableEmitter.onError(null); observableEmitter.onNext(0); }).onErrorResumeNext(Observable.range(1,5)).subscribe(System.out::print); 2.retry `retry`使用了一种错误重试机制,它可以在出现错误的时候进行重试,我们可以通过参数指定重试机制的条件。以下面的程序为例,这里我们设置了当出现错误的时候会进行2次重试,因此,第一次的时候出现错误会调用`onNext`,重试2次又会调用2次`onNext`,第二次重试的时候因为重试又出现了错误,因此此时会触发`onError`方法。也就是说,下面这段代码会触发`onNext`3次,触发`onError()`1次: Observable.create(((ObservableOnSubscribe) emitter -> { emitter.onNext(0); emitter.onError(new Throwable("Error1")); emitter.onError(new Throwable("Error2")); })).retry(2).subscribe(i -> System.out.println("onNext : " + i), error -> System.out.print("onError : " + error)); `retry`有几个重载的方法和功能相近的方法,下面是这些方法的定义(选取部分): 1. `public final Observable retry()`:会进行无限次地重试; 2. `public final Observable retry(BiPredicate predicate)` 3. `public final Observable retry(long times)`:指定重试次数; 4. `public final Observable retry(long times, Predicate predicate) ` 5. `public final Observable retryUntil(final BooleanSupplier stop)` 6. `public final Observable retryWhen(Function, ? extends ObservableSource> handler)` #### 2.1.7 条件操作符和布尔操作符 1.all & any 1. `all`用来判断指定的数据项是否全部满足指定的要求,这里的“要求”可以使用一个函数来指定; 2. `any`用来判断指定的Observable是否存在满足指定要求的数据项。 在下面的程序中,我们用该函数来判断指定的数据项是否全部满足大于5的要求,显然是不满足的,因此下面的程序将会输出`false`: Observable.range(5, 5).all(i -> i>5).subscribe(System.out::println); // false Observable.range(5, 5).any(i -> i>5).subscribe(System.out::println); // true 以下是该方法的定义: 1. `public final Single all(Predicate predicate)` 2. `public final Single any(Predicate predicate)` 2.contains & isEmpty 这两个方法分别用来判断数据项中是否包含我们指定的数据项,已经判断数据项是否为空: Observable.range(5, 5).contains(4).subscribe(System.out::println); // false Observable.range(5, 5).isEmpty().subscribe(System.out::println); // false 以下是这两个方法的定义: 1. `public final Single isEmpty()` 2. `public final Single contains(final Object element)` 3.sequenceEqual `sequenceEqual`用来判断两个Observable发射出的序列是否是相等的。比如下面的方法用来判断两个序列是否相等: Observable.sequenceEqual(Observable.range(1,5), Observable.range(1, 5)).subscribe(System.out::println); 4.amb `amb`作用的两个或多个Observable,但是只会发射最先发射数据的那个Observable的全部数据: Observable.amb(Arrays.asList(Observable.range(1, 5), Observable.range(6, 5))).subscribe(System.out::print) 该方法及其功能近似的方法的定义,这里前两个是静态的方法,第二个属于实例方法: 1. `public static Observable amb(Iterable> sources)` 2. `public static Observable ambArray(ObservableSource... sources)` 3. `public final Observable ambWith(ObservableSource other)` 5.defaultIfEmpty `defaultIfEmpty`用来当指定的序列为空的时候指定一个用于发射的值。下面的程序中,我们直接调用发射器的`onComplete`方法,因此序列是空的,结果输出一个整数`6`: Observable.create((ObservableOnSubscribe) Emitter::onComplete).defaultIfEmpty(6).subscribe(System.out::print); 下面是该方法的定义: 1. `public final Observable defaultIfEmpty(T defaultItem)` #### 2.1.8 转换操作符 1.toList & toSortedList `toList`和`toSortedList`用于将序列转换成列表,后者相对于前者增加了排序的功能: Observable.range(1, 5).toList().subscribe(System.out::println); Observable.range(1, 5).toSortedList(Comparator.comparingInt(o -> -o)).subscribe(System.out::println); 下面是它们的定义,它们有多个重载版本,这里选择其中的两个进行说明: 1. `public final Single> toList()` 2. `public final Single> toSortedList(final Comparator comparator)` 注意一下,这里的返回结果是`Single`类型的,不过这并不妨碍我们继续使用链式操作,因为`Single`的方法和`Observable`基本一致。 另外还要注意这里的`Single`中的参数是一个`List`,也就是说,它把整个序列转换成了一个列表对象。因此,上面的两个示例程序的输出是: [1, 2, 3, 4, 5] [5, 4, 3, 2, 1] 2.toMap & toMultimap `toMap`用于将发射的数据转换成另一个类型的值,它的转换过程是针对每一个数据项的。以下面的代码为例,它会将原始的序列中的每个数字转换成对应的十六进制。但是,`toMap`转换的结果不一定是按照原始的序列的发射的顺序来的: Observable.range(8, 10).toMap(Integer::toHexString).subscribe(System.out::print); 与`toMap`近似的是`toMultimap`方法,它可以将原始序列的每个数据项转换成一个集合类型: Observable.range(8, 10).toMultimap(Integer::toHexString).subscribe(System.out::print); 上面的两段程序的输出结果是: {11=17, a=10, b=11, c=12, d=13, e=14, f=15, 8=8, 9=9, 10=16} {11=[17], a=[10], b=[11], c=[12], d=[13], e=[14], f=[15], 8=[8], 9=[9], 10=[16]} 上面的两个方法的定义是(多个重载,选择部分): 1. `public final Single> toMap(final Function keySelector)` 2. `public final Single>> toMultimap(Function keySelector)` 3.toFlowable 该方法用于将一个Observable转换成Flowable类型,下面是该方法的定义,显然这个方法使用了策略模式,这里面涉及背压相关的内容,我们后续再详细介绍。 public final Flowable toFlowable(BackpressureStrategy strategy) 4.to 相比于上面的方法,`to`方法的限制更加得宽泛,你可以将指定的Observable转换成任意你想要的类型(如果你可以做到的话),下面是一个示例代码,用来将指定的整数序列转换成另一个整数类型的Observable,只不过这里的每个数据项都是原来的列表中的数据总数的值: Observable.range(1, 5).to(Observable::count).subscribe(System.out::println); 下面是该方法的定义: `public final R to(Function, R> converter)` ### 2.2 线程控制 之前有提到过RxJava的线程控制是通过`subscribeOn`和`observeOn`两个方法来完成的。 这里我们梳理一下RxJava提供的几种线程调度器以及RxAndroid为Android提供的调度器的使用场景和区别等。 1. `Schedulers.io()`:代表适用于io操作的调度器,增长或缩减来自适应的线程池,通常用于网络、读写文件等io密集型的操作。重点需要注意的是线程池是无限制的,大量的I/O调度操作将创建许多个线程并占用内存。 2. `Schedulers.computation()`:计算工作默认的调度器,代表CPU计算密集型的操作,与I/O操作无关。它也是许多RxJava方法,比如`buffer()`,`debounce()`,`delay()`,`interval()`,`sample()`,`skip()`,的默认调度器。 3. `Schedulers.newThread()`:代表一个常规的新线程。 4. `Schedulers.immediate()`:这个调度器允许你立即在当前线程执行你指定的工作。它是`timeout()`,`timeInterval()`以及`timestamp()`方法默认的调度器。 5. `Schedulers.trampoline()`:当我们想在当前线程执行一个任务时,并不是立即,我们可以用`trampoline()`将它入队。这个调度器将会处理它的队列并且按序运行队列中每一个任务。它是`repeat()`和`retry()`方法默认的调度器。 以及RxAndroid提供的线程调度器: `AndroidSchedulers.mainThread()`用来指代Android的主线程 ### 2.3 总结 上面的这些操作也基本适用于`Flowable`、`Single`、`Completable`和`Maybe`。 我们花费了很多的时间和精力来梳理了这些方法,按照上面的内容,使用RxJava实现一些基本的或者高级的操作都不是什么问题。 但是,Observable更适用于处理一些数据规模较小的问题,当数据规模比较多的时候可能会出现`MissingBackpressureException`异常。 因此,我们还需要了解背压和`Flowable`的相关内容才能更好地理解和应用RxJava. ================================================ FILE: 响应式编程/RxJava系列(2):Flowable和背压.md ================================================ # RxJava2 系列 (2):背压和Flowable 背压(Back Pressure)的概念最初并不是在响应式编程中提出的,它最初用在流体力学中,指的是后端的压力, 通常用于描述系统排出的流体在出口处或二次侧受到的与流动方向相反的压力。 在响应式编程中,我们可以将产生信息的部分叫做上游或者叫生产者,处理产生的信息的部分叫做下游或者消费者。 试想如果在异步的环境中,生产者的生产速度大于消费者的消费速度的时候,明显会出现生产过剩的情景,这时候就需要消费者对多余的数据进行缓存, 但如果生产的信息数量过多,以至于超出缓存大小,就会出现缓存溢出,甚至可能造成内存耗尽。 我们可以制定一个数据丢失的规则,来丢失那些“可以丢失的数据”,以减轻缓存的压力。 在之前我们介绍了一些方法,比如`throttleXXX`、`debounce`、`sample`等,都是用来解决在生产速度过快的情况下的数据过滤的,它们指定了数据取舍的规则。 而在`Flowable`,我们可以通过`onBackpressureXXX`一系列的方法来制定当数据生产过快情况下的数据取舍的规则, 我们可以把这种处理方式理解成背压,所谓背压,在Rx中就是通过一种下游用来控制上游事件发射频率的机制(就像流体在出口受到了阻力一样)。 所以,如何理解背压呢?笔者认为,在力学中它是一种现象,在Rx中它是一种机制。 在这篇文章中,我们会先介绍背压的相关内容,然后我们再介绍一下`onBackpressureXXX`系列的方法。 关于RxJava2的基础使用和方法梳理可以参考:[RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结](https://juejin.im/post/5b72f76551882561354462dd) 说明:以下文章部分翻译自RxJava官方文档[Backpressure (2.0)](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0))。 ## 1、背压机制 如果将生产和消费整体看作一个管道,生成看作上游,消费看作下游; 那么当异步的应用场景下,当生产者生产过快而消费者消费很慢的时候,可以通过背压来告知上游减慢生成的速度。 通常在进行异步的操作的时候会通过缓存来存储发射出的数据。在早期的RxJava中,这些缓存是无界的。 这意味着当需要缓存的数据非常多的时候,它们可能会占用非常多的存储空间,并有可能因为虚拟机不断GC而导致程序执行过慢,甚至直接抛出OOM。 在最新的RxJava中,大多数的异步操作内部都存在一个有界的缓存,当超出这个缓存的时候就会抛出`MissingBackpressureException`异常并结束整个序列。 然而,某些情况下的表现会有所不同,它们不会抛出`MissingBackpressureException`异常。比如下面的`range`操作: private static void compute(int i) throws InterruptedException { Thread.sleep(500); System.out.println("computing : " + i); } private static void testFlowable() throws InterruptedException { Flowable.range(1, MAX_LENGTH).observeOn(Schedulers.computation()).subscribe(FlowableTest::compute); Thread.sleep(500 * MAX_LENGTH); } 在这段代码中我们生成一段整数,然后每隔500毫秒执行依次计算操作。从输出的结果来看,在程序的实际执行过程中,数据的发射是串行的。 也就是发射完一个数据之后进入`compute`进行计算,等待500毫秒之后才发射下一个。 因此,在程序的执行过程中没有抛出异常,也没有过多的内存消耗。 而下面的这段代码就会在程序运行的时候立刻抛出`MissingBackpressureException`异常: PublishProcessor source = PublishProcessor.create(); source.observeOn(Schedulers.computation()).subscribe(v -> compute(v), Throwable::printStackTrace); for (int i = 0; i < 1_000_000; i++) source.onNext(i); Thread.sleep(10_000); 这是因为`PublishProcessor`底层会调用`PublishSubscription`,而后者实现了`AtomicLong`,它会通过判断引用的long是否为0来抛出异常,这个long型整数会在调用`PublishSubscription.request()`的时候被改写。前面的一个例子的原理就是当每次调用了观察者的`onNext`之后会调用`PublishSubscription.request()`来请求数据,这样相当于消费者会在消费完事件之后向生产者请求,因此整个序列的执行看上去是串行的,从而不会抛出异常。 ## 2、onBackpressureXXX 大多数开发者在遇到`MissingBackpressureException`通常是因为使用`observeOn`方法监听了非背压的`PublishProcessor`, `timer()`, `interval()`或者自定义的`create()`。我们有以下几种方式来解决这个问题: ### 2.1 增加缓存大小 `observeOn`方法的默认缓存大小是16,当生产的速率过快的时候,那么可能很快会超出该缓存大小,从而导致缓存溢出。 一种简单的解决办法是通过提升该缓存的大小来防止缓存溢出,我们可以使用`observeOn`的重载方法来设置缓存的大小。比如: PublishProcessor source = PublishProcessor.create(); source.observeOn(Schedulers.computation(), 1024 * 1024) .subscribe(e -> { }, Throwable::printStackTrace); 但是这种解决方案只能解决暂时的问题,当生产的速率过快的时候还是有可能造成缓存溢出,所以这不是根本的解决办法。 ### 2.2 通过丢弃和过滤来减轻缓存压力 我们可以根据自己的应用的场景和数据的重要性,选择使用一些方法来过滤和丢弃数据。 比如,丢弃的方式可以选择`throttleFirst`, `throttleLast`, `throttleWithTimeout`等,还可以使用按照时间采样的方式来减少接受的数据。 PublishProcessor source = PublishProcessor.create(); source.sample(1, TimeUnit.MILLISECONDS) .observeOn(Schedulers.computation(), 1024) .subscribe(v -> compute(v), Throwable::printStackTrace); 但是,这种方式仅仅用来减少下游接收的数据,当缓存的数据不断增加的时候还是有可能导致缓存溢出,所以,这也不是一种根本的解决办法。 ### 2.3 onBackpressureBuffer() 这种无参的方法会使用一个无界的缓存,只要虚拟机没有抛出OOM异常,它就会把所有的数据缓存起来。 Flowable.range(1, 1_000_000) .onBackpressureBuffer() .observeOn(Schedulers.computation(), 8) .subscribe(e -> { }, Throwable::printStackTrace); 上面的例子即使使用了很小的缓存也不会有异常抛出,因为`onBackpressureBuffer`会将发射的所有数据缓存起来,只会将一小部分的数据传递给`observeOn`。 这种处理方式实际上是不存在背压的,因为`onBackpressureBuffer`缓存了所有的数据,我们可以使用该方法的4个重载方法来对背压进行个性化设置。 ### 2.4 onBackpressureBuffer(int capacity) 这个方法使用一个有界的缓存,当达到了缓存大小的时候会抛出一个`BufferOverflowError`错误。 通过这种方法可以增加默认的缓存大小,但是通过`observeOn`方法一样可以指定缓存的大小,因此,这个方法的应用变得越来越少。 ### 2.5 onBackpressureBuffer(int capacity, Action onOverflow) 这方法除了可以指定一个有界的缓存还提供了一个,当缓存溢出的时候还会回调指定的Action。 但是这种回调的用途比较有限,因为它除了提供当前回调的栈信息以外提供不了任何有用的信息。 ### 2.6 onBackpressureBuffer(int capacity, Action onOverflow, BackpressureOverflowStrategy strategy) 这个重载方法相对比较实用一些,它除了上面的那些功能之外,还指定了当缓存到达指定的缓存时的行为。 这里的`BackpressureOverflowStrategy`顾名思义是一个策略,它是一个枚举类型,预定义了三种枚举值,最终会在`FlowableOnBackpressureBufferStrategy`中根据指定的枚举类型选择不同的实现策略,因此,我们可以使用它来指定缓存溢出时候的行为。 下面是该枚举类型的三个值及其含义: 1. `ERROR`:当缓存溢出的时候会抛出一个异常; 2. `DROP_OLDEST`:当缓存发生溢出的时候,会丢弃最老的值,并将新的值插入到缓存中; 3. `DROP_LATEST`:当缓存发生溢出的时候,最新的值会被忽略,只有比较老的值会被传递给下游使用; 需要注意的地方是,后面的两种策略会造成下游获取到的值是不连续的,因为有一部分值会因为缓存不够被丢弃,但是它们不会抛出`BufferOverflowException`。 ### 2.7 onBackpressureDrop() 这个方法会在数据达到缓存大小的时候丢弃最新的数据。可以将其看成是`onBackpressureBuffer`+`0 capacity`+`DROP_LATEST`的组合。 这个方法特别适用于那种可以忽略从源中发射出值的那种场景,比如GPS定位问题,定位数据会不断发射出来,即使丢失当前数据,等会儿一样能拿到最新的数据。 component.mouseMoves() .onBackpressureDrop() .observeOn(Schedulers.computation(), 1) .subscribe(event -> compute(event.x, event.y)); 该方法还存在一个重载方法`onBackpressureDrop(Consumer onDrop)`,它允许我们传入一个接口来指定当某个数据被丢失时的行为。 ### 2.8 onBackpressureLatest() 对应于`onBackpressureDrop()`的,还有`onBackpressureLatest()`方法,该方法只会保留最新的数据并会覆盖较老、没有分发的数据。 我们可以将其看成是`onBackpressureBuffer`+`1 capacity`+`DROP_OLDEST`的组合。 与`onBackpressureDrop()`不同的地方在于,当下游消费过慢的时候,这种方式总会存在一个缓存的值。 这种特别适用于那种数据的生产非常频繁,但是只有最新的数据会被消费的那种情形。比如,当用户点击了屏幕,那么我们倾向于只处理最新按下的位置的事件。 component.mouseClicks() .onBackpressureLatest() .observeOn(Schedulers.computation()) .subscribe(event -> compute(event.x, event.y), Throwable::printStackTrace); 所以,总结一下: 1. `onBackpressureDrop()`:不会缓存任何数据,专注于当下,新来的数据来不及处理就丢掉,以后会有更好的; 2. `onBackpressureLatest()`:会缓存一个数据,当正在执行某个任务的时候有新的数据过来,会把它缓存起来,如果又有新的数据过来,那就把之前的替换掉,缓存里面的总是最新的。 ## 3、总结 以上就是背压机制的一些内容,以及我们介绍了`Flowable`中的几个背压相关的方法。 实际上,RxJava的官方文档也有说明——`Flowable`适用于数据量比较大的情景,因为它的一些创建方法本身就使用了背压机制。 这部分方法我们就不再一一进行说明,因为,它们的方法签名和`Observable`基本一致,只是多了一层背压机制。 比较匆匆地整理完了背压的内容,但是我想这块还会有更加丰富的内容值得我们去发现和探索。 以上。 ================================================ FILE: 响应式编程/RxJava系列(3):用RxJava打造EventBus.md ================================================ # RxJava2 系列 (3):使用 Subject 在这篇文章中,我们会先分析一下 RxJava2 中的 Subject ;然后,我们会使用 Subject 制作一个类似于 EventBus 的全局的通信工具。 在了解本篇文章的内容之前,你需要先了解 RxJava2 中的一些基本的用法,比如 Observable 以及背压的概念,你可以参考我的其他两篇文章来获取这部分内容:[《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd)和[《RxJava2 系列 (2):背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187)。 ## 1、Subject ### 1.1 Subject 的两个特性 Subject 可以同时代表 Observer 和 Observable,允许从数据源中多次发送结果给多个观察者。除了 onSubscribe(), onNext(), onError() 和 onComplete() 之外,所有的方法都是线程安全的。此外,你还可以使用 toSerialized() 方法,也就是转换成串行的,将这些方法设置成线程安全的。 如果你已经了解了 Observable 和 Observer ,那么也许直接看 Subject 的源码定义会更容易理解: ``` public abstract class Subject extends Observable implements Observer { // ... } ``` 从上面看出,Subject 同时继承了 Observable 和 Observer 两个接口,说明它既是被观察的对象,同时又是观察对象,也就是可以生产、可以消费、也可以自己生产自己消费。所以,我们可以项下面这样来使用它。这里我们用到的是该接口的一个实现 PublishSubject : public static void main(String...args) { PublishSubject subject = PublishSubject.create(); subject.subscribe(System.out::println); Executor executor = Executors.newFixedThreadPool(5); Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> { try { Thread.sleep(i * 200); subject.onNext(i); } catch (InterruptedException e) { e.printStackTrace(); } })); } 根据程序的执行结果,程序在第200, 400, 600, 800, 1000毫秒依次输出了1到5的数字。 在这里,我们用 PublishSubject 创建了一个**主题**并对其监听,然后在线程当中又通知该主题内容变化,整个过程我们都只操作了 PublishSubject 一个对象。显然,使用 Subject 我们可以达到对一个指定类型的值的结果进行监听的目的——我们把值改变之后对应的逻辑写在 subscribe() 方法中,然后每次调用 onNext() 等方法通知结果之后就可以自动调用 subscribe() 方法进行更新操作。 同时,因为 Subject 实现了 Observer 接口,并且在 Observable 等的 subscribe() 方法中存在一个以 Observer 作为参数的方法(如下),所以,Subject 也是可以作为消费者来对事件进行消费的。 public final void subscribe(Observer observer) 以上就是 Subject 的两个主要的特性。 ### 1.2 Subject 的实现类 在 RxJava2 ,Subject 有几个默认的实现,下面我们对它们之间的区别做简单的说明: 1. `AsyncSubject`:只有当 Subject 调用 onComplete 方法时,才会将 Subject 中的**最后一个事件**传递给所有的 Observer。 2. `BehaviorSubject`:该类有创建时需要一个默认参数,该默认参数会在 Subject 未发送过其他的事件时,向注册的 Observer 发送;新注册的 Observer 不会收到之前发送的事件,这点和 PublishSubject 一致。 3. `PublishSubject`:不会改变事件的发送顺序;在已经发送了一部分事件之后注册的 Observer 不会收到之前发送的事件。 4. `ReplaySubject`:无论什么时候注册 Observer 都可以接收到任何时候通过该 Observable 发射的事件。 5. `UnicastSubject`:只允许一个 Observer 进行监听,在该 Observer 注册之前会将发射的所有的事件放进一个队列中,并在 Observer 注册的时候一起通知给它。 对比 PublishSubject 和 ReplaySubject,它们的区别在于新注册的 Observer 是否能够收到在它注册之前发送的事件。这个类似于 EventBus 中的 StickyEvent 即黏性事件,为了说明这一点,我们准备了下面两段代码: private static void testPublishSubject() throws InterruptedException { PublishSubject subject = PublishSubject.create(); subject.subscribe(i -> System.out.print("(1: " + i + ") ")); Executor executor = Executors.newFixedThreadPool(5); Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> { try { Thread.sleep(i * 200); subject.onNext(i); } catch (InterruptedException e) { e.printStackTrace(); } })); Thread.sleep(500); subject.subscribe(i -> System.out.print("(2: " + i + ") ")); Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown()); } private static void testReplaySubject() throws InterruptedException { ReplaySubject subject = ReplaySubject.create(); subject.subscribe(i -> System.out.print("(1: " + i + ") ")); Executor executor = Executors.newFixedThreadPool(5); Disposable disposable = Observable.range(1, 5).subscribe(i -> executor.execute(() -> { try { Thread.sleep(i * 200); subject.onNext(i); } catch (InterruptedException e) { e.printStackTrace(); } })); Thread.sleep(500); subject.subscribe(i -> System.out.print("(2: " + i + ") ")); Observable.timer(2, TimeUnit.SECONDS).subscribe(i -> ((ExecutorService) executor).shutdown()); } 它们的输出结果依次是 PublishSubject的结果:(1: 1) (1: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5) ReplaySubject的结果: (1: 1) (1: 2) (2: 1) (2: 2) (1: 3) (2: 3) (1: 4) (2: 4) (1: 5) (2: 5) 从上面的结果对比中,我们可以看出前者与后者的区别在于新注册的 Observer 并没有收到在它注册之前发送的事件。试验的结果与上面的叙述是一致的。 其他的测试代码这不一并给出了,详细的代码可以参考[Github - Java Advanced](https://github.com/Shouheng88/Java-advanced)。 ## 2、用 RxJava 打造 EventBus ### 2.1 打造 EventBus 清楚了 Subject 的概念之后,让我们来做一个实践——用 RxJava 打造 EventBus。 我们先考虑用一个全局的 PublishSubject 来解决这个问题,当然,这意味着我们发送的事件不是黏性事件。不过,没关系,只要这种实现方式搞懂了,用 ReplaySubject 做一个发送黏性事件的 EventBus 也非难事。 考虑一下,如果要实现这个功能我们需要做哪些准备: 1. **我们需要发送事件并能够正确地接收到事件。**要实现这个目的并不难,因为 Subject 本身就具有发送和接收两个能力,作为全局的之后就具有了全局的注册和通知的能力。因此,不论你在什么位置发送了事件,任何订阅的地方都能收到该事件。 2. **首先,我们要在合适的位置对事件进行监听,并在合适的位置取消事件的监听。如果我们没有在适当的时机释放事件,会不会造成内存泄漏呢?这还是有可能的。**所以,我们需要对注册监听的观察者进行记录,并提供注册和取消注册的方法,给它们在指定的生命周期中进行调用。 好了,首先是全局的 Subject 的问题,我们可以实现一个静态的或者单例的 Subject。这里我们选择使用后者,所以,我们需要一个单例的方式来使用 Subject: public class RxBus { private static volatile RxBus rxBus; private final Subject subject = PublishSubject.create().toSerialized(); public static RxBus getRxBus() { if (rxBus == null) { synchronized (RxBus.class) { if(rxBus == null) { rxBus = new RxBus(); } } } return rxBus; } } 这里我们应用了 DCL 的单例模式提供一个单例的 RxBus,对应一个唯一的 Subject. 这里我们用到了 Subject 的`toSerialized()`,我们上面已经提到过它的作用,就是用来保证 onNext() 等方法的线程安全性。 另外,因为 Observalbe 本身是不支持背压的,所以,我们还需要将该 Observable 转换成 Flowable 来实现背压的效果: public Flowable getObservable(Class type){ return subject.toFlowable(BackpressureStrategy.BUFFER).ofType(type); } 这里我们用到的背压的策略是`BackpressureStrategy.BUFFER`,它会缓存发射结果,直到有消费者订阅了它。而这里的`ofType()`方法的作用是用来过滤发射的事件的类型,只有指定类型的事件会被发布。 然后,我们需要记录订阅者的信息以便在适当的时机取消订阅,这里我们用一个`Map`类型的哈希表来解决。这里的`CompositeDisposable`用来存储 Disposable,从而达到一个订阅者对应多个 Disposable 的目的。`CompositeDisposable`是一个 Disposable 的容器,声称可以达到 O(1) 的增、删的复杂度。这里的做法目的是使用注册观察之后的 Disposable 的 dispose() 方法来取消订阅。所以,我们可以得到下面的这段代码: public void addSubscription(Object o, Disposable disposable) { String key = o.getClass().getName(); if (disposableMap.get(key) != null) { disposableMap.get(key).add(disposable); } else { CompositeDisposable disposables = new CompositeDisposable(); disposables.add(disposable); disposableMap.put(key, disposables); } } public void unSubscribe(Object o) { String key = o.getClass().getName(); if (!disposableMap.containsKey(key)) { return; } if (disposableMap.get(key) != null) { disposableMap.get(key).dispose(); } disposableMap.remove(key); } 最后,对外提供一下 Subject 的订阅和发布方法,整个 EventBus 就制作完成了: public void post(Object o){ subject.onNext(o); } public Disposable doSubscribe(Class type, Consumer next, Consumer error){ return getObservable(type) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(next,error); } ### 2.2 测试效果 我们只需要在最顶层的 Activity 基类中加入如下的代码。这样,我们就不需要在各个 Activity 中取消注册了。然后,就可以使用这些顶层的方法来进行操作了。 protected void postEvent(Object object) { RxBus.getRxBus().post(object); } protected void addSubscription(Class eventType, Consumer action) { Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, LogUtils::d); RxBus.getRxBus().addSubscription(this, disposable); } protected void addSubscription(Class eventType, Consumer action, Consumer error) { Disposable disposable = RxBus.getRxBus().doSubscribe(eventType, action, error); RxBus.getRxBus().addSubscription(this, disposable); } @Override protected void onDestroy() { super.onDestroy(); RxBus.getRxBus().unSubscribe(this); } 在第一个 Activity 中我们对指定的类型的结果进行监听: addSubscription(RxMessage.class, rxMessage -> ToastUtils.makeToast(rxMessage.message)); 然后,我们在另一个 Activity 中发布事件: postEvent(new RxMessage("Hello world!")); 这样当第二个 Activity 中调用指定的发送事件的方法之后,第一个 Activity 就可以接收到发射的事件了。 ## 总结 好了,以上就是 Subject 的使用,如果要用一个词来形容它的话,那么只能是“自给自足”了。就是说,它同时做了 Observable 和 Observer 的工作,既可以发射事件又可以对事件进行消费,可谓身兼数职。它在那种想要对某个值进行监听并处理的情形特别有用。因为它不需要你写多个冗余的类,只要它一个就完成了其他两个类来完成的任务,因而代码更加简洁。 ================================================ FILE: 四大组件/Activity.md ================================================ # Android 基础回顾:Activity 基础 ## 1、Activity 的生命周期 ### 1.1 一般情况下的生命周期 下图是一般情况下一个 Activity 将会经过的生命周期的流程图: ![Activity的生命周期](res/activity_life.png) 关于上图中生命周期方法的说明: 1. **onCreate() / onDestroy()**:onCreate() 表示 Activity 正在被创建,可以用来做初始化工作;onDestroy() 表示 Activity 正在被销毁,可以用来做释放资源的工作; 2. **onStart() / onStop()**:onStart() 在 Activity 从不可见变成可见的时候被调用;onStop() 在 Activity 从可见变成不可见的时候被调用; 3. **onRestart()**:在 Activity 从不可见到变成可见的过程中被调用; 4. **onResume() / onPause()**:onResume() 在 Activity() 可以与用户交互的时候被调用,onPause() 在 Activity 不可与用户交互的时候被调用。 所以根据上面的分析,我们可以将Activity的生命周期概况为:**创建->可见->可交互->不可交互->不可见->销毁**。因此,我们可以得到下面的这张图: ### 1.2 特殊情况下的生命周期 这里我们总结一下在实际的使用过程中可能会遇到的一些 Acitivity 的生命周期过程: 1. **当用户打开新的 Activity 或者切换回桌面**:会经过的生命周期为 `onPause()->onStop()`。因为此时 Activity 已经变成不可见了,当然,如果新打开的 Activity 用了透明主题,那么 onStop() 不会被调用,因此原来的 Activity 只是不能交互,但是仍然可见。 2. **从新的 Activity 回到之前的 Activity 或者从桌面回到之前的 Activity**:会经过的生命周期为 `onRestart()->onStart()-onResume()`。此时是从 onStop() 经 onRestart() 回到 onResume() 状态。 3. 如果在上述 1 的情况下,进入后台的 Activity 因为内存不足被销毁了,那么当再次回到该 Activity 的时候,生命周期方法将会从 onCreate() 开始执行到 onResume()。 4. **当用户按下 Back 键时**:如果当前 Activity 被销毁,那么经过的生命周期将会是 `onPause()->onStop()->onDestroy()`。 具体地,当存在两个 Activity,分别是 A 和 B 的时候,在各种情况下,它们的生命周期将会经过: 1. **Back 键 Home 键** 1. 当用户点击 A 中按钮来到 B 时,假设 B 全部遮挡住了 A,将依次执行:`A.onPause()->B.onCreate()->B.onStart()->B.onResume->A.onStop()`。 2. 接1,此时如果点击 Back 键,将依次执行:`B.onPause()->A.onRestart()->A.onStart()->A.onResume()->B.onStop()->B.onDestroy()`。 3. 接2,此时如果按下 Back 键,系统返回到桌面,并依次执行:`A.onPause()->A.onStop()->A.onDestroy()`。 4. 接2,此时如果按下 Home 键(非长按),系统返回到桌面,并依次执行`A.onPause()->A.onStop()`。由此可见,Back 键和 Home 键主要区别在于是否会执行 onDestroy()。 5. 接2,此时如果长按 Home 键,不同手机可能弹出不同内容,Activity 生命周期未发生变化。 2. **横竖屏切换时 Activity 的生命周期** 1. 不设置 Activity 的 `android:configChanges` 时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次。 2. 设置 Activity 的 `android:configChanges=“orientation”` 时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次。 3. 设置 Activity 的 `android:configChanges=“orientation|keyboardHidden”` 时,切屏不会重新调用各个生命周期,只会执行 onConfiguration() 方法。 ### 1.3 onSaveInstanceState() 和 onRestoreInstanceState() 当 Activity 被销毁的时候回调用 `onSaveInstanceState()` 方法来存储当前的状态。这样当 Activity 被重建的时候,可以在 `onCreate()` 和 `onRestoreInstanceState()` 中恢复状态。 对于 targetAPI 为 28 及以后的应用,该方法会在 `onStop()` 方法之后调用,对于之前的设备,这方法会在 `onStop()` 之前调用,但是无法确定是在 `onPause()` 之前还是之后调用。 `onRestoreInstanceState()` 方法用来恢复之前存储的状态,它会在 `onStart()` 和 `onPostCreate()` 之间被调用。此外,你也可以直接在 `onCreate()` 方法中进行恢复,但是基于这个方法调用的时机,如果有特别需求,可以在这个方法中进行处理。 ## 2、Activity 的启动模式 Activity 共有四种启动模式: 1. **standard**:默认,每次启动的时候会创建一个新的实例,并且被创建的实例所在的栈与启动它的 Activity 是同一个栈。比如,A 启动了 B,那么 B 将会与 A 处在同一个栈。假如,我们使用 Application 的 Context 启动一个 Activity 的时候会抛出异常,这是因为新启动的 Activity 不知道自己将会处于哪个栈。可以在启动 Activity 的时候使用 `FLAG_ACTIVITY_NEW_TASK`。这样新启动的 Acitivyt 将会创建一个新的栈。 2. **singleTop**:栈顶复用,如果将要启动的 Activity 已经位于栈顶,那么将会复用栈顶的 Activity,并且会调用它的 `onNewIntent()`。常见的应用场景是从通知打开 Activity 时。 3. **singleTask**:单例,如果启动它的任务栈中存在该 Activity,那么将会复用该 Activity,并且会将栈内的、它之上的所有的 Activity 清理出去,以使得该 Activity 位于栈顶。常见的应用场景是启动页面、购物界面、确认订单界面和付款界面等。 4. **singleInstance**:这种启动模式会在启动的时候为其指定一个单独的栈来执行。如果用同样的intent 再次启动这个 Activity,那么这个 Activity 会被调到前台,并且会调用其 `onNewIntent()` 方法。 ## 3、Activity 的 Flags 1. **FLAG_ACTIVITY_CLEAR_TOP** : 会清理掉该栈中位于 Activity 上面的所有的 Activity,通常与 FLAG_ACTIVITY_NEW_TASK 配合使用; 2. **FLAG_ACTIVITY_SINGLE_TOP**: 同样等同于 mainfest 中配置的 singleTop; 3. **FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS**: 对应于 mainfest 中的属性为`android:excludeFromRecents="true"`,当用户按了 “最近任务列表” 时,该任务不会出现在最近任务列表中,可达到隐藏应用的目的。 4. **FLAG_ACTIVITY_NO_HISTORY**: 对应于 mainfest 中的 `android:noHistory="true"`。这个 FLAG 启动的 Activity,一旦退出,它不会存在于栈中。 5. **FLAG_ACTIVITY_NEW_TASK**: 等同于 mainfest 中配置的 singleTask. ================================================ FILE: 四大组件/Broadcast.md ================================================ # Android 基础回顾:Broadcast 基础 ## 1、关于广播 广播是 Android 提供的一种全局通信机制。 ### 1.1 分类 1. 按照注册方式:**静态注册和动态注册**两种; 2. 按照作用范围:**本地广播和普通广播**两种,普通广播是全局的,所有应用程序都可以接收到,容易会引起安全问题。本地广播只能够在应用内传递,广播接收器也只能接收应用内发出的广播; 3. 按照是否有序:**有序广播和无序广播**两种,无序广播各接收器接收的顺序无法确定,并且在广播发出之后接收器只能接收,不能拦截和进行其他处理,两者的区别主要体现在发送时调用的方法上。 ### 1.2 实现 #### 1.2.1 静态广播 注册,这里的 StaticBroadcastReceiver 是自定义类: ```xml ``` 我们可以将要实现的逻辑放在这个类的方法中进行执行: ```java public class StaticBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // Do something } } ``` 需要注意 Andrdoid 8.0 之后系统对广播进行了一些限制([官方文档](https://developer.android.google.cn/about/versions/oreo/android-8.0)),具体地: 1. 在 Android 8.0 的平台上,应用不能对大部分的广播进行静态注册,也就是说,不能在AndroidManifest 文件对**有些**广播进行静态注册(注意“有些”,因为不是所有的广播都不能注册)。 2. 当程序运行在后台的时候,静态广播中不能启动服务。比如之前实现闹钟的时候是监听时间变化来实现的,在 8.0 之后就会抛出异常。 解决方式是使用动态注册方式(一般情况下使用动态注册就好了)。 #### 1.2.2 动态广播 与静态广播相似,但是不需要在 Manifest 中进行注册。 ```java // 监听广播:一般在 Activity 的 onCreate() 方法中注册 netWorkChangReceiver = new StaticBroadcastReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); registerReceiver(netWorkChangReceiver, filter); // 取消监听:然后在 Activity 的 onDestroy() 中取消注册 unregisterReceiver(netWorkChangReceiver); ``` **注意当页面被销毁的时候需要取消注册广播!** #### 1.2.3 本地广播 本地广播的核心类是 LocalBroadcastManager,使用它的静态方法 `getInstance()` 获取一个单例之后就可以使用该单例的 `registerReceiver()`、`unregisterReceiver()` 和 `sendBroadcast()` 等方法来进行操作了。 ```java // 获取单例 localBroadcastManager = LocalBroadcastManager.getInstance(this); // 注册广播 IntentFilter filter = new IntentFilter(); filter.addAction("me.shouheng.MyBroadcastReceiver"); localReceiver = new LocalReceiver(); localBroadcastManager.registerReceiver(localReceiver, filter); // 发送广播 Intent intent = new Intent("me.shouheng.MyBroadcastReceiver"); localBroadcastManager.sendBroadcast(intent); // 取消注册 localBroadcastManager.unregisterReceiver(localReceiver); ``` #### 1.2.4 有序广播 在 xml 中进行注册的时候通过 `android:priority` 指定一个范围在 -1000~1000 之间的整数来指定广播的接收顺序。优先级高的会先接收到,优先级相等的话则顺序不确定。并且前面的广播可以在方法中向 Intent 写入数据,后面的广播可以接收到写入的值。 ```xml ``` ================================================ FILE: 四大组件/Fragment.md ================================================ # Android 基础回顾:Fragment 基础 ## 1、Fragment 的生命周期 ### 1.1 Fragment 的生命周期 下面是 Fragment 的生命周期的流程图: ![Fragment的生命周期](res/fragment_lifecycle.png) 从上图可以看出,Fragment 的生命周期相比于 Activity,在创建的过程中增加了 `onAttach()`、`onCreateView()` 和 `onActivityCreated()` 三个方法,在销毁的过程中增加了 `onDestroyView()` 和 `onDetach()` 两个方法。 ### 1.2 Activity 与 Fragment 生命周期对应关系 ![Activity 与 Fragment 生命周期对应关系](res/activity_fragment_lifecycle.png) ================================================ FILE: 四大组件/Service.md ================================================ # Android 基础回顾:Service 基础 Service 主要用于在后台处理一些耗时的逻辑,或者去执行某些需要长期运行的任务。必要的时候我们甚至可以在程序退出的情况下,让 Service 在后台继续保持运行状态。相对于使用线程来实现异步任务的方式,它的安全性更高。(但是 Service 是运行在主线程中的,如果需要实现异步任务,可以单开线程。) ## 1、基础使用示例 ### 1.1 使用示例 首先,通过继承 Service 来定义 Service: ```java public class MyService extends Service { public static final String TAG = "MyService"; @Override public void onCreate() { super.onCreate(); Log.d(TAG, "onCreate() executed"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "onStartCommand() executed"); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy() executed"); } @Override public IBinder onBind(Intent intent) { return new MyBinder(); } class MyBinder extends Binder { public void startDownload() { Log.d("TAG", "startDownload() executed"); } } } ``` 定义了 Service 之后还要在 `AndroidManifest.xml` 中注册才能正常使用: ```xml ``` 然后,就可以使用该 Service 了: ```java // 启动 Service Intent startIntent = new Intent(this, MyService.class); startService(startIntent); // 停止 Service Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent); // 关联 Service 和 Activity Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, connection, BIND_AUTO_CREATE); // 解除 Service 和 Activity 关联 unbindService(connection); private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ } @Override public void onServiceConnected(ComponentName name, IBinder service) { myBinder = (MyService.MyBinder) service; myBinder.startDownload(); } }; ``` 以上就是 Service 的基础使用示例。 ### 1.2 Service 的使用小结 首先是 Service 的生命周期图 ![Service的生命周期图](res/service_life.png) 其他, 1. Service 有绑定模式和非绑定模式,以及这两种模式的混合使用方式。不同的使用方法生命周期方法也不同。 1. **非绑定模式**:当第一次调用 `startService()` 的时候执行的方法依次为 `onCreate()->onStartCommand()`;当 Service 关闭的时候调用 `onDestory()`。 2. **绑定模式**:第一次 `bindService()` 的时候,执行的方法为 `onCreate()->onBind()`;解除绑定的时候会执行 `onUnbind()->onDestory()`。 2. 我们在开发的过程中还必须注意 Service 实例只会有一个,也就是说如果当前要启动的 Service 已经存在了那么就不会再次创建该 Service 当然也不会调用 onCreate() 方法。所以, 1. 当第一次执行 `startService(intent)` 的时候,会调用该 Service 中的 `onCreate()` 和`onStartCommand()` 方法。 2. 当第二次执行 `startService(intent)` 的时候,只会调用该 Service 中的 `onStartCommand()` 方法。(因此已经创建了服务,所以不需要再次调用 `onCreate()` 方法了)。 3. `bindService()` 方法的第三个参数是一个标志位,这里传入 `BIND_AUTO_CREATE` 表示在Activity 和 Service 建立关联后自动创建 Service,这会使得 MyService 中的 `onCreate()` 方法得到执行,但 `onStartCommand()` 方法不会执行。所以,在上面的程序中当调用了`bindService()` 方法的时候,会执行的方法有,Service 的 `onCreate()` 方法,以及 ServiceConnection 的 `onServiceConnected()` 方法。 4. 在 3 中,如果想要停止 Service,需要调用 `unbindService()` 才行。 5. 如果我们既调用了 `startService()`,又调用 `bindService()` 会怎么样呢?这时不管你是单独调用 `stopService()` 还是 `unbindService()`,Service 都不会被销毁,必须要将两个方法都调用 Service 才会被销毁。也就是说,`stopService()` 只会让 Service 停止,`unbindService()` 只会让 Service 和 Activity 解除关联,一个 Service 必须要在既没有和任何 Activity 关联又处理停止状态的时候才会被销毁。 ## 2、Service 与线程 Service 运行在主线程里的,也就是说如果你在 Service 里编写了非常耗时的代码,程序可能会出现ANR。 Service 只意味着不需要前台 UI 的支持,即使 Activity 被销毁,或者程序被关闭,只要进程还在,Service 就可以继续运行。但是我们可以在 Service 中再创建一个子线程,然后在这里去处理耗时逻辑。 虽然也可以在 Activity 中创建线程来执行耗时任务,但是它的缺点在于该线程只能与该 Activity 关联,其他 Activity 无法对其进行控制。 所以,标准的使用是: ```java @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { // 开始执行后台任务 } }).start(); return super.onStartCommand(intent, flags, startId); } class MyBinder extends Binder { public void startDownload() { new Thread(new Runnable() { @Override public void run() { // 执行具体的下载任务 } }).start(); } } ``` 当然,你也可以使用 RxJava 等封装的线程池来实现异步任务。 ## 3、前台 Service 因为 Service 的系统优先级较低,所以当系统出现内存不足情况时,就有可能会回收掉正在后台运行的 Service。我们可以通过使用前台 Service 来解决 Service 可能被回收的问题。它的效果是在系统中显示一个驻留的通知。 前台服务的 ```java public class MyService extends Service { public static final String TAG = "MyService"; @Override public void onCreate() { super.onCreate(); Notification notification = new Notification(R.drawable.ic_launcher, "Msg", System.currentTimeMillis()); Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0); notification.setLatestEventInfo(this, "Title", "Content", pi); startForeground(1, notification); } } ``` ## 4、远程 Service 远程 Service 是一种运行在其他进程中的服务。我们可以在 Manifest 中进行注册的时候通过指定进程来讲服务设置成远程的。远程的服务因为运行在另一个进程中,所以涉及跨进程调用的问题。 可以通过在 `AndroidManifest.xml` 中进行如下设置来将一个 Service 设置在非主线程中: ```xml ``` 也就说当前的 Service 运行在其他的进程了,不会阻碍主进行,从而也不会存在 ANR 了。但是这种方式中的 Service 是无法与 Activity 进行关联的,也就是说调用 `bindService()` 的时候会出现错误。如果我们想要将该 Service 与 Activity 进行关联,就需要使用 AIDL 进行跨进程通信了(IPC)。 要实现跨进程调用,我们可以按照如下步骤来实现: 首先,新建 `MyAIDLService.aidl` 文件: ```java package com.example.servicetest; interface MyAIDLService { int plus(int a, int b); String toUpperCase(String str); } ``` 然后,我们要修改之前 Service 中的 `bind()` 方法: ```java @Override public IBinder onBind(Intent intent) { return mBinder; } MyAIDLService.Stub mBinder = new Stub() { @Override public String toUpperCase(String str) throws RemoteException { if (str != null) { return str.toUpperCase(); } return null; } @Override public int plus(int a, int b) throws RemoteException { return a + b; } }; ``` 然后,我们在 ServiceConnection 中进行如下实现: ```java private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { /* Do nothing. */ } @Override public void onServiceConnected(ComponentName name, IBinder service) { myAIDLService = MyAIDLService.Stub.asInterface(service); try { int result = myAIDLService.plus(3, 5); String upperStr = myAIDLService.toUpperCase("hello world"); } catch (RemoteException e) { e.printStackTrace(); } } }; ``` 也就是使用 `MyAIDLService.Stub.asInterface()` 方法获取 MyAIDLService,并调用 MyAIDLService 的方法。这时候再调用 `bindService()` 就不会出现错误了。 如果我们想要在其他进程(APP)中调用该 Service,我们可以进行如下操作: 首先在 Service 添加 `Intent-Filter`: ```xml ``` 这样,我们就将该 Service 设置成其他程序可访问的了。 然后,在要访问该 Service 的程序中进行如下操作: 1. 将上述定义的 MyAIDLService 连同其包拷贝到当前程序中,即 src 目录下面。 2. 然后在绑定 Service 的时候按照下面的方式绑定: ```java Intent intent = new Intent("com.example.servicetest.MyAIDLService"); bindService(intent, connection, BIND_AUTO_CREATE); private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) {} @Override public void onServiceConnected(ComponentName name, IBinder service) { myAIDLService = MyAIDLService.Stub.asInterface(service); try { int result = myAIDLService.plus(50, 50); String upperStr = myAIDLService.toUpperCase("comes from ClientTest"); } catch (RemoteException e) { e.printStackTrace(); } } }; ``` 这样我们就将该 Service 设置成了其他进程可访问的。 ## 5、IntentService 相比于一般的 Service,IntentService 具有的特征: 1. 会创建独立的线程来处理所有的 `Intent` 请求;。 2. 会创建独立的线程来处理 `onHandleIntent()` 方法实现的代码,无需处理多线程问题。 3. 所有请求处理完成后,`IntentService` 会自动停止,无需调用 `stopSelf()` 方法停止 Service;。 4. 为 Service 的 `onBind()` 提供默认实现,返回 null. 5. 为 Service 的 `onStartCommand()` 提供默认实现,将请求 Intent 添加到队列中。 6. IntentService 内置的是 HandlerThread 作为异步线程,每一个交给 IntentService 的任务都将以队列的方式逐个被执行到,一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。正在运行的 IntentService 的程序相比起纯粹的后台程序更不容易被系统杀死,该程序的优先级是介于前台程序与纯后台程序之间的 关于 IntentService 的源码分析,可以参考下面这篇文章: [Android 多线程编程:IntentService & HandlerThread](https://blog.csdn.net/github_35186068/article/details/83758049) ## 5、Service 保活的问题 我们可以用通过 `setForeground(true)` 来提升 Service 的优先级。当然这并不能保证你得Service 永远不被杀掉,只是提高了他的优先级。这种方式的缺点是会设置一个长期停留的通知,用户体验比较差。 那么如何避免后台进程被杀死? 首先,服务被杀死的情况包含下面三种: 1. 系统根据资源分配情况杀死服务 2. 用户通过 `settings->Apps->Running->Stop` 方式杀死服务 3. 用户通过 `settings->Apps->Downloaded->Force Stop` 方式杀死服务 以及对应的解决办法: 1. 调用 `startForegound()`,让你的 Service 所在的线程成为前台进程; 2. Service 的 `onStartCommond()` 返回 START_STICKY 或 START_REDELIVER_INTENT; 3. Service 的 `onDestroy()` 里面重新启动自己。 关于 `onStartCommond()` 的返回值的总结: |No|可选值|含义| |:-:|:-:|:-| |1|START_STICKY|当 Service 因内存不足而被系统 kill 后,一段时间后内存再次空闲时,系统将会尝试重新创建此 Service,一旦创建成功后将回调 `onStartCommand()` 方法,但其中的 Intent 将是 null,除非有挂起的 Intent,如 pendingintent,这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务| |2|START_NOT_STICKY|当 Service 因内存不足而被系统 kill 后,即使系统内存再次空闲时,系统也不会尝试重新创建此 Service。除非程序中再次调用 `startService()` 启动此 Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务| |3|START_REDELIVER_INTENT|当 Service 因内存不足而被系统 kill 后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 `onStartCommand()`,任何挂起 Intent 均依次传递。与START_STICKY 不同的是,其中的传递的 Intent 将是非空,是最后一次调用 `startService()` 中的 intent。这个值适用于主动执行应该立即恢复的作业(例如下载文件)的服务| 在 `onDestroy()` 中自启的示例: ```java public void onCreate() { super.onCreate(); mBroadcast = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Intent a = new Intent(ServiceA.this, ServiceA.class); startService(a); } }; mIF = new IntentFilter(); mIF.addAction("listener"); registerReceiver(mBroadcast, mIF); } @Override public void onDestroy() { super.onDestroy(); Intent intent = new Intent(); intent.setAction("listener"); sendBroadcast(intent); unregisterReceiver(mBroadcast); } ``` 上面的这种启动的实例会因为系统处于后台线程而抛出异常。 参考: 1. [Android Service完全解析,关于服务你所需知道的一切(上)](http://blog.csdn.net/guolin_blog/article/details/11952435) 2. [Android Service完全解析,关于服务你所需知道的一切(下)](http://blog.csdn.net/guolin_blog/article/details/9797169) 3. [关于Android Service真正的完全详解,你需要知道的一切](https://blog.csdn.net/javazejian/article/details/52709857) ================================================ FILE: 图片加载/Android相机最佳实践.md ================================================ # CameraX:Android 相机库开发实践 ## 前言 前段时间因为工作的需要对项目中的相机模块进行了优化,我们项目中的相机模块是基于开源库 CameraView 进行开发的。那次优化主要包括两个方面,一个是相机的启动速度,另一个是相机的拍摄的清晰度的问题。因为时间仓促,那次只是在原来的代码的基础之上进行的优化,然而那份代码本身存在一些问题,导致相机的启动速度无法进一步提升。所以,我准备自己开发一款功能完善,并且可拓展的相机库,于是 [CameraX](https://github.com/Shouheng88/CameraX) 就诞生了。 ## Android 相加开源库的现状 要使用 Android 相机实现图片拍照功能本身并不复杂,Camera1 + SurfaceView 就可以搞定。但是如果让相机能够自由拓展,就需要花费很多的功夫。我所接触的开源库包括 Google 非官方的 CameraView,以及 CameraFragment. 两个库的设计有各自的优点和缺点。 |开源库|优点|缺点| |:-|:-|:-| |CameraView|1.支持基本的拍照、缩放等功能;2.支持自定义图片的宽高比;3.支持多种预览布局方式;|1.每次获取相机支持的尺寸的时候,会先将其组装到一个有序的 Set 中,这个过程会占用一定的启动时间;2.不支持拍摄视频;3.代码堆砌,结构混乱| |CameraFragment|1.支持拍摄照片和视频;2.代码结构清晰|1.不支持缩放;2.默认宽高比4:3,无法运行时修改;3.必须基于 Fragment| 以上是两个开源库的优点和缺点,而我们可以结合它们的优缺点实现一个更加完善的相机库,同时对性能的优化和用户自定义配置,我们也提供了更多的可用的接口。 ## CameraX 整体结构设计 虽然文章的题目是相机开发实践,但是我们并不打算介绍太多关于如何使用 Camera API 的内容,因为本项目是开源的,读者可以自行 Fork 代码进行阅读。在这里,我们只对项目中的一些关键部分的设计思路进行说明。 ![相机整体架构](res/开发.png) 连接:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049 以上是我们相机库的整体架构的设计图,这里笔者使用了 UML 建模进行基础的架构设计(当然,并非严格遵循 UML 建模的语言规则)。下面,我们介绍下项目的关键部分的设计思路。 ### Camera1 还是 Camera2? 了解 Android 相机 API 的同学可能知道,在 LoliPop 上面提出了 Camera2 API. 就笔者个人的实践开发的效果来看,Camera2 相机的性能确实比 Camera1 要好得多,这体现在相机对焦的速率和相机启动的速率上。当然,这和硬件也有一定的关系。Camera2 比 Camera1 使用起来确实复杂得多,但提供的可以调用的 API 也更丰富。Camera2 的另一个问题是国内的很多手机设备对 Camera2 的支持并不好。 对于这个问题,首先,我们可以根据系统的参数来判断该设备是否支持 Camera2: ```java @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static boolean hasCamera2(Context context) { if (context == null) return false; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return false; try { CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); assert manager != null; String[] idList = manager.getCameraIdList(); boolean notNull = true; if (idList.length == 0) { notNull = false; } else { for (final String str : idList) { if (str == null || str.trim().isEmpty()) { notNull = false; break; } final CameraCharacteristics characteristics = manager.getCameraCharacteristics(str); Integer iSupportLevel = characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); if (iSupportLevel != null && iSupportLevel == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) { notNull = false; break; } } } return notNull; } catch (Throwable ignore) { return false; } } ``` 不过,即便上面方法返回的结果标明支持 Camera2,但相机仍然可能在启动中出现异常。所以 CameraView 的解决方案是,相机启动的方法返回一个 boolean 类型标明 Camera2 是否启动成功,如果失败了,就降级并使用 Camera1。但是降级的过程会浪费一定的启动时间,因此,有人提出了使用 SharedPreferences 存储降级的记录,下次直接使用 Camera1 的解决方案。 上面两种方案各自有优缺点,使用第二种方案意味着你要修改相机库的源代码,而我们希望以一种更加灵活的方式提供给用户选择相机的权力。没错,就是**策略设计模式**。 因为虽然 Camera1 和 Camera2 的 API 设计和使用不同,但是我们并不需要知道内部如何实现,我们只需要给用户提供切换相机、打开闪光灯、拍照、缩放等的接口即可。在这种情况下,当然使用**门面设计模式**是最好的选择。 另外,对于 TextureView 还是 SurfaceView 的选择,我们也使用了**策略模式+门面模式**的思路。 即。对于相机的选择,我们提供门面 CameraManager 接口,Camera1 的实现类 Camera1Manager 以及 Camera2 的实现类 Camera2Manager. Camera1Manager 和 Camera2Manager 又统一继承自 BaseCameraManager. 这里的 BaseCameraManager 是一个抽象类,用来封装一些通用的相机方法。 所以问题到了是 Camera1Manager 还是 Camera2Manager 的问题。这里我们提供了策略接口 CameraManagerCreator,它返回 CameraManager: ```java public interface CameraManagerCreator { CameraManager create(Context context, CameraPreview cameraPreview); } ``` 以及一个默认的实现: ```java public class CameraManagerCreatorImpl implements CameraManagerCreator { @Override public CameraManager create(Context context, CameraPreview cameraPreview) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && CameraHelper.hasCamera2(context)) { return new Camera2Manager(cameraPreview); } return new Camera1Manager(cameraPreview); } } ``` 因此,我们只需要在相机的全局配置中指定自己的 CameraManager 创建策略就可以使用指定的相机了。 ### 全局配置 之前考虑指定 CameraManager 创建策略的时候,思路是直接对静态的变量赋值的方式,不过后来考虑到对相机的支持的尺寸进行缓存的问题,所以将其设计了静态单实例的类: ```java public class ConfigurationProvider { private static volatile ConfigurationProvider configurationProvider; private ConfigurationProvider() { if (configurationProvider != null) { throw new UnsupportedOperationException("U can't initialize me!"); } initWithDefaultValues(); } public static ConfigurationProvider get() { if (configurationProvider == null) { synchronized (ConfigurationProvider.class) { if (configurationProvider == null) { configurationProvider = new ConfigurationProvider(); } } } return configurationProvider; } // ... ... } ``` 除了指定一些全局的配置之外,我们还可以在 ConfigurationProvider 中缓存一些相机的信息,比如相机支持的尺寸的问题。因为相机所支持的尺寸属于相机属性的一部分,是不变的,我们没有必要获取多次,可以将其缓存起来,下次直接使用。当然,我们还提供了不使用缓存的接口: ```java public class ConfigurationProvider { // ... private boolean useCacheValues; private List pictureSizes; public List getPictureSizes(android.hardware.Camera camera) { if (useCacheValues && pictureSizes != null) { return pictureSizes; } List sizes = Size.fromList(camera.getParameters().getSupportedPictureSizes()); if (useCacheValues) { pictureSizes = sizes; } return sizes; } } ``` 这样,我们在获取相机支持的图片尺寸信息的时候只需要传入 Camera 即可使用缓存的信息。当然,缓存信息在某些极端的情况下可能会带来问题,比如从 Camera1 切换到 Camera2 的时候,需要清除缓存。 *注:这里缓存的时候应该使用 SoftReference,但是考虑到数据量不大,没有这么设计,以后会考虑修改。* ### 输出媒体文件的尺寸的问题 使用 Android 相机一个让人头疼的地方是计算尺寸的问题:因为相机支持的尺寸有三种,包括相片的支持尺寸、预览的支持尺寸和视频的支持尺寸。预览的尺寸决定了用户看到的画面的清晰程度,但是真正拍摄出图片的清晰度取决于相片的尺寸,同理输出的视频的尺寸取决于视频的尺寸。 在 CameraView 中,它允许你指定一个图片的尺寸,当没有满足的要求的尺寸的时候会 Crash…这样的处理方式是将其不好的,因为用户根本无法确定相机最大的支持尺寸,而 CameraView 甚至没有提供获取相机支持尺寸的接口…… 为了解决这个问题,我们首先提供了一系列用户获取相机支持尺寸的接口: ```java Size getSize(@Camera.SizeFor int sizeFor); SizeMap getSizes(@Camera.SizeFor int sizeFor); ``` 这里的 SizeFor 是基于注解的枚举,我们通过它来判断用户是希望获取相片、预览还是视频的尺寸信息。这里的 SizeMap 是一个哈希表,从相机的宽高比映射到对应的尺寸列表。跟 CameraView 处理方式不同的是,我们只有在调用上述方法的时候才计算图片的宽高比信息,虽然调用下面的方法的时候会花费一丁点儿时间,但是相机的启动速度大大提升了: ```java @Override public SizeMap getSizes(@Camera.SizeFor int sizeFor) { switch (sizeFor) { case Camera.SIZE_FOR_PREVIEW: if (previewSizeMap == null) { previewSizeMap = CameraHelper.getSizeMapFromSizes(previewSizes); } return previewSizeMap; case Camera.SIZE_FOR_PICTURE: if (pictureSizeMap == null) { pictureSizeMap = CameraHelper.getSizeMapFromSizes(pictureSizes); } return pictureSizeMap; case Camera.SIZE_FOR_VIDEO: if (videoSizeMap == null) { videoSizeMap = CameraHelper.getSizeMapFromSizes(videoSizes); } return videoSizeMap; } return null; } ``` 获取了相机的尺寸信息的目的当然是将其设置到相机上面,所以我们提供了两个用来设置相机尺寸的接口: ```java void setExpectSize(Size expectSize); void setExpectAspectRatio(AspectRatio expectAspectRatio); ``` 它们一个用来指定期望的输出文件的尺寸,一个用来指定期望的图片的宽高比。 OK,既然用户可以指定计算参数,那么怎么计算呢?这当然还是用户说了算的,因为我们一样在全局配置中为用户提供了计算的策略接口: ```java public interface CameraSizeCalculator { Size getPicturePreviewSize(@NonNull List previewSizes, @NonNull Size pictureSize); Size getVideoPreviewSize(@NonNull List previewSizes, @NonNull Size videoSize); Size getPictureSize(@NonNull List pictureSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize); Size getVideoSize(@NonNull List videoSizes, @NonNull AspectRatio expectAspectRatio, @Nullable Size expectSize); } ``` 当然,我们也会提供一个默认的计算策略。在 CameraManager 内部,我们会在需要的地方调用上述接口的方法以获取最终的相机尺寸信息: ```java private void adjustCameraParameters(boolean forceCalculateSizes, boolean changeFocusMode, boolean changeFlashMode) { Size oldPreview = previewSize; long start = System.currentTimeMillis(); CameraSizeCalculator cameraSizeCalculator = ConfigurationProvider.get().getCameraSizeCalculator(); android.hardware.Camera.Parameters parameters = camera.getParameters(); if (mediaType == Media.TYPE_PICTURE && (pictureSize == null || forceCalculateSizes)) { pictureSize = cameraSizeCalculator.getPictureSize(pictureSizes, expectAspectRatio, expectSize); previewSize = cameraSizeCalculator.getPicturePreviewSize(previewSizes, pictureSize); parameters.setPictureSize(pictureSize.width, pictureSize.height); notifyPictureSizeUpdated(pictureSize); } // ... ... } ``` ### 性能优化 为了对相机的性能进行优化,笔者可是花了大量的精力。因为在之前进行优化的时候积累了一些经验,所以这次开发的时候就容易得多。下面是 TraceView 进行分析的图: ![Android 相机 TraceView 分析](res/QQ图片20190423230233.png) 可以看出从相机当中获取支持尺寸的本身会占用一定时间的,而这种属于相机固有的信息,一般是不会发生变化的,所以我们可以通过将其缓存起来来提升下一次打开相机的速率。 整体上,该项目的优化主要体现在几个地方: 1. 使用注解+常量取代枚举:因为枚举占用的内存空间比较大,而单纯使用注解无法约束输入参数的范围。这在 enums 包下面可以看到,这也是 Android 性能优化最常见的手段之一。 2. 延迟初始化:我们为了达到只在使用到某些数据的时候才初始化的目的采用了延迟初始化的解决方案,比如 Size 的宽高比的问题: ``` public class Size { // ... private double ratio; public double ratio() { if (ratio == 0 && width != 0) { ratio = (double) height / width; } return ratio; } } ``` 3. 数据结构的应用和选择:选择合适的数据结构和自定义数据结构往往能起到化腐朽为神奇的作用。比如 SizeMap ```java public class SizeMap extends HashMap> { } ``` 比如在列表数据结构的应用上面,使用 ArrayList 但是提前指定数组大小,减小数组扩容的次数: ```java public static List fromList(@NonNull List cameraSizes) { List sizes = new ArrayList<>(cameraSizes.size()); for (Camera.Size size : cameraSizes) { sizes.add(of(size.width, size.height)); } return sizes; } ``` 4. 缓存,这个我们之前已经提到过,除了尺寸信息我们还缓存了一些其他的信息,具体可以参考源码。 5. 异步线程:这个当然是最能提升应用相应速度的方式。它能够让我们不阻塞主线程,从而提升界面相应的速度。但是在相机开发的时候存在一个问题,即通常打开的相机的时候比较耗时,所以放在异步线程中;而开启预览处于主线程,这很容易因为线程执行的顺序的问题导致一些难以预测的异常。在之前,笔者的解决方案是使用一个私有锁来实现线程的控制。 ## 总结 本次相机库开发占用的时间其实不多,更多的时间花费在了 UML 建模图的设计和在真正开发之前收集资料信息。不得不说,如果你开发一个小的项目,不需要做什么设计,直接就可以上了,但是如果你设计一个比较复杂的库,花费更多时间在 UML 建模上面是值得的,因为它能让你的开发思路更加清晰。另外,为了开发 Camera2,笔者不仅找遍了开源库,还翻译了相关的官方文档,这在开源项目中会一并奉上。 ### 相机目前支持的功能 |编号|功能| |:-|:-| |1|拍摄照片| |2|拍摄视频| |3|指定使用 Camera1 还是 Camera2| |4|指定使用 TextureView 还是 SurfaceView| |5|闪光灯打开和关闭| |6|自动对焦的选择| |7|前置和后置相机| |8|快门声| |9|指定缩放的大小| |10|指定期望的图片大小| |11|指定期望的图片宽高比| |12|获取支持的图片、预览和视频的尺寸信息| |13|相机尺寸发生变化监听| |14|输出视频的文件位置| |15|输出视频的时间长度| |16|手指界面滑动的监听| |17|触摸进行缩放| |18|预览自适应和裁剪等| |19|缓存相机信息,清除和不适用缓存信息| ### 最后是关于项目的一些小问题 该项目目前所有功能已经开发完毕,不过仍有一些小的问题需要完善: 1. Camera2 预览放大之后拍摄出的图片没有放大效果的问题; 2. Camera1 拍摄出的图片需要旋转 90 度; 3. Camera2 在屏幕旋转成横屏之后相机预览需要同时选择 90 度的问题; 4. Camera1 和 Camera2 切换存在一些问题。 另外,由于时间限制,该相机库目前没有进行严格的测试,所以建议使用的时候进行充分测试之后再使用。 ### 是否会继续完善该项目? 是的,包括对相机的功能进行充分测试。只是目前的时间结点,笔者有其他的事务需要处理,所以先把它介绍给读者。当然也希望能够有更多感兴趣的朋友对该项目贡献代码。 ## 项目地址: 1. 项目地址:https://github.com/Shouheng88/CameraX 2. UML 建模图地址:https://www.processon.com/view/link/5c976af8e4b0d1a5b10a4049 3. 笔者翻译的Camera2 文档:https://github.com/Shouheng88/Android-notes/blob/master/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/Android%E7%9B%B8%E6%9C%BACamera2%E8%B5%84%E6%96%99.md ================================================ FILE: 图片加载/Glide系列:Glide主流程源码分析.md ================================================ # Glide 系列-2:主流程源码分析(4.8.0) Glide 是 Android 端比较常用的图片加载框架,这里我们就不再介绍它的基础的使用方式。你可以通过查看其官方文档学习其基础使用。这里,我们给出一个 Glide 的最基本的使用示例,并以此来研究这个整个过程发生了什么: ```java Glide.with(fragment).load(myUrl).into(imageView); ``` 上面的代码虽然简单,但是整个执行过程涉及许多类,其流程也比较复杂。为了更清楚地说明这整个过程,我们将 Glide 的图片加载按照调用的时间关系分成了下面几个部分: 1. `with()` 方法的执行过程 2. `load()` 方法的执行过程 3. `into()` 方法的执行过程 1. 阶段1:开启 `DecodeJob` 的过程 2. 阶段2:打开网络流的过程 3. 阶段3:将输入流转换为 `Drawable` 的过程 4. 阶段4:将 `Drawable` 展示到 `ImageView` 的过程 即按照上面的示例代码,先分成 `with()`、`load()` 和 `into()` 三个过程,而 `into()` 过程又被细化成四个阶段。 下面我们就按照上面划分的过程来分别介绍一下各个过程中都做了哪些操作。 ## 1、with() 方法的执行过程 ### 1.1 实例化单例的 Glide 的过程 当调用了 Glide 的 `with()` 方法的时候会得到一个 `RequestManager` 实例。`with()` 有多个重载方法,我们可以使用 `Activity` 或者 `Fragment` 等来获取 `Glide` 实例。它们最终都会调用下面这个方法来完成最终的操作: ```java public static RequestManager with(Context context) { return getRetriever(context).get(context); } ``` 在 `getRetriever()` 方法内部我们会先使用 `Glide` 的 `get()` 方法获取一个单例的 Glide 实例,然后从该 Glide 实例中得到一个 `RequestManagerRetriever`: ```java private static RequestManagerRetriever getRetriever(Context context) { return Glide.get(context).getRequestManagerRetriever(); } ``` 这里调用了 Glide 的 `get()` 方法,它最终会调用 `initializeGlide()` 方法实例化一个**单例**的 `Glide` 实例。在之前的文中我们已经介绍了这个方法。它主要用来从注解和 Manifest 中获取 GlideModule,并根据各 GlideModule 中的方法对 Glide 进行自定义: [《Glide 系列-1:预热、Glide 的常用配置方式及其原理》](Glide系列:Glide的配置和使用方式.md) 下面的方法中需要传入一个 `GlideBuilder` 实例。很明显这是一种构建者模式的应用,我们可以使用它的方法来实现对 Glide 的个性化配置: ```java private static void initializeGlide(Context context, GlideBuilder builder) { // ... 各种操作,略 // 赋值给静态的单例实例 Glide.glide = glide; } ``` 最终 Glide 实例由 `GlideBuilder` 的 `build()` 方法构建完毕。它会直接调用 Glide 的构造方法来完成 Glide 的创建。在该构造方法中会将各种类型的图片资源及其对应的加载类的映射关系注册到 Glide 中,你可以阅读源码了解这部分内容。 ### 1.2 Glide 的生命周期管理 在 `with()` 方法的执行过程还有一个重要的地方是 Glide 的生命周期管理。因为当我们正在进行图片加载的时候,Fragment 或者 Activity 的生命周期可能已经结束了,所以,我们需要对 Glide 的生命周期进行管理。 Glide 对这部分内容的处理也非常巧妙,它使用没有 UI 的 Fragment 来管理 Glide 的生命周期。这也是一种非常常用的生命周期管理方式,比如 `RxPermission` 等框架都使用了这种方式。你可以通过下面的示例来了解它的作用原理: [示例代码:使用 Fragment 管理 onActivityResult()](https://github.com/Shouheng88/Android-references/tree/master/advanced/src/main/java/me/shouheng/advanced/callback) 在 `with()` 方法中,当我们调用了 `RequestManagerRetriever` 的 `get()` 方法之后,会根据 Context 的类型调用 `get()` 的各个重载方法。 ```java public RequestManager get(@NonNull Context context) { if (context == null) { throw new IllegalArgumentException("You cannot start a load on a null Context"); } else if (Util.isOnMainThread() && !(context instanceof Application)) { if (context instanceof FragmentActivity) { return get((FragmentActivity) context); } else if (context instanceof Activity) { return get((Activity) context); } else if (context instanceof ContextWrapper) { return get(((ContextWrapper) context).getBaseContext()); } } return getApplicationManager(context); } ``` 我们以 Activity 为例。如下面的方法所示,当当前位于后台线程的时候,会使用 Application 的 Context 获取 `RequestManager`,否则会使用无 UI 的 Fragment 进行管理: ```java public RequestManager get(@NonNull Activity activity) { if (Util.isOnBackgroundThread()) { return get(activity.getApplicationContext()); } else { assertNotDestroyed(activity); android.app.FragmentManager fm = activity.getFragmentManager(); return fragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); } } ``` 然后就调用到了 `fragmentGet()` 方法。这里我们从 `RequestManagerFragment` 中通过 `getGlideLifecycle()` 获取到了 `Lifecycle` 对象。`Lifecycle` 对象提供了一系列的、针对 Fragment 生命周期的方法。它们将会在 Fragment 的各个生命周期方法中被回调。 ```java private RequestManager fragmentGet(Context context, FragmentManager fm, Fragment parentHint, boolean isParentVisible) { RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible); RequestManager requestManager = current.getRequestManager(); if (requestManager == null) { Glide glide = Glide.get(context); requestManager = factory.build( glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context); current.setRequestManager(requestManager); } return requestManager; } ``` 然后,我们将该 `Lifecycle` 传入到 `RequestManager` 中,以 `RequestManager` 中的两个方法为例,`RequestManager` 会对 `Lifecycle` 进行监听,从而达到了对 Fragment 的生命周期进行监听的目的: ```java public void onStart() { resumeRequests(); targetTracker.onStart(); } public void onStop() { pauseRequests(); targetTracker.onStop(); } ``` ### 1.3 小结 经过上述分析,我们可以使用下面的流程图总结 Glide 的 `with()` 方法的执行过程: ![Glide 的 with() 方法的执行过程](res/glide_with.jpg) ## 2、load() 方法的执行过程 ### 2.1 load() 的过程 当我们拿到了 `RequestManager` 之后就可以使用它来调用 `load()` 方法了。在我们的示例中传入的是一个 url 对象。`load()` 方法也是重载的,我们可以传入包括 Bitmap, Drawable, Uri 和 String 等在内的多种资源类型。示例中会调用下面的这个方法得到一个 `RequestBuilder` 对象,显然这是一种构建者模式的应用。我们可以使用 `RequestBuilder` 的其他方法来继续构建图片加载请求,你可以通过查看它的源码了解 Glide 都为我们提供了哪些构建方法: ```java public RequestBuilder load(@Nullable String string) { return loadGeneric(string); } ``` 在 `RequestBuilder` 的构造方法中存在一个 `apply()` 方法值得我们一提,其定义如下。从下面的方法定义中可以看出,我们可以通过为 `RequestBuilder` 指定 `RequestOptions` 来配置当前图片加载请求。比如,指定磁盘缓存的策略,指定占位图,指定图片加载出错时显示的图片等等。那么我们怎么得到 `RequestOptions` 呢?在 Glide 4.8.0 中的类 `RequestOptions` 为我们提供了一系列的静态方法,我们可以这些方法来得到 `RequestOptions` 的实例: ```java public RequestBuilder apply(RequestOptions requestOptions) { Preconditions.checkNotNull(requestOptions); this.requestOptions = getMutableOptions().apply(requestOptions); return this; } ``` 回过头来,我们可以继续跟踪 `load()` 方法。其实,不论我们使用了 `load()` 的哪个重载方法,最终都会调用到下面的方法。它的逻辑也比较简单,就是将我们的图片资源信息赋值给 `RequestBuilder` 的局部变量就完事了。至于图片如何被加载和显示,则在 `into()` 方法中进行处理。 ```java public RequestBuilder load(@Nullable String string) { return loadGeneric(string); } private RequestBuilder loadGeneric(@Nullable Object model) { this.model = model; isModelSet = true; return this; } ``` ### 2.2 小结 所以,我们可以总结 Glide 的 `load()` 方法的执行过程如下。也就是使用 `RequestManger` 得到一个 `RequestBuilder` 的过程: ![Glide 的 load() 方法执行过程](res/glide_load.jpg) ## 3、into() 方法的执行过程 考虑到 `into()` 方法流程比较长、涉及的类比较多,我们按照图片加载的过程将其分成四个阶段来进行介绍。 第一个阶段是开启 `DecodeJob` 的过程。`DecodeJob` 负责从缓存或者从原始的数据源中加载图片资源,对图片进行变换和转码,是 Glide 图片加载过程的核心。`DecodeJob` 继承了 `Runnable`,实际进行图片加载的时候会将其放置到线程池当中执行。这个阶段我们重点介绍的是从 `RequestBuilder` 构建一个 `DecodeJob` 并开启 `DecodeJob` 任务的过程。即构建一个 `DecodeJob` 并将其丢到线程池里的过程。 第二个阶段是打开网络流的过程。这个阶段会根据我们的图片资源来从数据源中加载图片数据。以我们的示例为例,在默认情况下会从网络当中加载图片,并得到一个 `InputStream`. 第三个阶段是将输入流转换为 `Drawable` 的过程。得到了 `InputStream` 之后还要调用 `BitmapFactory` 的 `decodeStream()` 方法来从 `InputStream` 中得到一个 `Drawable`. 第四个阶段是将 `Drawable` 显示到 `ImageView` 上面的过程。 ### 3.1 阶段1:开启 DecodeJob 的过程 #### 3.1.1 流程分析 我们继续沿着 `into()` 方法进行分析。 `into()` 方法也定义在 `RequestBuilder` 中,并且也是重载的。不论我们调用哪个重载方法都会将要用来显示图片的对象封装成一个 `Target` 类型。`Target` 主要用来对用来显示图片的对象的生命周期进行管理。当我们要将图片加载到 ImageView 的时候,最终会调用下面的 `buildTarget()` 方法来讲我们的 ImageView 封装成一个 `ViewTarget`,然后调用 `into()` 的重载方法进行后续处理: ```java public ViewTarget buildTarget(ImageView view, Class clazz) { if (Bitmap.class.equals(clazz)) { return (ViewTarget) new BitmapImageViewTarget(view); } else if (Drawable.class.isAssignableFrom(clazz)) { return (ViewTarget) new DrawableImageViewTarget(view); } else { throw new IllegalArgumentException( "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); } } private > Y into(Y target, RequestListener targetListener, RequestOptions options) { options = options.autoClone(); Request request = buildRequest(target, targetListener, options); // 1 Request previous = target.getRequest(); if (request.isEquivalentTo(previous) && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) { request.recycle(); if (!Preconditions.checkNotNull(previous).isRunning()) { previous.begin(); } return target; } requestManager.clear(target); target.setRequest(request); requestManager.track(target, request); // 2 return target; } ``` 在上面的 `into()` 方法的 `1` 处最终会调用到下面的方法来构建一个请求对象。(这里我们忽略掉具体的参数,只给看构建请求的逻辑)。简而言之,该方法会根据我们是否调用过 `RequestBuilder` 的 `error()` 方法设置过图片加载出错时候显示的图片来决定返回 `mainRequest` 还是 `errorRequestCoordinator`。因为我们没有设置该参数,所以会直接返回 `mainRequest`。 ```java private Request buildRequestRecursive(/*各种参数*/) { ErrorRequestCoordinator errorRequestCoordinator = null; if (errorBuilder != null) { errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator); parentCoordinator = errorRequestCoordinator; } Request mainRequest = buildThumbnailRequestRecursive(/*各种参数*/); // 1 if (errorRequestCoordinator == null) { return mainRequest; } // ... 略 Request errorRequest = errorBuilder.buildRequestRecursive(/*各种参数*/); errorRequestCoordinator.setRequests(mainRequest, errorRequest); return errorRequestCoordinator; } ``` 上面是根据是否设置加载失败时显示的图片来决定返回的请求对象的。如果你使用过 Glide 的话,那么一定记得除了设置加载失败时的图片,我们还会先加载一张小图,即 `Thumbnail`。所以,在上面方法的 `1` 处会根据设置调用过 `RequestBuilder` 的 `thumbnail()` 方法来决定返回 `Thumbnail` 的请求还是真实图片的请求。同样因为我们没有设置过该方法,所以最终会调用下面的方法来构建最终的图片加载请求。 ```java private Request obtainRequest(/*各种参数*/) { return SingleRequest.obtain(/*各种参数*/); } ``` 在 `SingleRequest` 的 `obtain()` 方法中会先尝试从请求的池中取出一个请求,当请求不存在的时候就会实例化一个 `SingleRequest`,然后调用它的 `init()` 方法完成请求的初始化工作。这里的请求池使用了 Android 的 support v4 包中的 `Pool` 相关的 API. 它被设计用来构建基于数组的请求池,具体如何使用可以参考相关的文档和源码。 ```java public static SingleRequest obtain(/*各种参数*/) { SingleRequest request = (SingleRequest) POOL.acquire(); if (request == null) { request = new SingleRequest<>(); } request.init(/*各种参数*/); return request; } ``` 得到了请求之后会用 `RequestManager` 的 `track()` 方法: ```java void track(@NonNull Target target, @NonNull Request request) { targetTracker.track(target); requestTracker.runRequest(request); } ``` 该方法的主要作用有两个: 1. 调用 `TargetTracker` 的 `track()` 方法对对当前 `Target` 的生命周期进行管理; 2. 调用 `RequestTracker` 的 `runRequest()` 方法对当前请求进行管理,当 Glide 未处于暂停状态的时候,会直接使用 `Request` 的 `begin()` 方法开启请求。 下面是 `SingeleRequest` 的 `begin()` 方法。它会根据当前加载的状态来判断应该调用哪个方法。因为我们之前图片加载的过程可能因为一些意想不到的原因被终止,所以当重启的时候就需要根据之前的状态进行恢复。对于我们第一次加载的情况,则会直接进入到下方 1 处的 `onSizeReady()` 方法中: ```java public void begin() { if (model == null) { if (Util.isValidDimensions(overrideWidth, overrideHeight)) { width = overrideWidth; height = overrideHeight; } int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG; onLoadFailed(new GlideException("Received null model"), logLevel); return; } if (status == Status.RUNNING) { throw new IllegalArgumentException("Cannot restart a running request"); } // 如果我们在完成之后重新启动(通常通过诸如 notifyDataSetChanged() 之类的方法, // 在相同的目标或视图中启动相同的请求),我们可以使用我们上次检索的资源和大小 // 并跳过获取新的大小。所以,如果你因为 View 大小发生了变化而想要重新加载图片 // 就需要在开始新加载之前清除视图 (View) 或目标 (Target)。 if (status == Status.COMPLETE) { onResourceReady(resource, DataSource.MEMORY_CACHE); return; } status = Status.WAITING_FOR_SIZE; if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); // 1 } else { target.getSize(this); } if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE) && canNotifyStatusChanged()) { target.onLoadStarted(getPlaceholderDrawable()); // 2 } } ``` 下面是 `onSizeReady()` 方法,我们可以看出它会先判断当前是否处于 `Status.WAITING_FOR_SIZE` 状态,并随后将状态更改为 `Status.RUNNING` 并调用 `engine` 的 `load()` 方法。显然,更改完状态之后继续回到上面的方法,在 2 处即调用了 `Target` 的 `onLoadStarted()` 方法。这样 `Target` 的第一个生命周期就被触发了。 ```java public void onSizeReady(int width, int height) { if (status != Status.WAITING_FOR_SIZE) { return; } status = Status.RUNNING; float sizeMultiplier = requestOptions.getSizeMultiplier(); this.width = maybeApplySizeMultiplier(width, sizeMultiplier); this.height = maybeApplySizeMultiplier(height, sizeMultiplier); loadStatus = engine.load(/*各种参数*/); if (status != Status.RUNNING) { loadStatus = null; } } ``` 然后,让我们将重点放到 `Engine` 的 `load()` 方法。该方法虽然不长,但是却包含了许多重要的内容。我们在下篇文章中将要研究的 Glide 的缓存就是在这里实现的。该方法大致的逻辑上,先尝试从内存缓存当中查找指定的资源,当内存中不存在的时候就准备使用 `DecodeJob` 来加载图片。 ```java public LoadStatus load(/*各种参数*/) { EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options); EngineResource active = loadFromActiveResources(key, isMemoryCacheable); if (active != null) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); return null; } EngineResource cached = loadFromCache(key, isMemoryCacheable); if (cached != null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); return null; } EngineJob current = jobs.get(key, onlyRetrieveFromCache); if (current != null) { current.addCallback(cb); return new LoadStatus(cb, current); } EngineJob engineJob = engineJobFactory.build( key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache); DecodeJob decodeJob = decodeJobFactory.build(/*各种参数*/); jobs.put(key, engineJob); engineJob.addCallback(cb); engineJob.start(decodeJob); return new LoadStatus(cb, engineJob); } ``` 上面方法中涉及两个类,一个是 `DecodeJob`、一个是 `EngineJob`。它们之间的关系是,`EngineJob` 内部维护了线程池,用来管理资源加载,已经当资源加载完毕的时候通知回调。 `DecodeJob` 继承了 `Runnable`,是线程池当中的一个任务。就像上面那样,我们通过调用 `engineJob.start(decodeJob)` 来开始资源加载。 #### 3.1.2 小结 ![阶段1:开启 DecodeJob 的过程](res/glide_into_stage1.jpg) 根据上文中的分析,我们不难得出上面的流程图。不考虑缓存的问题,这个部分的逻辑还是比较清晰的,即:当调用了 `into()` 之后,首先构建一个请求对象 `SingleRequest`,然后调用 `RequestManager` 的 `track()` 方法对 `Request` 和 `Target` 进行管理;随后,使用 `Request` 的 `begin()` 方法来启动请求;该方法中会使用 `Engine` 的 `load()` 方法决定是从缓存当中获取资源还是从数据源中加载数据;如果是从数据源中加载数据的话,就构建一个 `DecodeJob` 交给 `EngineJob` 来执行即可。 ### 3.2 阶段2:打开网络流的过程 #### 3.2.1 打开网络流的过程 在上面的分析中,将 `DecodeJob` 交给 `EngineJob` 就完事了。因为 `DecodeJob` 是一个任务,会在线程池当中进行执行。所以,如果我们继续追踪的话,就应该从 `DecodeJob` 的 `run()` 方法开始: 所以,如果想要找到加载资源和解码的逻辑,就应该查看 DecodeJob 的 `run()` 方法。下面就是这个方法的定义: ```java public void run() { DataFetcher localFetcher = currentFetcher; try { if (isCancelled) { notifyFailed(); return; } runWrapped(); } catch (Throwable t) { if (stage != Stage.ENCODE) { throwables.add(t); notifyFailed(); } if (!isCancelled) { throw t; } } finally { if (localFetcher != null) { localFetcher.cleanup(); } GlideTrace.endSection(); } } ``` `DecodeJob` 的执行过程使用了状态模式,它会根据当前的状态决定将要执行的方法。在上面的方法中,当当前任务没有被取消的话,会进入到 `runWrapped()` 方法。该方法中会使用 `runReason` 作为当前的状态决定要执行的逻辑: ```java private void runWrapped() { switch (runReason) { case INITIALIZE: stage = getNextStage(Stage.INITIALIZE); currentGenerator = getNextGenerator(); runGenerators(); break; case SWITCH_TO_SOURCE_SERVICE: runGenerators(); break; case DECODE_DATA: decodeFromRetrievedData(); break; default: throw new IllegalStateException("Unrecognized run reason: " + runReason); } } ``` 这里的 `runReason` 是一个枚举类型,它包含的枚举值即为上面的三种类型。当我们在一个过程执行完毕之后会回调 `DecodeJob` 中的方法修改 `runReason`,然后根据新的状态值执行新的逻辑。 除了 `runReason`,`DecodeJob` 中还有一个变量 `stage` 也是用来决定 `DecodeJob` 状态的变量。同样,它也是一个枚举,用来表示将要加载数据的数据源以及数据的加载状态。它主要在加载数据的时候在 `runGenerators()`、`runWrapped()` 和 `getNextStage()` 三个方法中被修改。通常它的逻辑是,先从(大小、尺寸等)转换之后的缓存中拿数据,如果没有的话再从没有转换过的缓存中拿数据,最后还是拿不到的话就从原始的数据源中加载数据。 以上就是 `DecodeJob` 中的状态模式运行的原理。 对于一个新的任务,会在 `DecodeJob` 的 `init()` 方法中将 `runReason` 置为 `INITIALIZE`,所以,我们首先会进入到上述 `switch` 中的 `INITIALIZE` 中执行。然后,因为我们没有设置过磁盘缓存的策略,因此会使用默认的 `AUTOMATIC` 缓存方式。于是,我们将会按照上面所说的依次从各个缓存中拿数据。由于我们是第一次加载,并且暂时我们不考虑缓存的问题,所以,最终数据的加载会交给 `SourceGenerator` 进行。 不知道你是否还记得上一篇文章中我们在讲解在 Glide 中使用 OkHttp 时提到的相关的类。它们真正作用的地方就在下面的这个方法中。这是 `SourceGenerator` 的 `startNext()` 方法,它会: 1. 先使用 `DecodeHelper` 的 `getLoadData()` 方法从注册的映射表中找出当前的图片类型对应的 `ModelLoader`; 2. 然后使用它的 `DataFetcher` 的 `loadData()` 方法从原始的数据源中加载数据。 ```java public boolean startNext() { if (dataToCache != null) { Object data = dataToCache; dataToCache = null; cacheData(data); } if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) { return true; } sourceCacheGenerator = null; loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { loadData = helper.getLoadData().get(loadDataListIndex++); if (loadData != null && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource()) || helper.hasLoadPath(loadData.fetcher.getDataClass()))) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; } ``` 由于我们的图片时网络中的资源,在默认情况下会使用 Glide 内部的 `HttpUrlFetcher` 从网络中加载数据。其 `loadData()` 方法定义如下: ```java public void loadData(Priority priority, DataCallback callback) { try { InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); callback.onDataReady(result); } catch (IOException e) { callback.onLoadFailed(e); } finally { } } ``` 很明显,这里从网络中打开输入流之后得到了一个 `InputStream` 之后就使用回调将其返回了。至于 `loadDataWithRedirects()` 方法的实现,就是使用 `HttpURLConnection` 打开网络流的过程,这里我们不进行详细的说明了。 #### 3.2.2 小结 ![阶段2:打开网络流的过程](res/glide_into_stage2.jpg) 这样,`into()` 方法的第二个阶段,即从网络中获取一个输入流的过程就分析完毕了。整个过程并不算复杂,主要是在 `DecodeJob` 中的状态模式可能一开始看不太懂,还有就是其中涉及到的一些类不清楚其作用。如果你存在这两个疑惑的话,那么建议你:1).耐心思考下状态模式的转换过程;2).翻下上一篇文章了解自定义 Glide 图片加载方式的几个类的设计目的;3).最重要的,多看源码。 ### 3.3 阶段3:将输入流转换为 Drawable 的过程 #### 3.3.1 转换 Drawable 的过程 在上面的小节中我们已经打开了网络流,按照 Android 自身提供的 `BitmapFactory`,我们可以很容易地从输入流中得到 Drawable 不是?那么为什么这个转换的过程还要单独分为一个阶段呢? 实际上,这里的转换过程并不比上面打开输入流的过程简单多少。这是因为它涉及转码和将图片转换成适合控件大小的过程。好了,下面就让我们来具体看一下这个过程都发生了什么吧! 首先,从上面的 `loadData()`,我们可以看出当得到了输入流之后会回调 `onDataReady()` 方法。这个方法会一直从 `HttpUrlFetcher` 中一直回调到 `SourceGenerator` 中。这里它会使用默认的磁盘缓存策略判断数据是否可以缓存,并决定对数据进行缓存还是继续回调。 ```java public void onDataReady(Object data) { DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy(); if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) { dataToCache = data; cb.reschedule(); // 1 } else { cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher, loadData.fetcher.getDataSource(), originalKey); } } ``` 因为我们的数据是使用 `HttpUrlFetcher` 加载的,所以将会进入到 1 处继续进行处理。此时,`DecodeJob` 将会根据当前的状态从 `run()` 方法开始执行一遍,并再次调用 `DataCacheGenerator` 的 `startNext()` 方法。但是,此次与上一次不同的地方在于,这次已经存在可以用于缓存的数据了。所以,下面的方法将会被触发: ```java private void cacheData(Object dataToCache) { try { Encoder encoder = helper.getSourceEncoder(dataToCache); DataCacheWriter writer = new DataCacheWriter<>(encoder, dataToCache, helper.getOptions()); originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature()); helper.getDiskCache().put(originalKey, writer); } finally { loadData.fetcher.cleanup(); } sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this); } ``` 这里的主要逻辑是构建一个用于将数据缓存到磁盘上面的 `DataCacheGenerator`。`DataCacheGenerator` 的流程基本与 `SourceGenerator` 一致,也就是根据资源文件的类型找到 `ModelLoader`,然后使用 `DataFetcher` 加载缓存的资源。与之前不同的是,这次是用 `DataFecher` 来加载 `File` 类型的资源。也就是说,当我们从网络中拿到了数据之后 Glide 会先将其缓存到磁盘上面,然后再从磁盘上面读取图片并将其显示到控件上面。所以,当从网络打开了输入流之后 `SourceGenerator` 的任务基本结束了,而后的显示的任务都由 `DataCacheGenerator` 来完成。 与 `HttpUrlFetcher` 一样,File 类型的资源将由 `ByteBufferFetcher` 来加载,当它加载完毕之后也也会回调 `onDataReady()` 方法。此时,将会调用 `DataCacheGenerator` 的 `onDataReady()`: ```java public void onDataReady(Object data) { cb.onDataFetcherReady(sourceKey, data, loadData.fetcher, DataSource.DATA_DISK_CACHE, sourceKey); } ``` 该方法会继续回调到 `DecodeJob` 的 `onDataFetcherReady()` 方法,后续的逻辑比较清晰,只是在不断继续调用方法,我们依次给出这些方法: ```java // DecodeJob#onDataFetcherReady() public void onDataFetcherReady(Key sourceKey, Object data, DataFetcher fetcher, DataSource dataSource, Key attemptedKey) { // ... 赋值,略 if (Thread.currentThread() != currentThread) { runReason = RunReason.DECODE_DATA; callback.reschedule(this); } else { try { // decode 数据以得到期待的资源类型 decodeFromRetrievedData(); } finally { GlideTrace.endSection(); } } } // DecodeJob#decodeFromRetrievedData() private void decodeFromRetrievedData() { Resource resource = null; try { resource = decodeFromData(currentFetcher, currentData, currentDataSource); } catch (GlideException e) { // ... 异常处理 } // ... 释放资源和错误重试等 } // DecodeJob#decodeFromData() private Resource decodeFromData(DataFetcher fetcher, Data data, DataSource dataSource) throws GlideException { try { // ... 略 Resource result = decodeFromFetcher(data, dataSource); return result; } finally { fetcher.cleanup(); } } // DecodeJob#decodeFromFetcher() private Resource decodeFromFetcher(Data data, DataSource dataSource) throws GlideException { LoadPath path = decodeHelper.getLoadPath((Class) data.getClass()); return runLoadPath(data, dataSource, path); } // DecodeJob#runLoadPath() private Resource runLoadPath(Data data, DataSource dataSource, LoadPath path) throws GlideException { // ... 获取参数信息 try { // 使用 LoadPath 继续处理 return path.load( rewinder, options, width, height, new DecodeCallback(dataSource)); } finally { rewinder.cleanup(); } } // LoadPath#load() public Resource load(DataRewinder rewinder, @NonNull Options options, int width, int height, DecodePath.DecodeCallback decodeCallback) throws GlideException { try { // 继续加载 return loadWithExceptionList(rewinder, options, width, height, decodeCallback, throwables); } finally { listPool.release(throwables); } } // LoadPath#loadWithExceptionList() private Resource loadWithExceptionList(/*各种参数*/) throws GlideException { Resource result = null; for (int i = 0, size = decodePaths.size(); i < size; i++) { DecodePath path = decodePaths.get(i); try { // 使用 DecodePath 继续处理 result = path.decode(rewinder, width, height, options, decodeCallback); } catch (GlideException e) { exceptions.add(e); } if (result != null) { break; } } return result; } ``` 经过了上面的一系列猛如虎的操作之后,我们进入了 `loadWithExceptionList()` 方法,这里会对 `DecodePath` 进行过滤,以得到我们期望的图片的类型。这个方法中调用了 `DecodePath` 的 `decode()` 方法。这个方法比较重要,它像一个岔路口:1 处的代码是将数据转换成我们期望的图片的过程;2 处的代码是当得到了期望的图片之后对处理继续处理并显示的过程。 ```java // DecodePath#decode() public Resource decode(DataRewinder rewinder, int width, int height, Options options, DecodeCallback callback) throws GlideException { Resource decoded = decodeResource(rewinder, width, height, options); // 1 Resource transformed = callback.onResourceDecoded(decoded); // 2 return transcoder.transcode(transformed, options); } ``` 然后,让我们继续沿着 `decodeResource()` 走。它会调用下面的这个循环对当前的数据类型和期望的、最终的图片类型匹配从而决定用来继续处理的 `ResourceDecoder`。 ```java private Resource decodeResourceWithList(/*各种参数*/) throws GlideException { Resource result = null; for (int i = 0, size = decoders.size(); i < size; i++) { ResourceDecoder decoder = decoders.get(i); try { DataType data = rewinder.rewindAndGet(); if (decoder.handles(data, options)) { data = rewinder.rewindAndGet(); result = decoder.decode(data, width, height, options); } } catch (IOException | RuntimeException | OutOfMemoryError e) { exceptions.add(e); } if (result != null) { break; } } return result; } ``` `ResourceDecoder` 具有多个实现类,比如 `BitmapDrawableDecoder`、`ByteBufferBitmapDecoder`等。从名字也可以看出来是用来将一个类型转换成另一个类型的。 在我们的程序中会使用 `ByteBufferBitmapDecoder` 来将 `ByteBuffer` 专成 `Bitmap`。它最终会在 `Downsampler` 的 `decodeStream()` 方法中调用 `BitmapFactory` 的 `decodeStream()` 方法来从输入流中得到 Bitmap。(我们的 `ByteBuffer` 在 `ByteBufferBitmapDecoder` 中先被转换成了输入流。) ```java private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { // ... 略 TransformationUtils.getBitmapDrawableLock().lock(); try { result = BitmapFactory.decodeStream(is, null, options); } catch (IllegalArgumentException e) { // ... 错误处理,略 } finally { TransformationUtils.getBitmapDrawableLock().unlock(); } if (options.inJustDecodeBounds) { is.reset(); } return result; } ``` 这样剩下的就只有不断继续向上回调或者返回,最终回到了我们上面所说的岔路口。这样从输入流中加载图片的逻辑就结束了:) #### 3.3.2 小结 ![阶段3:将输入流转换为 Drawable 的过程](res/glide_into_stage3.jpg) 怎么样,是不是觉得这个过程比打开输入流的过程复杂多了?毕竟这个部分涉及到了从缓存当中取数据以及向缓存写数据的过程,算的上是核心部分了。整体而言,这部分的设计还是非常巧的,即使用了状态模式,根据当前的状态来决定下一个 `Generator`。从网络中拿到输入流之后又使用 `DataCacheGenerator` 从缓存当中读取数据,这个过程连我第一次读源码的时候都没发现,以至于后来调试验证了推理之后才确信这部分是这样设计的…… ### 3.4 阶段4:将 Drawable 展示到 ImageView 的过程 根据上面的分析,我们已经从网络中得到了图片数据,并且已经将其放置到了缓存中,又从缓存当中取出数据进行准备进行显示。上面的过程比较复杂,下面将要出场的这个阶段也并不轻松…… #### 3.4.1 最终展示图片的过程 在上面分析中,我们已经进入到了之前所谓的岔路口,这里我们再给出这个方法的定义如下。上面的分析到了代码 1 处,现在我们继续从代码 2 处进行分析。 ```java // DecodePath#decode() public Resource decode(DataRewinder rewinder, int width, int height, Options options, DecodeCallback callback) throws GlideException { Resource decoded = decodeResource(rewinder, width, height, options); // 1 Resource transformed = callback.onResourceDecoded(decoded); // 2 return transcoder.transcode(transformed, options); // 3 } ``` 这里会调用 `callback` 的方法进行回调,它最终会回调到 `DecodeJob` 的 `onResourceDecoded()` 方法。其主要的逻辑是根据我们设置的参数进行变化,也就是说,如果我们使用了 `centerCrop` 等参数,那么这里将会对其进行处理。这里的 `Transformation` 是一个接口,它的一系列的实现都是对应于 `scaleType` 等参数的。 ```java Resource onResourceDecoded(DataSource dataSource, Resource decoded) { Class resourceSubClass = (Class) decoded.get().getClass(); Transformation appliedTransformation = null; Resource transformed = decoded; // 对得到的图片资源进行变换 if (dataSource != DataSource.RESOURCE_DISK_CACHE) { appliedTransformation = decodeHelper.getTransformation(resourceSubClass); transformed = appliedTransformation.transform(glideContext, decoded, width, height); } if (!decoded.equals(transformed)) { decoded.recycle(); } // ... 缓存相关的逻辑,略 return result; } ``` 在上面的方法中对图形进行变换之后还会根据图片的缓存策略决定对图片进行缓存。然后这个方法就直接返回了我们变换之后的图象。这样我们就又回到了之前的岔路口。程序继续执行就到了岔路口方法的第 3 行。这里还会使用 `BitmapDrawableTranscoder` 的 `transcode()` 方法返回 `Resouces`。只是这里会使用 `BitmapDrawableTranscoder` 包装一层,即做了延迟初始化处理。 这样,当第 3 行方法也执行完毕,我们的岔路口方法就分析完了。然后就是不断向上 `return` 进行返回。所以,我们又回到了 `DecodeJob` 的 `decodeFromRetrievedData()` 方法如下。这里会进入到下面方法的 1 处来完成最终的图片显示操作。 ```java private void decodeFromRetrievedData() { Resource resource = null; try { resource = decodeFromData(currentFetcher, currentData, currentDataSource); } catch (GlideException e) { throwables.add(e); } if (resource != null) { notifyEncodeAndRelease(resource, currentDataSource); // 1 } else { runGenerators(); } } ``` 接着程序会达到 `DecodeJob` 的 `onResourceReady()` 方法如下。因为达到下面的方法的过程的逻辑比较简单,我们就不贴出这部分的代码了。 ```java public void onResourceReady(Resource resource, DataSource dataSource) { this.resource = resource; this.dataSource = dataSource; MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget(); } ``` 这里会获取到一个消息并将其发送到 `Handler` 中进行处理。当 `Handler` 收到消息之后会调用 `EncodeJob` 的 `handleResultOnMainThread()` 方法继续处理: ```java void handleResultOnMainThread() { // ... 略 engineResource = engineResourceFactory.build(resource, isCacheable); hasResource = true; engineResource.acquire(); listener.onEngineJobComplete(this, key, engineResource); for (int i = 0, size = cbs.size(); i < size; i++) { ResourceCallback cb = cbs.get(i); if (!isInIgnoredCallbacks(cb)) { engineResource.acquire(); cb.onResourceReady(engineResource, dataSource); // 1 } } engineResource.release(); release(false /*isRemovedFromQueue*/); } ``` 经过一系列的判断之后程序进入到代码 1 处,然后继续进行回调。这里的 `cb` 就是 `SingeleRequest`。 程序到了 `SingleRequest` 的方法中之后在下面的代码 1 处回调 `Target` 的方法。而这里的 `Target` 就是我们之前所说的 `ImageViewTarget`. ```java private void onResourceReady(Resource resource, R result, DataSource dataSource) { boolean isFirstResource = isFirstReadyResource(); status = Status.COMPLETE; this.resource = resource; isCallingCallbacks = true; try { // ... 略 if (!anyListenerHandledUpdatingTarget) { Transition animation = animationFactory.build(dataSource, isFirstResource); target.onResourceReady(result, animation); // 1 } } finally { isCallingCallbacks = false; } notifyLoadSuccess(); } ``` 当程序到了 `ImageViewTarget` 之后会使用 `setResource()` 方法最终调用 `ImageView` 的方法将 `Drawable` 显示到控件上面。 ```java protected void setResource(@Nullable Drawable resource) { view.setImageDrawable(resource); } ``` 这样,我们的 Glide 的加载过程就结束了。 #### 3.4.2 小结 ![阶段4:将 Drawable 展示到 ImageView 的过程](res/glide_into_stage4.jpg) 上面是我们将之前得到的 `Drawable` 显示到控件上面的过程。这个方法包含了一定的逻辑,涉及的代码比较多,但是整体的逻辑比较简单,所以这部分的篇幅并不长。 ### 4、总结 以上的内容便是我们的 Glide 加载图片的整个流程。从文章的篇幅和涉及的代码也可以看出,整个完整的过程是比较复杂的。从整体来看,Glide 之前启动和最终显示图片的过程比较简单、逻辑也比较清晰。最复杂的地方也是核心的地方在于 `DecodeJob` 的状态切换。 上面的文章中,我们重点梳理图片加载的整个流程,对于图片缓存和缓存的图片的加载的过程我没有做过多的介绍。我们会在下一篇文章中专门来介绍这部分内容。 以上。 ================================================ FILE: 图片加载/Glide系列:Glide的缓存的实现原理.md ================================================ # Glide 系列-3:Glide 缓存的实现原理(4.8.0) ## 1、在 Glide 中配置缓存的方式 首先,我们可以在自定义的 GlideModule 中制定详细的缓存策略。即在 `applyOptions()` 中通过直接调用 `GlideBuilder` 的方法来指定缓存的信息: ```java @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE)); builder.setMemoryCache(...); builder.setDiskCache(...); // ... 略 } ``` 另外,我们在每个图片加载请求中自定义当前图片加载请求的缓存策略, ```java Glide.with(getContext()) .load("https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png") .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.AUTOMATIC)) .apply(RequestOptions.skipMemoryCacheOf(false)) .into(getBinding().iv); ``` 以上是两个比较常用的缓存的配置方式,具体的 API 可以查看相关的源码了解. 不论 Glide 还是其他的框架的缓存无非就是基于内存的缓存和基于磁盘的缓存两种,而且缓存的管理算法基本都是 LRU. 针对内存缓存,Android 中提供了 `LruCache`,笔者在之前的文章中曾经分析过这个框架: [《Android 内存缓存框架 LruCache 的源码分析》](https://juejin.im/post/5bea581be51d451402494af2) 至于磁盘缓存, Glide 和 OkHttp 都是基于 [DiskLruCache](https://github.com/JakeWharton/DiskLruCache) 进行了封装。这个框架本身的逻辑并不复杂,只是指定了一系列缓存文件的规则,读者可以自行查看源码学习。本文中涉及上述两种框架的地方不再详细追究缓存框架的源码。 ## 2、Glide 缓存的源码分析 ### 2.1 缓存配置 首先, 我们在 `applyOptions()` 方法中的配置会在实例化单例的 Glide 对象的时候被调用. 所以, 这些方法的作用范围是全局的, 对应于整个 Glide. 下面的方法是 `RequestBuilder` 的 `build()` 方法, 也就是我们最终完成构建 Glide 的地方. 我们可以在这个方法中了解 `RequestBuilder` 为我们提供了哪些与缓存相关的方法. 以及默认的缓存配置. ```java Glide build(@NonNull Context context) { // ... 无关代码, 略 if (diskCacheExecutor == null) { diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); } if (memorySizeCalculator == null) { memorySizeCalculator = new MemorySizeCalculator.Builder(context).build(); } if (bitmapPool == null) { int size = memorySizeCalculator.getBitmapPoolSize(); if (size > 0) { bitmapPool = new LruBitmapPool(size); } else { bitmapPool = new BitmapPoolAdapter(); } } if (arrayPool == null) { arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes()); } if (memoryCache == null) { // 默认的缓存配置 memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize()); } if (diskCacheFactory == null) { diskCacheFactory = new InternalCacheDiskCacheFactory(context); } if (engine == null) { engine = new Engine(/*各种参数*/); } return new Glide(/*各种方法*/); } ``` 这里我们对 `MemorySizeCalculator` 这个参数进行一些说明. 顾名思义, 它是缓存大小的计算器, 即用来根据当前设备的环境计算可用的缓存空间 (主要针对的时基于内存的缓存). ```java MemorySizeCalculator(MemorySizeCalculator.Builder builder) { this.context = builder.context; arrayPoolSize = isLowMemoryDevice(builder.activityManager) ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR : builder.arrayPoolSizeBytes; // 计算APP可申请最大使用内存,再乘以乘数因子,内存过低时乘以0.33,一般情况乘以0.4 int maxSize = getMaxSize( builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier); // ARGB_8888 ,每个像素占用4个字节内存 // 计算屏幕这么大尺寸的图片占用内存大小 int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL; // 计算目标位图池内存大小 int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens); // 计算目标Lrucache内存大小,也就是屏幕尺寸图片大小乘以2 int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens); // 最终APP可用内存大小 int availableSize = maxSize - arrayPoolSize; if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) { // 如果目标位图内存大小+目标Lurcache内存大小小于APP可用内存大小,则OK memoryCacheSize = targetMemoryCacheSize; bitmapPoolSize = targetBitmapPoolSize; } else { // 否则用APP可用内存大小等比分别赋值 float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens); memoryCacheSize = Math.round(part * builder.memoryCacheScreens); bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens); } } ``` ### 2.2 内存缓存 对于, 每个加载请求时对应的 `DiskCacheStrategy` 的设置, 我们之前的文章中已经提到过它的作用位置, 你可以参考之前的文章了解, [《Glide 系列-2:主流程源码分析(4.8.0)》](https://juejin.im/post/5c31fbdff265da610e803d4e) `DiskCacheStrategy` 的作用位置恰好也是 Glide 的缓存最初发挥作用的地方, 即 Engine 的 `load()` 方法. 这里我们只保留了与缓存相关的逻辑, 从下面的方法中也可以看出, 当根据各个参数构建了用于缓存的键之后先后从两个缓存当中加载数据, 拿到了数据之后就进行回调, 否则就需要从原始的数据源中加载数据. ```java public LoadStatus load(/*各种参数*/) { // 根据请求参数得到缓存的键 EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options); // 检查内存中弱引用是否有目标图片 EngineResource active = loadFromActiveResources(key, isMemoryCacheable); // 1 if (active != null) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); return null; } // 检查内存中Lrucache是否有目标图片 EngineResource cached = loadFromCache(key, isMemoryCacheable); // 2 if (cached != null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); return null; } // ...内存中没有图片构建任务往下执行, 略 return new LoadStatus(cb, engineJob); } ``` 这里存在两个方法,即 1 处的从弱引用中获取缓存数据,以及 2 处的从内存缓存中获取缓存数据。它们两者之间有什么区别呢? 1. 弱引用的缓存会在内存不够的时候被清理掉,而基于 LruCache 的内存缓存是强引用的,因此不会因为内存的原因被清理掉。LruCache 只有当缓存的数据达到了缓存空间的上限的时候才会将最近最少使用的缓存数据清理出去。 2. 两个缓存的实现机制都是基于哈希表的,只是 LruCahce 除了具有哈希表的数据结构还维护了一个链表。而弱引用类型的缓存的键与 LruCache 一致,但是值是弱引用类型的。 3. 除了内存不够的时候被释放,弱引用类型的缓存还会在 Engine 的资源被释放的时候清理掉。 4. 基于弱引用的缓存是一直存在的,无法被用户禁用,但用户可以关闭基于 LruCache 的缓存。 5. 本质上基于弱引用的缓存与基于 LruCahce 的缓存针对于不同的应用场景,弱引用的缓存算是缓存的一种类型,只是这种缓存受可用内存的影响要大于 LruCache. 接下来让我们先看下基于弱引用的缓存相关的逻辑,从上面的 1 处的代码开始: ```java // Engine#loadFromActiveResources private EngineResource loadFromActiveResources(Key key, boolean isMemoryCacheable) { if (!isMemoryCacheable) { return null; } EngineResource active = activeResources.get(key); // 1 if (active != null) { active.acquire(); // 2 } return active; } // ActiveResources#get() EngineResource get(Key key) { ResourceWeakReference activeRef = activeEngineResources.get(key); if (activeRef == null) { return null; } EngineResource active = activeRef.get(); if (active == null) { cleanupActiveReference(activeRef); // 3 } return active; } // ActiveResources#cleanupActiveReference() void cleanupActiveReference(@NonNull ResourceWeakReference ref) { activeEngineResources.remove(ref.key); if (!ref.isCacheable || ref.resource == null) { // 4 return; } EngineResource newResource = new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false); newResource.setResourceListener(ref.key, listener); listener.onResourceReleased(ref.key, newResource); // 5 } // Engine#onResourceReleased() public void onResourceReleased(Key cacheKey, EngineResource resource) { Util.assertMainThread(); activeResources.deactivate(cacheKey); if (resource.isCacheable()) { cache.put(cacheKey, resource); // 将数据缓存到 LruCahce } else { resourceRecycler.recycle(resource); } } ``` 这里的 1 处会先调用 ActiveResources 的 `get()` 从弱引用中拿数据。当拿到了数据之后调用 `acquire()` 方法将 `EngineResource` 的引用计数加 1. 当这个资源被释放的时候,又会将引用计数减 1(参考 EngineResource 的 `release()` 方法). 当发现了弱引用中引用的 `EngineResource` 不存在的时候会在 3 处执行一次清理的逻辑。并在 5 处调用回调接口将弱引用中缓存的数据缓存到 LruCache 里面。 这里在将数据缓存之前会先在 4 处判断缓存是否可用。这里使用到了 `isCacheable` 这个字段。通过查看源码我们可以追踪到这个字段最初传入的位置是在 `RequestOptions` 里面。也就是说,这个字段是针对一次请求的,我们可以在构建 Glide 请求的时候通过 `apply()` 设置这个参数的值(这个字段默认是 `true`,也就是默认是启用内存缓存的)。 ```java Glide.with(getContext()) .load("https://3-im.guokr.com/0lSlGxgGIQkSQVA_Ja0U3Gxo0tPNIxuBCIXElrbkhpEXBAAAagMAAFBO.png") .apply(RequestOptions.skipMemoryCacheOf(false)) // 不忽略内存缓存,即启用 .into(getBinding().iv); ``` ### 2.3 磁盘缓存 上面介绍了内存缓存,下面我们分析一下磁盘缓存。 正如我们最初的示例那样,我们可以通过在构建请求的时候指定缓存的策略。我们的图片加载请求会得到一个 `RequestOptions`,我们通过查看该类的代码也可以看出,默认的缓存策略是 `AUTOMATIC` 的。 这里的 `AUTOMATIC` 定义在 `DiskCacheStrategy` 中,除了 `AUTOMATIC` 还有其他几种缓存策略,那么它们之间又有什么区别呢? 1. `ALL`:既缓存原始图片,也缓存转换过后的图片;对于远程图片,缓存 `DATA` 和 `RESOURCE`;对于本地图片,只缓存 `RESOURCE`。 2. `AUTOMATIC` (默认策略):尝试对本地和远程图片使用最佳的策略。当你加载远程数据(比如,从 `URL` 下载)时,`AUTOMATIC` 策略仅会存储未被你的加载过程修改过 (比如,变换、裁剪等) 的原始数据(`DATA`),因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,`AUTOMATIC` 策略则会仅存储变换过的缩略图(`RESOURCE`),因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。 3. `DATA`:只缓存未被处理的文件。我的理解就是我们获得的 `stream`。它是不会被展示出来的,需要经过装载 `decode`,对图片进行压缩和转换,等等操作,得到最终的图片才能被展示。 4. `NONE`:表示不缓存任何内容。 5. `RESOURCE`:表示只缓存转换过后的图片(也就是经过decode,转化裁剪的图片)。 那么这些缓存的策略是在哪里使用到的呢?回顾上一篇文章,首先,我们是在 `DecodeJob` 的状态模式中用到了磁盘缓存策略: ```java private Stage getNextStage(Stage current) { switch (current) { case INITIALIZE: // 是否解码缓存的转换图片,就是只做过变换之后的缓存数据 return diskCacheStrategy.decodeCachedResource() ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE); case RESOURCE_CACHE: // 是否解码缓存的原始数据,就是指缓存的未做过变换的数据 return diskCacheStrategy.decodeCachedData() ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE); case DATA_CACHE: return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE; case SOURCE: case FINISHED: return Stage.FINISHED; default: throw new IllegalArgumentException("Unrecognized stage: " + current); } } private DataFetcherGenerator getNextGenerator() { switch (stage) { case RESOURCE_CACHE: return new ResourceCacheGenerator(decodeHelper, this); case DATA_CACHE: return new DataCacheGenerator(decodeHelper, this); case SOURCE: return new SourceGenerator(decodeHelper, this); case FINISHED: return null; default: throw new IllegalStateException("Unrecognized stage: " + stage); } } ``` 首先会根据当前所处的阶段 `current` 以及缓存策略判断应该使用哪个 `DataFetcherGenerator` 加载数据。我们分别来看一下它们: 首先是 `ResourceCacheGenerator`,它用来从缓存中得到变换之后数据。当从缓存中拿数据的时候会调用到它的 `startNext()` 方法如下。从下面的方法也可以看出,当从缓存中拿数据的时候会先在代码 1 处构建一个用于获取缓存数据 key。在构建这个 key 的时候传入了图片大小、变换等各种参数,即根据各种变换后的条件获取缓存数据。因此,这个类是用来获取变换之后的缓存数据的。 ```java public boolean startNext() { List sourceIds = helper.getCacheKeys(); if (sourceIds.isEmpty()) { return false; } List> resourceClasses = helper.getRegisteredResourceClasses(); if (resourceClasses.isEmpty()) { if (File.class.equals(helper.getTranscodeClass())) { return false; } } while (modelLoaders == null || !hasNextModelLoader()) { resourceClassIndex++; if (resourceClassIndex >= resourceClasses.size()) { sourceIdIndex++; if (sourceIdIndex >= sourceIds.size()) { return false; } resourceClassIndex = 0; } Key sourceId = sourceIds.get(sourceIdIndex); Class resourceClass = resourceClasses.get(resourceClassIndex); Transformation transformation = helper.getTransformation(resourceClass); currentKey = new ResourceCacheKey( // 1 构建获取缓存信息的键 helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(), transformation, resourceClass, helper.getOptions()); cacheFile = helper.getDiskCache().get(currentKey); // 2 从缓存中获取缓存信息 if (cacheFile != null) { sourceKey = sourceId; modelLoaders = helper.getModelLoaders(cacheFile); modelLoaderIndex = 0; } } loadData = null; boolean started = false; while (!started && hasNextModelLoader()) { ModelLoader modelLoader = modelLoaders.get(modelLoaderIndex++); // 3 使用文件方式从缓存中读取缓存数据 loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; } ``` 当找到了缓存的值之后会使用 `File` 类型的 `ModelLoader` 加载数据。这个比较容易理解,因为数据存在磁盘上面,需要用文件的方式打开。 另外,我们再关注下 2 处的代码,它会使用 `helper` 的 `getDiskCache()` 方法获取 `DiskCache` 对象。我们一直追踪这个对象就会找到一个名为 `DiskLruCacheWrapper` 的类,它内部包装了 `DiskLruCache`。所以,最终从磁盘加载数据是使用 `DiskLruCache` 来实现的。对于最终使用 `DiskLruCache` 获取数据的逻辑我们不进行说明了,它的逻辑并不复杂,都是单纯的文件读写,只是设计了一套缓存的规则。 上面是从磁盘读取数据的,那么数据又是在哪里向磁盘缓存数据的呢? 在之前的文章中我们也分析过这部分内容,即当从网络中打开输入流之后会回到 `DecodeJob` 中,进入下一个阶段,并再次调用 `SourceGenerator` 的 `startNext()` 方法。此时会进入到 `cacheData()` 方法,并将数据缓存到磁盘上: ```java private void cacheData(Object dataToCache) { long startTime = LogTime.getLogTime(); try { Encoder encoder = helper.getSourceEncoder(dataToCache); DataCacheWriter writer = new DataCacheWriter<>(encoder, dataToCache, helper.getOptions()); originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature()); helper.getDiskCache().put(originalKey, writer); // 将数据缓存到磁盘上面 } finally { loadData.fetcher.cleanup(); } sourceCacheGenerator = new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this); } ``` 然后构建一个 `DataCacheGenerator` 再从磁盘上面读取出缓存的数据,显示到控件上面。 还有一个问题,从上文中我们也可以看出 Glide 在进行缓存的时候可以缓存转换之后的数据,也可以缓存原始的数据。我们可以通过构建的用于获取缓存的键看出这一点:在 `ResourceCacheGenerator` 中获取转换之后的缓存数据的时候,我们使用 `ResourceCacheKey` 并传入了各种参数构建了缓存的键;在将数据存储到磁盘上面的时候我们使用的是 `DataCacheKey`,并且没有传入那么多参数。这说明获取的和存储的并不是同一份数据,那么转换之后的数据是在哪里缓存的呢? 我们通过查找类 `ResourceCacheKey` 将位置定位在了 `DecodeJob` 的 `onResourceDecoded()` 方法中: ```java Resource onResourceDecoded(DataSource dataSource, Resource decoded) { // ... 略 Resource result = transformed; boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey); if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource, encodeStrategy)) { if (encoder == null) { throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass()); } final Key key; // 根据缓存的此略使用不同的缓存的键 switch (encodeStrategy) { case SOURCE: key = new DataCacheKey(currentSourceKey, signature); break; case TRANSFORMED: key = new ResourceCacheKey( decodeHelper.getArrayPool(), currentSourceKey, signature, width, height, appliedTransformation, resourceSubClass, options); break; default: throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy); } LockedResource lockedResult = LockedResource.obtain(transformed); // 将缓存的键和数据信息设置到 deferredEncodeManager 中,随后会将其缓存到磁盘上面 deferredEncodeManager.init(key, encoder, lockedResult); result = lockedResult; } return result; } ``` 显然,这里会根据缓存的策略构建两种不同的 key,并将其传入到 `deferredEncodeManager` 中。然后将会在 `DecodeJob` 的 `notifyEncodeAndRelease()` 方法中调用 `deferredEncodeManager` 的 `encode()` 方法将数据缓存到磁盘上: ```java void encode(DiskCacheProvider diskCacheProvider, Options options) { try { // 将数据缓存到磁盘上面 diskCacheProvider.getDiskCache().put(key, new DataCacheWriter<>(encoder, toEncode, options)); } finally { toEncode.unlock(); } } ``` 以上就是 Glide 的磁盘缓存的实现原理。 ### 3、总结 在这篇文中我们在之前的两篇文章的基础之上分析了 Glide 的缓存的实现原理。 首先 Glide 存在两种内存缓存,一个基于弱引用的,一个是基于 LruCache 的。两者存在一些不同,在文中我们已经总结了这部分内容。 然后,我们分析了 Glide 的磁盘缓存的实现原理。Glide 的磁盘缓存使用了策略模式,存在 4 种既定的缓存策略。Glide 不仅可以原始的数据缓存到磁盘上面,还可以将做了转换之后的数据缓存到磁盘上面。它们会基于自身的缓存方式构建不同的 key 然后底层使用 DiskLruCache 从磁盘种获取数据。这部分的核心代码在 `DecodeJob` 和三个 `DataFetcherGenerator` 中。 以上就是 Glide 缓存的所有实现原理。 ================================================ FILE: 图片加载/Glide系列:Glide的配置和使用方式.md ================================================ # Glide 系列-1:预热、Glide 的常用配置方式及其原理 在接下来的几篇文章中,我们会对 Android 中常用的图片加载框架 Glide 进行分析。在本篇文章中,我们先通过介绍 Glide 的几种常用的配置方式来了解 Glide 的部分源码。后续的文中,我们会对 Glide 的源码进行更详尽的分析。 对于 Glide,相信多数 Android 开发者并不陌生,在本文中,我们不打算对其具体使用做介绍,你可以通过查看官方文档进行学习。Glide 的 API 设计非常人性化,上手也很容易。 在这篇文中中我们主要介绍两种常用的 Glide 的配置方式,并以此为基础来分析 Glide 的工作原理。在本文中我们将会介绍的内容有: 1. 通过自定义 GlideModule 指定 Glide 的缓存路径和缓存空间的大小; 2. 带有时间戳的图片的缓存命中问题的解决; 3. 在 Glide 中使用 OkHttp 作为网络中的图片资源加载方式的实现。 ## 1、自定义图片加载方式 有时候,我们需要对 Glide 进行配置来使其能够对特殊类型的图片进行加载和缓存。考虑这么一个场景:图片路径中带有时间戳。这种情形比较场景,即有时候我们通过为图片设置时间戳来让图片链接在指定的时间过后失效,从而达到数据保护的目的。 在这种情况下,我们需要解决几个问题:1).需要配置缓存的 key,不然缓存无法命中,每次都需要从网络中进行获取;2).根据正确的链接,从网络中获取图片并展示。 我们可以使用自定义配置 Glide 的方式来解决这个问题。 ### 1.1 带时间戳图片加载的实现 #### 1.1.1 MyAppGlideModule 首先,按照下面的方式自定义 `GlideModule`, ```java @GlideModule public class MyAppGlideModule extends AppGlideModule { /** * 配置图片缓存的路径和缓存空间的大小 */ @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20)); } /** * 注册指定类型的源数据,并指定它的图片加载所使用的 ModelLoader */ @Override public void registerComponents(Context context, Glide glide, Registry registry) { glide.getRegistry().append(CachedImage.class, InputStream.class, new ImageLoader.Factory()); } /** * 是否启用基于 Manifest 的 GlideModule,如果没有在 Manifest 中声明 GlideModule,可以通过返回 false 禁用 */ @Override public boolean isManifestParsingEnabled() { return false; } } ``` 在上面的代码中,我们通过覆写 `registerComponents()` 方法,并调用 Glide 的 `Registry` 的 `append()` 方法来向 Glide **增加**我们的自定义图片类型的加载方式。(如果替换某种资源加载方式则需要使用 `replace()` 方法,此外 `Registry` 还有其他的方法,可以通过查看源码进行了解。) 在上面的方法中,我们新定义了两个类,分别是 `CachedImage` 和 `ImageLoader`。`CachedImage` 就是我们的自定义资源类型,`ImageLoader` 是该资源类型的加载方式。当进行图片加载的时候,会根据资源的类型找到该图片加载方式,然后使用它来进行图片加载。 #### 1.1.2 CachedImage 我们通过该类的构造方法将原始的图片的链接传入,并通过该类的 `getImageId()` 方法来返回图片缓存的键,在该方法中我们从图片链接中过滤掉时间戳: ```java public class CachedImage { private final String imageUrl; public CachedImage(String imageUrl) { this.imageUrl = imageUrl; } /** * 原始的图片的 url,用来从网络中加载图片 */ public String getImageUrl() { return imageUrl; } /** * 提取时间戳之前的部分作为图片的 key,这个 key 将会被用作缓存的 key,并用来从缓存中找缓存数据 */ public String getImageId() { if (imageUrl.contains("?")) { return imageUrl.substring(0, imageUrl.lastIndexOf("?")); } else { return imageUrl; } } } ``` #### 1.1.3 ImageLoader `CachedImage` 的加载通过 `ImageLoader` 实现。正如上面所说的,我们将 `CachedImage` 的 `getImageId()` 方法得到的字符串作为缓存的键,然后使用默认的 `HttpUrlFetcher` 作为图片的加载方式。 ```java public class ImageLoader implements ModelLoader { /** * 在这个方法中,我们使用 ObjectKey 来设置图片的缓存的键 */ @Override public LoadData buildLoadData(CachedImage cachedImage, int width, int height, Options options) { return new LoadData<>(new ObjectKey(cachedImage.getImageId()), new HttpUrlFetcher(new GlideUrl(cachedImage.getImageUrl()), 15000)); } @Override public boolean handles(CachedImage cachedImage) { return true; } public static class Factory implements ModelLoaderFactory { @Override public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ImageLoader(); } @Override public void teardown() { /* no op */ } } } ``` #### 1.1.4 使用 当我们按照上面的方式配置完毕之后就可以在项目中使用 `CachedImage` 来加载图片了: ```java GlideApp.with(getContext()) .load(new CachedImage(user.getAvatarUrl())) .into(getBinding().ivAccount); ``` 这里,当有加载图片需求的时候,都会把原始的图片链接使用 `CachedImage` 包装一层之后再进行加载,其他的步骤与 Glide 的基本使用方式一致。 ### 1.2 原理分析 当我们启用了 `@GlideModule` 注解之后会在编译期间生成 `GeneratedAppGlideModuleImpl`。从下面的代码中可以看出,它实际上就是对我们自定义的 `MyAppGlideModule` 做了一层包装。这么去做的目的就是它可以通过反射来寻找 `GeneratedAppGlideModuleImpl`,并通过调用 `GeneratedAppGlideModuleImpl` 的方法来间接调用我们的 `MyAppGlideModule`。本质上是一种代理模式的应用: ```java final class GeneratedAppGlideModuleImpl extends GeneratedAppGlideModule { private final MyAppGlideModule appGlideModule; GeneratedAppGlideModuleImpl() { appGlideModule = new MyAppGlideModule(); } @Override public void applyOptions(Context context, GlideBuilder builder) { appGlideModule.applyOptions(context, builder); } @Override public void registerComponents(Context context, Glide glide, Registry registry) { appGlideModule.registerComponents(context, glide, registry); } @Override public boolean isManifestParsingEnabled() { return appGlideModule.isManifestParsingEnabled(); } @Override public Set> getExcludedModuleClasses() { return Collections.emptySet(); } @Override GeneratedRequestManagerFactory getRequestManagerFactory() { return new GeneratedRequestManagerFactory(); } } ``` 下面就是 `GeneratedAppGlideModuleImpl` 被用到的地方: 当我们实例化单例的 Glide 的时候,会调用下面的方法来通过反射获取该实现类(所以对生成类的混淆就是必不可少的): ```java Class clazz = (Class) Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl"); ``` 当得到了之后会调用 `GeneratedAppGlideModule` 的各个方法。这样我们的自定义 `GlideModule` 的方法就被触发了。(下面的方法比较重要,我们自定义 Glide 的时候许多的配置都能够从下面的源码中寻找到答案,后文中我们仍然会提到这个方法) ```java private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) { Context applicationContext = context.getApplicationContext(); // 利用反射获取 GeneratedAppGlideModuleImpl GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules(); // 从 Manifest 中获取 GlideModule List manifestModules = Collections.emptyList(); if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) { manifestModules = new ManifestParser(applicationContext).parse(); } // 获取被排除掉的 GlideModule if (annotationGeneratedModule != null && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) { Set> excludedModuleClasses = annotationGeneratedModule.getExcludedModuleClasses(); Iterator iterator = manifestModules.iterator(); while (iterator.hasNext()) { com.bumptech.glide.module.GlideModule current = iterator.next(); if (!excludedModuleClasses.contains(current.getClass())) { continue; } iterator.remove(); } } // 应用 GlideModule,我们自定义 GlideModuel 的方法会在这里被调用 RequestManagerRetriever.RequestManagerFactory factory = annotationGeneratedModule != null ? annotationGeneratedModule.getRequestManagerFactory() : null; builder.setRequestManagerFactory(factory); for (com.bumptech.glide.module.GlideModule module : manifestModules) { module.applyOptions(applicationContext, builder); } if (annotationGeneratedModule != null) { annotationGeneratedModule.applyOptions(applicationContext, builder); } // 构建 Glide 对象 Glide glide = builder.build(applicationContext); for (com.bumptech.glide.module.GlideModule module : manifestModules) { module.registerComponents(applicationContext, glide, glide.registry); } if (annotationGeneratedModule != null) { annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry); } applicationContext.registerComponentCallbacks(glide); Glide.glide = glide; } ``` 再回到之前的自定义 GlideModule 部分代码中: ```java public void applyOptions(Context context, GlideBuilder builder) { builder.setDiskCache(new InternalCacheDiskCacheFactory(context, Constants.DISK_CACHE_DIR, 100 << 20)); } ``` 这里的 `applyOptions()` 方法允许我们对 Glide 进行自定义。从 `initializeGlide()` 方法中,我们也看出,这里的 `GlideBuilder` 也就是 `initializeGlide()` 方法中传入的 `GlideBuilder`。这里使用了构建者模式,`GlideBuilder` 是构建者的实例。所以,我们可以通过调用 `GlideBuilder` 的方法来对 Glide 进行自定义。 在上面的自定义 GlideModule 中,我们通过构建者来指定了 Glide 的缓存大小和缓存路径。 `GlideBuilder` 还提供了一些其他的方法,我们可以通过查看源码了解,并调用这些方法来自定义 Glide. ## 2、在 Glide 中使用 OkHttp Glide 默认使用 `HttpURLConnection` 实现网络当中的图片的加载。我们可以通过对 Glide 进行配置来使用 OkHttp 进行网络图片加载。 首先,我们需要引用如下依赖: ```groovy api ('com.github.bumptech.glide:okhttp3-integration:4.8.0') { transitive = false } ``` 该类库中提供了基于 OkHttp 的 `ModelLoader` 和 `DataFetcher` 实现。它们是 Glide 图片加载环节中的重要组成部分,我们会在后面介绍源码和 Glide 的架构的时候介绍它们被设计的意图及其作用。 然后,我们需要在自定义的 `GlideModule` 中注册网络图片加载需要的组件,即在 `registerComponents()` 方法中替换 `GlideUrl` 的加载的默认实现: ```java @GlideModule @Excludes(value = {com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule.class}) public class MyAppGlideModule extends AppGlideModule { private static final String DISK_CACHE_DIR = "Glide_cache"; private static final long DISK_CACHE_SIZE = 100 << 20; // 100M @Override public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) { builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DISK_CACHE_DIR, DISK_CACHE_SIZE)); } @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .eventListener(new EventListener() { @Override public void callStart(Call call) { // 输出日志,用于确认使用了我们配置的 OkHttp 进行网络请求 LogUtils.d(call.request().url().toString()); } }) .build(); registry.replace(GlideUrl.class, InputStream.class, new Factory(okHttpClient)); } @Override public boolean isManifestParsingEnabled() { // 不使用 Manifest 中的 GlideModule return false; } } ``` 这样我们通过自己的配置指定网络中图片加载需要使用 OkHttp. 并且自定义了 OkHttp 的超时时间等参数。按照上面的方式我们可以在 Glide 中使用 OkHttp 来加载网络中的图片了。 不过,当我们在项目中引用了 `okhttp3-integration` 的依赖之后,不进行上述配置一样可以使用 OkHttp 来进行网络图片加载的。这是因为上述依赖的包中已经提供了一个自定义的 GlideModule,即 `OkHttpLibraryGlideModule`。该类使用了 `@GlideModule` 注解,并且已经指定了网络图片加载使用 OkHttp。所以,当我们不自定义 GlideModule 的时候,只使用它一样可以在 Glide 中使用 OkHttp. 如果我们使用了自定义的 GlideModule,当我们编译的时候会看到 `GeneratedAppGlideModuleImpl` 中的 `registerComponents()` 方法定义如下: ```java @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { new OkHttpLibraryGlideModule().registerComponents(context, glide, registry); appGlideModule.registerComponents(context, glide, registry); } ``` 这里先调用了 `OkHttpLibraryGlideModule` 的 `registerComponents()` 方法,然后调用了我们自定义的 GlideModule 的 `registerComponents()` 方法,只是,我们的 GlideModule 的 `registerComponents()` 方法会覆盖掉 `OkHttpLibraryGlideModule` 中的实现。(因为我们的 GlideModule 的 `registerComponents()` 方法中调用的是 `Registry` 的 `replace()` 方法,会替换之前的效果。) 如果不希望多此一举,我们可以直接在自定义的 GlideModule 中使用 `@Excludes` 注解,并指定 `OkHttpLibraryGlideModule` 来直接排除该类。这样 `GeneratedAppGlideModuleImpl` 中的 `registerComponents()` 方法将只使用我们自定义的 GlideModule. 以下是排除之后生成的类中 `registerComponents()` 方法的实现: ```java @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { appGlideModule.registerComponents(context, glide, registry); } ``` ## 3、总结 在本文中,我们通过介绍 Glide 的两种常见的配置方式来分析了 Glide 的部分源码实现。在这部分中,我们重点介绍了初始化 Glide 的并获取 `GlideModule` 的过程,以及与图片资源的时候相关的 `ModelLoader` 等的源码。了解这部分内容是比较重要的,因为它们是暴露给用户的 API 接口,比较常用;并且对这些类简单了解之后能够不至于在随后分析 Glide 整个加载流程的时候迷路。 这里我们对上面两种配置方式中涉及到的类进行一个分析。如下图所示 ![Glide源码](res/glide_configuration.png) 当我们初始化 Glide 的时候会使用 `Registry` 的 `append()` 等一系列的方法构建`资源类型-加载方式-输出类型` 的一个映射,然后当我们使用 Glide 进行记载的时候,会先根据资源类型找到对应的加载方式,然后使用该加载方式从指定的数据源中加载数据,并将其转换成指定的输出类型。 以上面我们自定义图片加载方式的过程为例,这里我们自定义了一个资源类型 `CacheImage`,并通过自定义 GlideModule 指定了它的加载实现是我们自定义的 `ImageLoader` 类。然后,在我们自定义的 ImageLoader 中,我们指定了获取该资源的缓存的键的方式和从数据源中记载数据的具体实现 `HttpUrlFetcher`。这样,当 Glide 要加载某个 CacheImage 的时候,会先使用该缓存的键尝试从缓存中获取,拿不到结果之后使用 `HttpUrlFetcher` 从网络当中获取数据。从网络中获取数据的时候会得到 InputStream,最后,再调用一个回调类,使用 BitmapFactory 从 InputStream 中获取 Bitmap 并将其显示到 ImageView 上面,这样就完成了整个图片加载的流程。 从上文的分析中,我们可以总结出 Glide 的几个设计人性的地方: 1. 使用代理类包装自定义 GlideModule,然后可以使用发射获取该代理类,并通过调用代理类的方法来间接调用我们的 GlideModuel; 2. 构建`资源类型-加载方式-输出类型`映射的时候使用工厂方法而不是通过某个类建立一对一映射。 上面我们通过 Glide 的几种配置方式简单介绍了 Glide 的图片加载流程。其实际的执行过程远比我们上述过程更加复杂。在下文中我们会对 Glide 的图片加载的主流程进行分析。欢迎继续关注和阅读! ================================================ FILE: 图片加载/图片压缩框架封装.md ================================================ # 开源一个 Android 图片压缩框架 在我们的业务场景中,需要使用客户端采集图片,上传服务器,然后对图片信息进行识别。为了提升程序的性能,我们需要保证图片上传服务器的速度的同时,保证用于识别图片的质量。整个优化包括两个方面的内容: 1. 相机拍照的优化:包括相机参数的选择、预览、启动速度和照片质量等; 2. 图片压缩的优化:基于拍摄的图片和从相册中选择的图片进行压缩,控制图片大小和尺寸。 在本文中,我们主要介绍图片压缩优化,后续我们会介绍如何对 Android 的相机进行封装和优化。本项目主要基于 Android 自带的图片压缩 API 进行封装,结合了 Luban 和 Compressor 的优点,同时提供了用户自定义压缩策略的接口。该项目的主要目的在于,统一图片压缩框库的实现,集成常用的两种图片压缩算法,让你以更低的成本集成图片压缩功能到自己的项目中。 ## 1、图片压缩的基础知识 对于一般业务场景,当我们展示图片的时候,Glide 会帮我们处理加载的图片的尺寸问题。但在把采集来的图片上传到服务器之前,为了节省流量,我们需要对图片进行压缩。 在 Android 平台上,默认提供的压缩有三种方式:质量压缩和两种尺寸压缩,邻近采样以及双线性采样。下面我们简单介绍下者三种压缩方式都是如何使用的: ### 1.1 质量压缩 所谓的质量压缩就是下面的这行代码,它是 Bitmap 的方法。当我们得到了 Bitmap 的时候,即可使用这个方法来实现质量压缩。它一般位于我们所有压缩方法的最后一步。 ```java // android.graphics。Bitmap compress(CompressFormat format, int quality, OutputStream stream) ``` 该方法接受三个参数,其含义分别如下: 1. format:枚举,有三个选项 `JPEG`, `PNG` 和 `WEBP`,表示图片的格式; 2. quality:图片的质量,取值在 `[0,100]` 之间,表示图片质量,越大,图片的质量越高; 3. stream:一个输出流,通常是我们压缩结果输出的文件的流 ### 1.2 邻近采样 邻近采样基于临近点插值算法,用像素代替周围的像素。邻近采样的核心代码只有下面三行, ```java BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 1; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options); ``` 邻近采样核心的地方在于 `inSampleSize` 的计算。它通常是我们使用的压缩算法的第一步。我们可以通过设置 inSampleSize 来得到原始图片采样之后的结果,而不是将原始的图片全部加载到内存中,以防止 OOM。标准使用姿势如下: ```java // 获取原始图片的尺寸 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.inSampleSize = 1; BitmapFactory.decodeStream(srcImg.open(), null, options); this.srcWidth = options.outWidth; this.srcHeight = options.outHeight; // 进行图片加载,此时会将图片加载到内存中 options.inJustDecodeBounds = false; options.inSampleSize = calInSampleSize(); Bitmap bitmap = BitmapFactory.decodeStream(srcImg.open(), null, options); ``` 这里主要分成两个步骤,它们各自的含义是: 1. 先通过设置 Options 的 `inJustDecodeBounds` 为 true,来加载图片,以得到图片的尺寸信息。此时图片不会被加载到内存中,所以不会造成 OOM,同时我们可以通过 Options 得到原图的尺寸信息。 2. 根据上一步中得到的图片的尺寸信息,计算一个 inSampleSize,然后将 inJustDecodeBounds 设置为 false,以加载采样之后的图片到内存中。 关于 inSampleSize 需要简单说明一下:inSampleSize 代表压缩后的图像一个像素点代表了原来的几个像素点,例如 inSampleSize 为 4,则压缩后的图像的宽高是原来的 1/4,像素点数是原来的 1/16,inSampleSize 一般会选择 2 的指数,如果不是 2 的指数,内部计算的时候也会向 2 的指数靠近。所以,实际使用过程中,我们会通过明确指定 inSampleSize 为 2 的指数,来避免内部计算导致的不确定性。 ### 1.3 双线性采样 邻近采样可以对图片的尺寸进行有效的控制,但是它存在几个问题。比如,当我需要把图片的宽度压缩到 1200 左右的时候,如果原始的图片的宽度压是 3200,那么我只能通过设置 inSampleSize 将采样率设置为 2 来将其压缩到 1600. 此时图片的尺寸比我们的要求要大。就是说,邻近采样无法对图片的尺寸进行更加精准的控制。如果需要对图片尺寸进行更加精准的控制,那么就需要使用双线性压缩了。 双线性采样采用双线性插值算法,相比邻近采样简单粗暴的选择一个像素点代替其他像素点,双线性采样参考源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算得到目标图像。 它在 Android 中的使用也比较简单, ```java Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red); Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true); ``` 也就是对得到的 Bitmap 应用 `createBitmap()` 进行处理,并传入 Matrix 指定图片尺寸放缩的比例。该方法返回的 Bitmap 就是双线性压缩之后的结果。 ### 1.4 图片压缩算法总结 在实际使用过程中,我们通常会结合三种压缩方式使用,一般使用的步骤如下, 1. 使用邻近采样对原始的图片进行采样,将图片控制到比目标尺寸稍大的大小,防止 OOM; 2. 使用双线性采样对图片的尺寸进行压缩,控制图片的尺寸为目标的大小; 3. 对上述两个步骤之后得到的图片 Bitmap 进行质量压缩,并将其输出到磁盘上。 当然,本质上 Android 图片的编码是由 [Skia](https://skia.org/index_zh) 库来完成的,所以,除了使用 Android 自带的库进行压缩,我们还可以调用外部的库进行压缩。为了追求更高的压缩效率,通常我们会在 Native 层对图片进行处理,这将涉及 JNI 的知识。笔者曾在之前的文章 [《在 Android 中使用 JNI 的总结》](https://juejin.im/post/5c79f5d0518825347a56275f) 中介绍过 Android 平台上 JNI 的调用的常规思路,感兴趣的同学可以参考下。 ## 2、Github 上的开源的图片压缩库 现在 Github 上的图片压缩框架主要有 Luban 和 Compressor 两个。Star 的数量也比较高,一个 9K,另一个 4K. 但是,这两个图片压缩的库有各自的优点和缺点。下面我们通过一个表格总结一下: |框架|优点|缺点| |:-:|:-|:-| |Luban|据说是根据微信图片压缩逆推的算法|1.只适用于一般的图片展示的场景,无法对图片的尺寸进行精准压缩;2.内部封装 AsyncTaks 来进行异步的图片压缩,对于 RxJava 支持不好。| |Compressor|1.可以对图片的尺寸进行压缩;2.支持 RxJava。|1.尺寸压缩的场景有限,如果有特别的需求,则需要手动修改源代码;2.图片压缩采样的时候计算有问题,导致采样后的图片尺寸总是小于我们指定的尺寸| 上面的图表已经总结得很详细了。所以,根据上面的两个库各自的优缺点,我们打算开发一个新的图片压缩框架。它满足下面的功能: 1. 支持 RxJava:我们可以像使用 Compressor 的时候那样,指定图片压缩的线程和结果监听的线程; 2. 支持 Luban 压缩算法:Luban 压缩算法核心的部分只在于 inSampleSize 的计算,因此,我们可以很容易得将其集成到我们的新的库中。之所以加入 Luban,是为了让我们的库可以适用于一般图片展示的场景。用户无需指定图片的尺寸,用起来省心省力。 3. 支持 Compressor 压缩算法同时指定更多的参数:Compressor 压缩算法就是我们上述提到的三种压缩算法的总和。不过,当要压缩的宽高比与原始图片的宽高比不一致的时候,它只提供了一种情景。下文中介绍我们框架的时候会说明进行更详细的说明。当然,你可以在调用框架的方法之前主动去计算出一个宽高比,但是你需要把图片压缩的第一个阶段主动走一遍,费心费力。 4. 提供用户自定义压缩算法的接口:我们希望设计的库可以允许用户自定义压缩策略。在想要替换图片压缩算法的时候,通过链式调用的一个方法直接更换策略即可。即,我们希望能够让用户以最低的成本替换项目中的图片压缩算法。 ## 3、项目整体架构 以下是我们的图片压缩框架的整体架构,这里我们只列举除了其中核心的部分代码。这里的 Compress 是我们的链式调用的起点,我们可以用它来指定图片压缩的基本参数。然后,当我们使用它的 `strategy()` 方法之后,方法将进入到图片压缩策略中,此时,我们继续链式调用压缩策略的自定义方法,个性化地设置各压缩策略自己的参数: ![项目整体架构](res/QQ截图20190312223345.png) 这里的所有的压缩策略都继承自抽线的基类 AbstractStrategy,它提供了两个默认的实现 Luban 和 Compressor. 接口 CompressListener 和 CacheNameFactory 分别用来监听图片压缩进度和自定义压缩的图片的名称。下面的三个是图片相关的工具类,用户可以调用它们来实现自己压缩策略。 ## 4、使用 首先,在项目的 Gradle 中加入我的 Maven 仓库的地址: maven { url "https://dl.bintray.com/easymark/Android" } 然后,在你的项目的依赖中,添加该库的依赖: implementation 'me.shouheng.compressor:compressor:0.0.1' 然后,就可以在项目中使用了。你可以参考 Sample 项目的使用方式。不过,下面我们还是对它的一些 API 做简单的说明。 ### 4.1 Luban 的使用 下面是 Luban 压缩策略的使用示例,它与 Luban 库的使用类似。只是在 Luban 的库的基础上,我们增加了一个 copy 的选项,用来表示当图片因为小于指定的大小而没有被压缩之后,是否将原始的图片拷贝到指定的目录。因为,比如当你使用回调获取图片压缩结果的时候,如果按照 Luban 库的逻辑,你得到的是原始的图片,所以,此时你需要额外进行判断。因此,我们增加了这个布尔类型的参数,你可以通过它指定将原始文件进行拷贝,这样你就不需要在回调中对是否是原始图片进行判断了。 ```kotlin // 在 Compress 的 with() 方法中指定 Context 和 要压缩文件 File val luban = Compress.with(this, file) // 这里添加一个回调,如果你不使用 RxJava,那么可以用它来处理压缩的结果 .setCompressListener(object : CompressListener{ override fun onStart() { LogUtils.d(Thread.currentThread().toString()) Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show() } override fun onSuccess(result: File?) { LogUtils.d(Thread.currentThread().toString()) displayResult(result?.absolutePath) Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show() } override fun onError(throwable: Throwable?) { LogUtils.d(Thread.currentThread().toString()) Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show() } }) // 压缩图片的名称工厂方法,用来指定压缩结果的文件名 .setCacheNameFactory { System.currentTimeMillis().toString() } // 图片的质量 .setQuality(80) // 上面基本的配置完了,下面指定图片的压缩策略为 Luban .strategy(Strategies.luban()) // 指定如果图片小于等于 100K 就不压缩了,这里的参数 copy 表示,如果不压缩的话要不要拷贝文件 .setIgnoreSize(100, copy) // 按上面那样得到了 Luban 实例之后有下面两种方式启动图片压缩 // 启动方式 1:使用 RxJava 进行处理 val d = luban.asFlowable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { displayResult(it.absolutePath) } // 启动方式 2:直接启动,此时使用内部封装的 AsyncTask 进行压缩,压缩结果只能在上面的回调中进行处理了 luban.launch() ``` ### 4.2 Compressor 的使用 下面是 Compressor 压缩策略的基本的使用,在调用 `strategy()` 方法指定压缩策略之前,你的任务与 Luban 一致。所以,如果你需要更换图片压缩算法的时候,直接使用 `strategy()` 方法更换策略即可,前面部分的逻辑无需改动,因此,可以降低你更换压缩策略的成本。 ```kotlin val compressor = Compress.with(this, file) .setQuality(60) .setTargetDir("") .setCompressListener(object : CompressListener { override fun onStart() { LogUtils.d(Thread.currentThread().toString()) Toast.makeText(this@MainActivity, "Compress Start", Toast.LENGTH_SHORT).show() } override fun onSuccess(result: File?) { LogUtils.d(Thread.currentThread().toString()) displayResult(result?.absolutePath) Toast.makeText(this@MainActivity, "Compress Success : $result", Toast.LENGTH_SHORT).show() } override fun onError(throwable: Throwable?) { LogUtils.d(Thread.currentThread().toString()) Toast.makeText(this@MainActivity, "Compress Error :$throwable", Toast.LENGTH_SHORT).show() } }) .strategy(Strategies.compressor()) .setMaxHeight(100f) .setMaxWidth(100f) .setScaleMode(Configuration.SCALE_SMALLER) .launch() ``` 这里的 `setMaxHeight(100f)` 和 `setMaxWidth(100f)` 用来表示图片压缩的目标大小。具体的大小是如何计算的呢?在 Compressor 库中你是无法确定的,但是在我们的库中,你可以通过 `setScaleMode()` 方法来指定。这个方法接收一个整数类型的枚举,它的取值范围有 4 个,即 `SCALE_LARGER`, `SCALE_SMALLER`, `SCALE_WIDTH` 和 `SCALE_HEIGHT`,它们具体的含义我们会进行详细说明。这里我们默认的压缩方式是 SCALE_LARGER,也就是 Compressor 库的压缩方式。那么这四个参数分别是什么含义呢? 这里我们以一个例子来说明,假设有一个图片的宽度是 1000,高度是 500,简写作 (W:1000, H:500),通过 `setMaxHeight()` 和 `setMaxWidth()` 指定的参数均为 100,那么,就称目标图片的尺寸,宽度是 100,高度是 100,简写作 (W:100, H:100)。那么按照上面的四种压缩方式,最终的结果将是: - **SCALE_LARGER**:对高度和长度中较大的一个进行压缩,另一个自适应,因此压缩结果是 (W:100, H:50). 也就是说,因为原始图片宽高比 2:1,我们需要保持这个宽高比之后再压缩。而目标宽高比是 1:1. 而原图的宽度比较大,所以,我们选择将宽度作为压缩的基准,宽度缩小 10 倍,高度也缩小 10 倍。这是 Compressor 库的默认压缩策略,显然它只是优先使得到的图片更小。这在一般情景中没有问题,但是当你想把短边控制在 100 就无计可施了(需要计算之后再传参),此时可以使用 SCALE_SMALLER。 - **SCALE_SMALLER**:对高度和长度中较大的一个进行压缩,另一个自适应,因此压缩结果是 (W:200, H:100). 也就是,高度缩小 5 倍之后,达到目标 100,然后宽度缩小 5 倍,达到 200. - **SCALE_WIDTH**:对宽度进行压缩,高度自适应。因此得到的结果与 SCALE_LARGER 一致。 - **SCALE_HEIGHT**:对高度进行压缩,宽度自适应,因此得到的结果与 SCALE_HEIGHT 一致。 ### 4.3 自定义策略 自定义一个图片压缩策略也是很简单的,你可以通过继承 SimpleStrategy 或者直接继承 AbstractStrategy 来实现: ```kotlin class MySimpleStrategy: SimpleStrategy() { override fun calInSampleSize(): Int { return 2 } fun myLogic(): MySimpleStrategy { return this } } ``` 注意下,如果想要实现链式的调用,自定义压缩策略的方法需要返回自身。 ## 5、最后 因为我们的项目中,需要把图片的短边控制到 1200,长变只适应,只通过改变 Luban 来改变采样率只能把边长控制到一个范围中,无法精准压缩。所以,我们想到了 Compressor,并提出了 SCALE_SMALLER 的压缩模式. 但是 Luban 也不是用不到,一般用来展示的图片的压缩,它用起来更加方便。因此,我们在库中综合了两个框架,其实代码量并不大。当然,为了让我们的库功能更加丰富,因此我们提出了自定义压缩策略的接口,也是用来降低压缩策略的更换成本吧。 最后项目开源在 Github,地址是:https://github.com/Shouheng88/Compressor. 欢迎 Star 和 Fork,为该项目贡献代码或者提出 issue :) 后续,笔者会对 Android 端的相机优化和 JNI 操作 OpenCV 进行图片处理进行讲解,感兴趣的关注作者呦 :) ================================================ FILE: 工作空间/OOM优化.md ================================================ ### Oom OOM就是所谓的内存溢出(Out Of Memory),也就是说内存占有量超过了VM所分配的最大 ### 出现OOM的原因 1. 加载对象过大 2. 相应资源过多,来不及释放 ### 如何解决 1. 在内存引用上做些处理,常用的有软引用、强化引用、弱引用 2. 在内存中加载图片时直接在内存中作处理,如边界压缩 3. 动态回收内存 4. 优化Dalvik虚拟机的堆内存分配 5. 自定义堆内存大小 ================================================ FILE: 工作空间/Tinker.md ================================================ # Tinker 热补丁的源码分析 Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。 Github https://github.com/Tencent/tinker 它主要包括以下几个部分: 1. gradle编译插件: `tinker-patch-gradle-plugin` 2. 核心sdk库: `tinker-android-lib` 3. 非gradle编译用户的命令行版本: `tinker-patch-cli.jar` ================================================ FILE: 工作空间/URL编码问题.md ================================================ 今天在使用URL访问别人的接口的时候,因为要查询的数据中包含中文就出现了问题。 http://api.map.baidu.com/telematics/v3/weather?location=北京&output=json&ak=XXXX 使用 URL url = new URL(address); connection = (HttpURLConnection) url.openConnection(); 来打开数据连接,但是始终获取不到数据。这是因为要访问的URL中包含中文“北京”,因而无法查询。 如何修改这个问题呢? 实际上,将上面的链接复制到浏览器进行访问是没有问题的。这时,如果细心的话就会发现,粘贴 之后被访问的连接并非粘贴过去的那串文字,其中的“北京”两个字变成了 http://api.map.baidu.com/telematics/v3/weather?location=%E5%8C%97%E4%BA%AC&output=json&ak=XXXX 就是将“北京”连个字做了变化。所以,要想使用中文的URL的话,也应该将其中的中文进行转码之后再访问。 那么如何转码呢? String city = mEditText.getText().toString(); try { city = URLEncoder.encode(city, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } 使用ERLEncoder.encode即可。这样中文就变成上面那串带有%的字符串了。 ================================================ FILE: 工作空间/文章暂时存放.md ================================================ [ Android ̱Ҫ֪һ](https://mp.weixin.qq.com/s/FudvsrZEEVnAbtMokpjP0Q) [Androidڴй©ĵ](https://mp.weixin.qq.com/s/Ag1QtUgIS5WSemyY2pJ8-A) [ŵAndroidͻںϼܹݽ֮·](https://mp.weixin.qq.com/s/4mI5p6oVmEO3Q1v0SaH0TQ) ================================================ FILE: 工作空间/百度定位API.md ================================================ ### 百度语音识别API异常: 异常1: 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) so文件存在冲突,只要保证libs文件夹下面只有armeabi一个文件夹即可。 可能之前因为想要兼容各不同的系统所以在使用百度定位的时候创建了多个文件夹,这里需要只留下armeabi一个文件夹。 ## 使用百度API进行定位(Android Studio) ### 1.步骤1:获取密钥 ### 2.步骤2:下载API开发包 ### 3.步骤3:配置环境 #### 3.1 导入jar文件 切换Android Studio的工作目录为Project,在lib文件夹下面导入解压后的API开发包。注意要将.so文件和jar文件同时导入,对jar文件还要在其上面右键单击,选择Add as Library,将其作为库导入进来。 #### 3.2 配置gradle 对使用Android Studio开发的同学,仅仅做到这些还是不够的,还要在build.gradle(Modile:app)中添加 sourceSets { main { jniLibs.srcDirs = ['libs'] } } 这里的作用是添加.so文件,没有添加.so文件或者添加了.so文件而没有在gradle中进行配置的同学,都无法正常使用百度进行定位。 #### 3.3 在AndroidManifest.xml中进行配置 1.声明要使用的权限 2.在application中声明service组件 每个app拥有自己单独的定位 3.在application中声明APP KEY: ### 步骤4:添加代码 我们将与定位相关的代码全部放在一个工具类中 public class LocationUtils { private static LocationUtils sInstance; private LocationClient mLocationClient; private BDLocationListener bdLocationListener; public static LocationUtils getInstance(Context mContext){ if (sInstance == null){ sychronized(LocationUtils.class) { if (sInstance == null) { sInstance = new LocationUtils(mContext.getApplicationContext()); } } } return sInstance; } private LocationUtils(Context mContext){ mLocationClient = new LocationClient(mContext); } public void locate(BDLocationListener mListener){ bdLocationListener = mListener; mLocationClient.registerLocationListener(bdLocationListener); LocationClientOption option = new LocationClientOption(); option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy); option.setIsNeedAddress(true); mLocationClient.setLocOption(option); mLocationClient.requestLocation(); mLocationClient.start(); } public void stop() { if (mLocationClient.isStarted()) { mLocationClient.stop(); if (bdLocationListener != null) { mLocationClient.unRegisterLocationListener(bdLocationListener); bdLocationListener = null; } } } } 这样每当我们需要定位的时候就可以这样调用: ToastUtils.showShortToast(getContext(), R.string.trying_to_get_location); LocationUtils.getInstance(getContext()).locate(new BDLocationListener() { @Override public void onReceiveLocation(BDLocation bdLocation) { // ... 然后当bdLocation不为空的时候,直接从bdLocation上面获取位置信息就可以了 } }); ### 常见错误: #### 1.无法获取位置信息:在监听器当中没有获取到位置的详细信息 原因可能是: 1. 没有添加 option.setIsNeedAddress(true); 这行代码是用来设置用户需不需要获取返回信息的,老版本的方法是使用 option.setAddrType(“all”) 实际上,根据源代码这两个代码的功能是相同的,只是后者现在被抛弃了,使用前者即可。 2. 没有在AndroidManifest.xml文件中添加 出现上面的这种情况也是无法获取的。 3. 没有添加.so文件或者添加了.so文件但是没有在gralde文件中进行声明。前面提到过关于.so文件在gralde中声明的方法。 ================================================ FILE: 工作空间/第三方库整理.md ================================================ # ## 1 ### 1.1 ܣܺʾ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ٿ|[FastDev4Android](https://github.com/jiangqqlmj/FastDev4Android)|MVP,EventBus,ʽ,ORM,| |2||[ArmsComponent](https://github.com/JessYanCoding/ArmsComponent)|Arms,MVP,| |3|MVPٻ|[MVPArms](https://github.com/JessYanCoding/MVPArms)|Arms,MVP,ÿģ| |4|MVVM|[Android-mvvm-architecture](https://github.com/MindorksOpenSource/android-mvvm-architecture)|MVVMʾ| |5|MVVM|[AndroidArchitecture](https://github.com/iammert/AndroidArchitecture)|LiveData, Room Persistence, Dagger 2, Retrofit, MVVM and DataBinding| ### 1.2 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|HTTPͻ|[Retrofit](https://github.com/square/retrofit)|Type-safe HTTP client for Android and Java by Square, Inc. | |2|첽Http|[Android-async-http](https://github.com/loopj/android-async-http)|| |3|װOKHttp|[OKHttpUtils](https://github.com/duzechao/OKHttpUtils)|| |4|װOKHttp|[OkGo](https://github.com/jeasonlzy/okhttp-OkGo)|| |5|OKHttp|[OKHttp](https://github.com/square/okhttp)|| |6|װOKHttp|[Okhttputils](https://github.com/hongyangAndroid/okhttputils)|| ### 1.3 ־û |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|̻|[DiskLruCache](https://github.com/JakeWharton/DiskLruCache)|| |2|ݿ|[WCDB](https://github.com/Tencent/wcdb)|ƽ̨ݿ| |3|ݿ|[GreenDAO](https://github.com/greenrobot/greenDAO)|| ### 1.4 && |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[Android-Debug-Database](https://github.com/amitshekhariitbhu/Android-Debug-Database)|| |2||[GT](https://github.com/Tencent/GT)|ѶAPPƽ̨| |3||[Stetho](https://github.com/facebook/stetho)|| |4||[ACRA](https://github.com/ACRA/acra)|| ### 1.5 ޸ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|޸|[AndFix](https://github.com/alibaba/AndFix)|| |2|޸|[tinker](https://github.com/Tencent/tinker)|| ### |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|UI|[RapidView](https://github.com/Tencent/RapidView)|UI| |2|Ϸ|[Libgdx](https://github.com/libgdx/libgdx)|ƽ̨JavaϷ| |3|ȸʾ|[Android-architecture-components](https://github.com/googlesamples/android-architecture-components)|| |4|ע|[Dagger](https://github.com/square/dagger)|| |5|¼ͨ|[EventBus](https://github.com/greenrobot/EventBus)|| ## 2ý ### 2.1 RecyelerView |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[BaseRecyclerViewAdapterHelper](https://github.com/CymChad/BaseRecyclerViewAdapterHelper)|ͨAdapterװȽʹ| |2||[FastAdapter](https://github.com/mikepenz/FastAdapter)|| |3|RecyclerView|[XRecyclerView](https://github.com/XRecyclerView/XRecyclerView)|ˢºͼظ| |4|RecyclerView|[SwipeRecyclerView](https://github.com/yanzhenjie/SwipeRecyclerView)|໬˵| |5|RecyclerView|[EasyRecyclerView](https://github.com/Jude95/EasyRecyclerView)|More,Header/Footer,EmptyView,ProgressView,ErrorView| |6|RecyclerView|[MaterialScrollBar](https://github.com/turing-tech/MaterialScrollBar)|ٻ| |7|RecyclerView|[sticky-headers-recyclerview](https://github.com/timehop/sticky-headers-recyclerview)|̶| |8|RecyclerView|[RecyclerViewSnap](https://github.com/rubensousa/RecyclerViewSnap)|| |9|RecyclerView|[RecyclerViewFastScroller](https://github.com/danoz73/RecyclerViewFastScroller)|ٻ| |10|RecyclerView|[RecyclerView-FastScroll](https://github.com/timusus/RecyclerView-FastScroll)|| |11|RecyclerView|[android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)|ں˶RecyclerView| |12|LayoutManager|[vlayout](https://github.com/alibaba/vlayout)|,LayoutManager| ### 2.2 ͼƬ #### 2.2.1 ͼƬģ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ͼƬģ|[StackBlur](https://github.com/kikoso/android-stackblur)|| |2|ͼƬģ|[GaussianBlur](https://github.com/jrvansuita/GaussianBlur)|| #### 2.2.2 ͼƬ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ͼƬ༭|[PhotoEditDemo](https://github.com/jarlen/PhotoEditDemo)|| |2|ͼƬ༭|[PhotoEdit](https://github.com/jarlen/PhotoEdit)|| |3|άɨ|[Barcodescanner](https://github.com/dm77/barcodescanner)|| |4|ͼƬѹ|[Luban](https://github.com/Curzibn/Luban)|| #### 2.2.3 ͼƬ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ͼƬ|[Universal-Image-Loader](https://github.com/nostra13/Android-Universal-Image-Loader)|| |2|ͼƬ|[Picasso](https://github.com/square/picasso)|| |3|ͼƬ|[Fresco](https://github.com/facebook/fresco)|| |4|ͼƬ|[Glide](https://github.com/bumptech/glide)|| #### 2.2.4 ͼƬչʾ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ԲͼƬ|[CircleImageView](https://github.com/hdodenhof/CircleImageView)|| |2|ͼƬ|[Subsampling-scale-image-view)](https://github.com/davemorrissey/subsampling-scale-image-view)|| |3|ͼƬ|[PhotoView](https://github.com/bm-x/PhotoView)|| |4|ԲͼƬ|[RoundedImageView](https://github.com/vinc3m1/RoundedImageView)|| #### 2.2.4 ͼƬѡ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ͼƬѡ|[ImagePicker](https://github.com/martin90s/ImagePicker)|| |2|ͼƬѡ|[ImagePicker](https://github.com/jeasonlzy/ImagePicker)|| |3|ͼƬѡ|[ImagePicker](https://github.com/martin90s/ImagePicker)|| ### 2.3 Ƶ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| ### 2.4 Ƶ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|Ƶ|[NiceVieoPlayer](https://github.com/xiaoyanger0825/NiceVieoPlayer)|| |2|Ƶ|[VideoPlayerManager](https://github.com/danylovolokh/VideoPlayerManager)|| |3|Ƶ¼|[RecordVideoDemo](https://github.com/szitguy/RecordVideoDemo)|| ### 2.5 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ؼ|[Cameraview](https://github.com/google/cameraview)|| |2||[CameraFragment](https://github.com/florent37/CameraFragment)|| |3|&ͼƬ|[MagicCamera](https://github.com/wuhaoyu1990/MagicCamera)|յ40ʵʱ˾ա¼ͼƬ޸| ### 2.6 ͼ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ͼ|[Android-maps-utils](https://github.com/googlemaps/android-maps-utils)|| ## 3 ### 3.1 Android߿ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[RxTool](https://github.com/vondear/RxTool)|AndroidԱòռĹ༯| |2||[AndroidUtils](https://github.com/Blizzard-liu/AndroidUtils)|׿ʨر| |3||[XUtils3](https://github.com/wyouflf/xUtils3)|| |4||[AndroidDeviceNames](https://github.com/jaredrummler/AndroidDeviceNames)|ȡAndroid豸| |5||[Android-proguards](https://github.com/yongjhih/android-proguards)|| |6|Gitļ|[Gitignore](https://github.com/github/gitignore)|| |7|Ȩ|[AndPermission](https://github.com/yanzhenjie/AndPermission)|| |8||[Dex2jar](https://github.com/pxb1988/dex2jar)|| |9|Lambda|[Retrolambda](https://github.com/orfjackal/retrolambda)|Backport of Java 8's lambda expressions to Java 7, 6 and 5| |10|Lambda|[Gradle-retrolambda](https://github.com/evant/gradle-retrolambda)|A gradle plugin for getting java lambda support in java 6, 7 and android| ### 3.2 Java |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|Java|[Guava](https://github.com/google/guava)|Google Java Ŀ| |2|Java㷨|[TheAlgorithms-Java](https://github.com/TheAlgorithms/Java)|| |3|ģʽ|[Java-design-patterns](https://github.com/iluwatar/java-design-patterns)|| |4|ʱAPI|[Joda-time](https://github.com/JodaOrg/joda-time)|| ### 3.3 Java |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[Encrypt](https://github.com/GcsSloop/encrypt)|| |2|Markdown|[Flexmark-java](https://github.com/vsch/flexmark-java)|| |3|ѧ|[Calci-kernel](https://github.com/Iraka-C/Calci-kernel)|A complex calculation kernel in Java (for Calci calculator)| |4|תƴ|[TinyPinyin](https://github.com/promeG/TinyPinyin)|| ## 4Ԫ ### 4.1 л |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[BGASwipeBackLayout-Android](https://github.com/bingoogolapple/BGASwipeBackLayout-Android)|| |2|л|[Transitions](https://github.com/rubensousa/Transitions)|ʾ| |3||[Pull-back-layout](https://github.com/oxoooo/pull-back-layout)|| ### 4.2 沼 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[MaterialDrawer](https://github.com/mikepenz/MaterialDrawer)|| |2|·ҳ|[android-vertical-slide-view](https://github.com/xmuSistone/VerticalSlideFragment)|Ʒҳ϶ʱԼһҳ| |3|ҷҳ|[MaterialViewPager](https://github.com/florent37/MaterialViewPager)|| |4|ײ|[AndroidSlidingUpPanel](https://github.com/umano/AndroidSlidingUpPanel)|| |5|ViewPagerָʾ|[ViewPagerIndicator](https://github.com/JakeWharton/ViewPagerIndicator)|| |6|ViewPagerָʾ|[MagicIndicator](https://github.com/hackware1993/MagicIndicator)|| |7|бؼViewPager|[DiscreteScrollView](https://github.com/yarolegovich/DiscreteScrollView)|| |8|ԽЧ|[OverScroll-Everywhere](https://github.com/Mixiaoxiao/OverScroll-Everywhere)|ߵײʱЧ| |9|ٻ|[FastScroll-Everywhere](https://github.com/Mixiaoxiao/FastScroll-Everywhere)|ΪɻViewӿٻ| ### 4.3 Ч |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ˮɢЧ|[WaveView](https://github.com/hackware1993/WaveView)|| |2|Ч|[LikeAnimation](https://github.com/frogermcs/LikeAnimation)|| |3||[Backboard](https://github.com/tumblr/Backboard)|| |4|ؼ|[AndroidViewAnimations](https://github.com/daimajia/AndroidViewAnimations)|| |5|ûʾ|[MaterialTapTargetPrompt](https://github.com/sjwall/MaterialTapTargetPrompt)|| ### 4.4 ҳ棺ټ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ӭҳ|[AppIntro](https://github.com/apl-devs/AppIntro)|| |2|ҳ|[MaterialAbout](https://github.com/jrvansuita/MaterialAbout)|| ### 4.5 Ի |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ײ|[BottomDialog](https://github.com/shaohui10086/BottomDialog)|| |2|ײ|[bottomsheet](https://github.com/Flipboard/bottomsheet)|| |3|ײ|[BottomSheet](https://github.com/Kennyc1012/BottomSheet)|| |4|ײ|[BottomSheetBuilder](https://github.com/rubensousa/BottomSheetBuilder)|| |5|ѡ|[AndroidPicker](https://github.com/gzu-liyujiang/AndroidPicker)|| |6|ֶԻ|[material-dialogs](https://github.com/afollestad/material-dialogs)|| |7|ɫѡ|[HoloColorPicker](https://github.com/LarsWerkman/HoloColorPicker)|| |8|ںʱѡ|[datetimepicker](https://github.com/flavienlaurent/datetimepicker)|| ### 4.6 ʱ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[CalendarView](https://github.com/huanghaibin-dev/CalendarView)|| |2||[Android-Week-View](https://github.com/alamkanak/Android-Week-View)|1죬3죬7| |3||[Calendar](https://github.com/xiaojianglaile/Calendar)|| |4|С|[Android-MonthCalendarWidget](https://github.com/romannurik/Android-MonthCalendarWidget)|| |5||[AgendaCalendarView](https://github.com/Tibolte/AgendaCalendarView)|| |6|ʱ|[GAHonorClock](https://github.com/Ajian-studio/GAHonorClock)|| ### 4.7 ؼϼ #### 4.7.1 ͼ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[MPAndroidChart](https://github.com/PhilJay/MPAndroidChart)|| |2||[hellocharts](https://github.com/lecho/hellocharts-android)|| #### 4.7.2 ť |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|FAB|[FloatingToolbar](https://github.com/rubensousa/FloatingToolbar)|FABתToolbar| |2|FAB|[FloatingActionButton](https://github.com/Clans/FloatingActionButton)|| #### 4.7.3 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[ActionBarSherlock](https://github.com/JakeWharton/ActionBarSherlock)|| #### 4.7.4 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[MaterialProgressBar](https://github.com/DreaminginCodeZH/MaterialProgressBar)|| |2||[verticalseekbar](https://github.com/h6ah4i/android-verticalseekbar)|| |3||[BubbleSeekBar](https://github.com/woxingxiao/BubbleSeekBar)|| |4||[ProgressWheel](https://github.com/Todd-Davies/ProgressWheel)|| |5||[PreviewSeekBar](https://github.com/rubensousa/PreviewSeekBar)|ƵԤ| |6||[discreteSeekBar](https://github.com/AnderWeb/discreteSeekBar)|| |7|״̬|[AVLoadingIndicatorView](https://github.com/81813780/AVLoadingIndicatorView)|| |8|ʶ|[SpeechRecognitionView](https://github.com/zagum/SpeechRecognitionView)|| #### 4.7.5 ؼԴ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ʸпؼϼ|[material-components-android](https://github.com/material-components/material-components-android)|| |2|Material|[MaterialDesignLibrary](https://github.com/navasmdc/MaterialDesignLibrary)|| #### 4.7.6 ༭ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|MarkdownԤ|[MarkdownView](https://github.com/tiagohm/MarkdownView)|| |2|MarkdownԤ|[RxMarkdown](https://github.com/yydcdut/RxMarkdown)|| |3|ͼƬͿѻ|[Graffiti](https://github.com/1993hzw/Graffiti)|| #### 4.7.7 ť |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ť|[RaiflatButton](https://github.com/rubensousa/RaiflatButton)|| #### 4.7.8 |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[PinLockView](https://github.com/aritraroy/PinLockView)|| #### |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[MaterialSearchView](https://github.com/MiguelCatalan/MaterialSearchView)|| |2|α|[AnchorImageView](https://github.com/jcodeing/AnchorImageView)|| ## 5Դ ### 5.1 ѧϰ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|Android ѧϰ|[AndroidNote](https://github.com/GcsSloop/AndroidNote)|| |2|Android ѧϰ|[CoreLink](https://github.com/lizhangqu/CoreLink)|| |3|Android ѧϰ|[Android_Data](https://github.com/Freelander/Android_Data)|| |4|Android ʾ|[android-examples](https://github.com/nisrulz/android-examples)|| |5|WebView ȫʹ|[WebViewStudy](https://github.com/youlookwhat/WebViewStudy)|| |6|RxJava ѧϰ|[RxJava-Android-Samples](https://github.com/kaushikgopal/RxJava-Android-Samples)|| |7|WebView |[WebviewCapture](https://github.com/hsk256/WebviewCapture)|Webviewļֽʽ| |8|Android |[Android-interview-questions](https://github.com/MindorksOpenSource/android-interview-questions)|| ### 5.2 Դ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|Android Դ|[Android-open-project](https://github.com/Trinea/android-open-project)|| |2|Android Դ|[Android-open-project-analysis](https://github.com/android-cn/android-open-project-analysis)|| |3|Android Դ|[Open-source-android-apps](https://github.com/pcqpcq/open-source-android-apps)|| |4|Android Դ|[Android-Develop-Resources](https://github.com/zmywly8866/Android-Develop-Resources)|| |5|Android ؼ|[Awesome-android-ui](https://github.com/wasabeef/awesome-android-ui)|| |6|Android |[Android-discuss](https://github.com/android-cn/android-discuss)|| |7|Android |[Android-jobs](https://github.com/android-cn/android-jobs)|| ### 5.3 UIԴ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1||[awesome-design-cn](https://github.com/jobbole/awesome-design-cn)|ʦԴ| |2|ͼ|[Android-Iconics](https://github.com/mikepenz/Android-Iconics)|| ### 5.4 ԴĿ |||ƺ͵ַ|ע| |:-:|:-:|:-:|:-:| |1|ʼ|[EverMemo](https://github.com/daimajia/EverMemo)|| |2||[MVVM-JueJin](https://github.com/fashare2015/MVVM-JueJin)|| |3|ֽɻ|[PaperPlane](https://github.com/TonnyL/PaperPlane)|kotlin| |4|ʫ|[Android-poetry](https://github.com/VinsonGuo/android-poetry)|ͻ+| |5|΢Yue|[΢Yue](https://github.com/LiangLuDev/WeYueReader)|| |6||[SimplifyReader](https://github.com/chentao0707/SimplifyReader)|| |7||[MinimalistWeather](https://github.com/BaronZ88/MinimalistWeather)|| |8|༭|[Turbo Editor](https://github.com/vmihalachi/turbo-editor)|| |9|ʼ+TODO|[rgzly-android](https://github.com/orgzly/orgzly-android)|| |10|Markdown༭|[MarkdownEditors](https://github.com/qinci/MarkdownEditors)|| |11|߷΢|[wechat](https://github.com/motianhuo/wechat)|| |12|΢СƵ+|[VCameraDemo](https://github.com/motianhuo/VCameraDemo)|΢СƵ+,FFmpegװ| |13||[DarkCalculator](https://github.com/HK-SHAO/DarkCalculator)|һָ֧ͽ̵ⷽȹܵļ| |14||[ncalc](https://github.com/tranleduy2000/ncalc)|| |15||[LeafPic](https://github.com/HoraApps/LeafPic)|| |16||[Eyepetizer-in-Kotlin](https://github.com/LRH1993/Eyepetizer-in-Kotlin)|| |17|ʼ|[simplenote](https://github.com/Automattic/simplenote-android)|| |19|ļ|[aFileChooser](https://github.com/iPaulPro/aFileChooser)|| |20||[CloudReader](https://github.com/youlookwhat/CloudReader)|UIʹGankIoapi| |21|MaterialDesignʾ|[LollipopShowcase](https://github.com/mikepenz/LollipopShowcase)|MaterialDesignʾ| |22||[Weather](https://github.com/Mixiaoxiao/Weather)|| |23|ֲ|[Shuttle](https://github.com/timusus/Shuttle)|| |24|ļ|[AnExplorer](https://github.com/1hakr/AnExplorer)|| |25||[Travel-Mate](https://github.com/project-travel-mate/Travel-Mate)|| |26| |[SuperMvp](https://github.com/liuyanggithub/SuperMvp)|| |27|MaterialDesignʾ|[MaterialDesignDemo](https://github.com/Eajy/MaterialDesignDemo)|| |28|ֲ|[MusicX-music-player](https://github.com/RajneeshSingh007/MusicX-music-player)|| |29|ȫֱ|[KingTV](https://github.com/jenly1314/KingTV)|| |30||[KotlinMvp](https://github.com/git-xuhao/KotlinMvp)|| |31||[Eyepetizer](https://github.com/kaikaixue/Eyepetizer)|| |32|ʼ|[Notes](https://github.com/lguipeng/Notes)|| |33|СױǩԴ|[Notes](https://github.com/MiCode/Notes)|| |34|MIUIļԴ|[FileExplorer](https://github.com/MiCode/FileExplorer)|| |35| "" С˵Ķ|[BookReader](https://github.com/JustWayward/BookReader)|| |36|ֲ|[Phonograph](https://github.com/kabouzeid/Phonograph)|| |37|ļ|[AmazeFileManager](https://github.com/TeamAmaze/AmazeFileManager)|| |38|Ķ|[AndroidFire](https://github.com/jaydenxiao2016/AndroidFire)|| |39|ֲ|[MusicDNA](https://github.com/harjot-oberai/MusicDNA)|| |40|Ķ|[LookLook](https://github.com/xinghongfei/LookLook)|| |41|Ķ|[LookLook](https://github.com/xinghongfei/LookLook)|| |42|BiliBili|[bilibili-android-client](https://github.com/HotBitmapGG/bilibili-android-client)|| |43||[Douya](https://github.com/DreaminginCodeZH/Douya)|| |44|+ͼƬ|[Meizhi](https://github.com/drakeet/Meizhi)|| |45||[rebase](https://github.com/drakeet/rebase-android)|| |46|ֲ|[Music-Player](https://github.com/andremion/Music-Player)|| |47|ʼ|[Omni-Notes](https://github.com/federicoiosue/Omni-Notes)|| |48|͹|[NewPipe](https://github.com/TeamNewPipe/NewPipe)|| |49|ֲ|[UniversalMusicPlayer](https://github.com/googlesamples/android-UniversalMusicPlayer)|| |50|LeetCode|[LeeCo](https://github.com/Nightonke/LeeCo)|| |51||[AnimeTaste](https://github.com/daimajia/AnimeTaste)|| |52|ֲ|[Timber](https://github.com/naman14/Timber)|| |53|ý|[kotlin-life](https://github.com/Cuieney/kotlin-life)|| |54||[Minimal-Todo](https://github.com/avjinder/Minimal-Todo)|| |55|ļ|[MLManager](https://github.com/javiersantos/MLManager)|| |56|QQ|[QQ](https://github.com/HuTianQi/QQ)|| ================================================ FILE: 开发工具/ADB_常见的ADB指令总结.md ================================================ ================================================ FILE: 开发工具/Gradle_常见的指令和配置总结.md ================================================ # 常见的 Gralde 配置和指令总结 ## 1、依赖相关 ### 1.1 transitive = true ```groovy dependencies { implementation ("com.github.bumptech.glide:glide:4.8.0@aar") { transitive = true } } ``` 在后面加上 `@aar`,意指你只是下载该 `aar` 包,而并不下载该 `aar` 包所依赖的其他库,那如果想在使用 `@aar` 的前提下还能下载其依赖库,则需要添加 `transitive=true` 的条件。该属性的默认值是 false,表示你所添加的库的所依赖的其他库会被 Gradle 自动下载。 ### 1.2 强制设置某个模块的版本 ```groovy configurations.all { resolutionStrategy { force'org.hamcrest:hamcrest-core:1.3' } } ``` ### 1.3 强制排除某个依赖 ```groovy configurations.all { exclude module: 'okhttp-ws' } ``` ### 1.3 输出模块的依赖树 按照下面的方式,这里的 `commons` 是模块名: ```groovy gradlew :commons:dependencies ``` 或者点击 AS 右侧的 `Gradle` 选择 `:commons` -> `Tasks` -> `android` -> `:commons:androidDependencies` ================================================ FILE: 开发工具/Keytool_常用的指令.md ================================================ # Keytool 常用的指令合集 ### 获取 APK 的签名信息 方式 1:在命令行中输入下面的命令,即可获取 SHA1 签名信息: keytool -printcert -file C:\META-INF\CERT.RSA 这里的 `C:\META-INF\CERT.RSA` 是从 APK 包中解压出来的 `RSA` 文件的路径。 方式 2:或者使用下面的命令也可以达到相同的目的,并且不用对 APK 进行解压: keytool -printcert -jarfile example-release-v2.apk ### 显示签名文件的信息 在命令行输入下面的指令来显示签名文件的信息,这里的 `palm.jks` 是签名文件: keytool -list -v -keystore palm.jks 指令执行的结果是: ![显示签名文件信息](res/keytool_list.jpg) ### 修改签名文件的密码 在命令行输入下面的指令来修改签名文件的密码,这里的 `palm.jks` 是签名文件: keytool -storepasswd -keystore palm.jks 指令的执行结构如下,我们输入旧密码,然后两次输入并确认新密码之后,密码即修改成功: ![修改签名文件的密码](res/keytool_password.jpg) ### 修改签名文件的别名 在命令行输入下面的指令来修改签名文件的别名,这里的 `palm.jks` 是签名文件,`key0` 是之前的别名,`alias0` 是要修改成的别名: keytool -changealias -keystore palm.jks -alias key0 -destalias alias0 指令执行的结果如下,输入签名文件的密码之后,我们就成功地修改了别名: ![修改签名文件的别名](res/keytool_alias_name.jpg) 然后,我们使用下面的指令再来查看一下修改之后的签名文件的信息,以确认别名修改成功。如下图所示,别名已经被我们成功地修改成了 `alias0`: ![确认修改别名成功](res/keytool_list_after.jpg) ### 修改签名文件的别名的密码 在命令行输入下面的指令来修改签名文件的别名的密码,这里的 `palm.jks` 是签名文件,`alias0` 是签名文件的别名: keytool -keypasswd -keystore palm.jks -alias alias0 指令执行的结果如下所示,当我们输入完了签名文件的密码和原来的别名密码之后,输入两遍新的别名的密码即可: ![修改签名文件的别名的密码](res/keytool_alias_psd.jpg) ================================================ FILE: 异步编程/Android多线程编程:IntentService和HandlerThread.md ================================================ # Android 多线程编程:IntentService & HandlerThread 因为 Android 是使用 Java 开发的,所以当我们谈及 Android 中的多线程,必然绕不过 Java 中的多线程编程。但在这篇文章中,我们不会过多地分析 Java 中的多线程编程的知识。我们会在以后分析 Java 并发编程的时候分析 Java 中的多线程、线程池和并发 API 的用法。 我们先来总结一下 Android 多线程编程的演变过程:首先是 Java 的 Thread。因为本身在创建一个线程和销毁一个线程的时候会有一定的开销,当我们任务的执行时间相比于这个开销很小的时候,单独创建一个线程就显得不划算。所以,当程序中存在大量的、小的任务的时候,建议使用线程池来进行管理。但我们一般也很少主动去创建线程池,这是因为——也许是考虑到开发者自己去维护一个线程池比较复杂—— Android 中已经为我们设计了 AsyncTask。AsyncTask 内部封装了一个线程池,我们可以使用它来执行耗时比较短的任务。但 AsyncTask 也有一些缺点:1).如果你的程序中存在很多的不同的任务的时候,你可能要为每个任务定义一个 AsyncTask 的子类。2).从异步线程切换到主线程的方式不如 RxJava 简洁。所以,在实际开发的过程中,我通常使用 RxJava 来实现异步的编程。尤其是局部的优化、不值得专门定义一个 AsyncTask 类的时候,RxJava 用起来更加舒服。 上面的多线程创建的只是普通的线程,对系统来说,优先级比较低。在 Android 中还提供了 IntentService 来执行优先级相对较高的任务。启动一个 IntentService 任务的时候会将任务添加到其内部的、异步的消息队列中执行。此外,IntentService 又继承自 Service,所以这让它具有异步和较高的优先级两个优势。 在之前的文章中,我们已经分析过 AsyncTask、RxJava 以及用来实现线程切换的 Handler. 这里奉上这些文章的链接: - [《Android AsyncTask 源码分析》](https://juejin.im/post/5b65c71af265da0f9402ca4a) - [《RxJava2 系列 (1):一篇的比较全面的 RxJava2 方法总结》](https://juejin.im/post/5b72f76551882561354462dd) - [《RxJava2 系列 (2):背压和Flowable》](https://juejin.im/post/5b759b9cf265da283719d187) - [《RxJava2 系列 (3):使用 Subject》](https://juejin.im/post/5b801dfa51882542cb409905) - [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb) 你可以通过以上的文章来了解这部分的内容。在本篇文章中,我们主要来梳理下另外两个多线程相关的 API,HandlerThread 和 IntentService。 ## 1、异步消息队列:HandlerThread 如果你之前还没有了解过 Handler 的实现的话,那么最好通过我们上面的那篇文章 [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb) 了解一下。因为 HandlerThread 就是通过封装一个 Looper 来实现的。 ### 1.1 HandlerThread 的使用 HandlerThread 继承自线程类 Thread,内部又维护了一个 Looper,Looper 内又维护了一个消息队列。所以,我们可以使用 HandlerThread 来创建一个异步的线程,然后不断向该线程发送任务。这些任务会被封装成消息放进 HandlerThread 的消息队列中被执行。所以,我们可以用 HandlerThread 来创建异步的消息队列。 在使用 HandlerThread 的时候有两个需要注意的地方: 1. 因为 HandlerThread 内部的 Looper 的初始化和开启循环的过程都在 `run()` 方法中执行,所以,在使用 HandlerThread 之前,你必须调用它的 start() 方法。 2. 因为 HandlerThread 的 `run()` 方法使用 Looper 开启一个了无限循环,所以,当不再使用它的时候,应该调用它的 `quitSafely()` 或 `quit()` 方法来结束该循环。 在使用 HandlerThread 的时候只需要创建一个它的实例,然后使用它的 Looper 来创建 Handler 实例,并通过该 Handler 发送消息来将任务添加到队列中。下面是一个使用示例: myHandlerThread = new HandlerThread("MyHandlerThread"); myHandlerThread.start(); handler = new Handler( myHandlerThread.getLooper() ){ @Override public void handleMessage(Message msg) { // ... do something } }; handler.sendEmptyMessage(1); 这里我们创建了 HandlerThread 实例之后用它来创建 Handler 然后通过 Handler 把任务加入到消息队列中进行执行。 显然,使用 HandlerThread 可以很轻松地实现一个消息队列。你只需要在创建了 Handler 之后向它发送消息,然后所有的任务将被加入到队列中执行。当然,它也有缺点。因为所有的任务将会被按顺序执行,所以一旦队列中有某个任务执行时间过长,那么就会导致后续的任务都会被延迟处理。 ### 1.2 HandlerThread 源码解析 下面是该 API 的源码,实现也比较简单,我们直接通过注释来对主要部分进行说明: public class HandlerThread extends Thread { int mPriority; int mTid = -1; Looper mLooper; private @Nullable Handler mHandler; public HandlerThread(String name) { super(name); mPriority = Process.THREAD_PRIORITY_DEFAULT; } protected void onLooperPrepared() { } // 在这个方法开启了 Looper 循环,因为是一个无限循环,所以不适用的时候应该将其停止 @Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; } // 获取该 HandlerThread 对应的 Looper public Looper getLooper() { if (!isAlive()) { return null; } synchronized (this) { while (isAlive() && mLooper == null) { try { wait(); } catch (InterruptedException e) { } } } return mLooper; } public boolean quit() { Looper looper = getLooper(); if (looper != null) { looper.quit(); return true; } return false; } public boolean quitSafely() { Looper looper = getLooper(); if (looper != null) { looper.quitSafely(); return true; } return false; } // ... 无关代码 } 上面的代码比较简单明了,在 run() 方法中初始化 Looper 并执行。如果 Looper 还没有被创建,那么当调用 `getLooper()` 方法获取 Looper 的时候会让线程阻塞。当 Looper 创建完毕之后会唤醒所有阻塞的线程继续执行。另外,就是两个停止 Looper 的方法。它们基本上就是对 Looper 进行了一层封装。 ## 2、IntentService ### 2.1 使用 IntentService IntentService 继承自 Serivce,因此它比普通的多线程任务优先级要高。这使得它相比于普通的异步任务不容易被系统 kill 掉。它内部也是通过一个 Looper 来实现的,所以也是一种消息队列。在研究它的源码之前,我们先来看一下它的使用。 IntentService 的使用是比较简单的,只需要:1).继承它并实现其中的 `onHandleIntent()` 方法;2). 将 IntentService 注册到 manifest 中;3). 像开启一个普通的服务那样开启一个 IntentService 即可。下面是该类的一个使用示例: public class FileRecognizeTask extends IntentService { public static void start(Context context) { Intent intent = new Intent(context, FileRecognizeTask.class); context.startService(intent); } public FileRecognizeTask() { super("FileRecognizeTask"); } @Override protected void onHandleIntent(@androidx.annotation.Nullable @Nullable Intent intent) { // 你的需要异步执行的业务逻辑 } } OK,介绍完了 IntentService 的使用,我们再来分析一下它的源码。 ### 2.2 IntentService 源码分析 实现 IntentService 的时候使用到了我们上面分析过的 HandlerThread. 首先,在 `onCreate()` 回调方法中创建了一个 HandlerThread,然后使用它的 Looper 创建了一个 ServiceHandler: HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); ServiceHandler 是 IntentSerice 的内部类,其定义如下: private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); stopSelf(msg.arg1); } } ServiceHandler 用来执行被添加到队列中的消息。它会回调 IntentService 中的 `onHandleIntent()` 方法,也就是我们实现业务逻辑的方法。当消息执行完毕之后,会调用 Service 的 `stopSelf(int)` 方法来尝试停止服务。注意这里调用的是 `stopSelf(int)` 而不是 `stopSelf()`。它们之间的区别是,当还存在没有完成的任务的时候 `stopSelf(int)` 不会立即停止服务,而 `stopSelf()` 方法会立即停止服务。 IntentSerivce 的 `onCreate()` 方法会在第一次启动的时候被调用,来创建服务。而 `onStartCommond()` 方法会在每次启动的时候被调用。下面是该方法的定义。 @Override public void onStart(@Nullable Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); } @Override public int onStartCommand(@Nullable Intent intent, int flags, int startId) { onStart(intent, startId); return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY; } `onStartCommand()` 内部调用了 `onStart()` 来处理请求。在 `onStart()` 方法中会通过 mServiceHandler 创建一个消息,并将该消息发送给 mServiceHandler. 该消息会在被 mServiceHandler 放进消息队列中排队,并在合适的时机被执行。 因此,我们可以总结一下 IntentService 的工作过程:首先,当我们第一次启动 IntentService 的时候会初始化一个 Looper 和 Handler;然后调用它的 onStartCommond() 方法,把请求包装成消息之后发送到消息队列中等待执行;当消息被 Handler 处理的时候会回调 IntentService 的 `onHandleIntent()` 方法来执行。此时,如果又有一个任务需要执行,那么 IntentService 的 onStartCommond() 方法会再次被执行并把请求封装之后放入队列中。当队列中的所有的消息都执行完毕,并且没有新加入的请求,那么此时服务就会自动停止,否则服务还会继续在后台执行。 这里,同样也应该注意下,IntentService 中的任务是按照被添加的顺序来执行的。 ## 总结 以上就是我们对 IntentService 和 HandlerThread 的分析。它们都是使用了 Handler 来实现,所以搞懂它们的前提是搞懂 Handler。关于 Handler,还是推荐一下笔者的另一篇文章 [《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)。 ------ **如果您喜欢我的文章,可以在以下平台关注我:** - 个人主页:[https://shouheng88.github.io/](https://shouheng88.github.io/) - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) ================================================ FILE: 异步编程/AsyncTask的使用和源码分析.md ================================================ # AsyncTask 的使用和源码分析 ## 1、AsyncTask的使用 使用 `AsyncTask` 可以更加简单地实现任务的异步执行,以及任务执行完毕之后与主线程的交互。它被设计用来执行耗时比较短的任务,通常是几秒种的那种,如果要执行耗时比较长的任务,那么就应该使用 **JUC** 包中的框架,比如 `ThreadPoolExecutor` 和 `FutureTask`等。 AsyncTask 用来在后台线程中执行任务,当任务执行完毕之后将结果发送到主线程当中。它有三个重要的泛类型参数,分别是 `Params`、`Progress` 和 `Result`,分别用来指定参数、进度和结果的值的类型。 以及四个重要的方法,分别是 `onPreExecute()`, `doInBackground()`, `onProgressUpdate()` 和 `onPostExecute()`。 这四个方法中,除了 `doInBackground()`,其他三个都是运行在UI线程的,分别用来处理在任务开始之前、任务进度改变的时候以及任务执行完毕之后的逻辑,而 `doInBackground()` 运行在后台线程中,用来执行耗时的任务。 一种典型的使用方法如下: ```java private class DownloadFilesTask extends AsyncTask { @Override protected Long doInBackground(URL... urls) { int count = urls.length; long totalSize = 0; for (int i = 0; i < count; i++) { totalSize += Downloader.downloadFile(urls[i]); publishProgress((int) ((i / (float) count) * 100)); if (isCancelled()) break; } return totalSize; } @Override protected void onProgressUpdate(Integer... progress) { setProgressPercent(progress[0]); } @Override protected void onPostExecute(Long result) { showDialog("Downloaded " + result + " bytes"); } } ``` 上面说 `AsyncTask` 有4个重要的方法,这里我们覆写了3个。`doInBackground()` 运行在线程当中,耗时的任务可以放在这里进行;`onProgressUpdate()` 用来处理当任务的进度发生变化时候的逻辑;`onPostExecute()` 用来处理当任务执行完毕之后的逻辑。另外,这里我们还用到了 `publishProgress()` 和 `isCancelled()` 两个方法,分别用来发布任务进度和判断任务是否被取消。 然后,我们可以用下面的方式来使用它: ```java new DownloadFilesTask().execute(url1, url2, url3); ``` 使用AsyncTask的时候要注意以下几点内容: 1. AsyncTask 的类必须在主线程中进行加载,当在4.1之后这个过程会自动进行; 2. AsyncTask 的对象必须在主线程中创建; 3. `execute()` 方法必须在UI线程中被调用; 4. 不要直接调用 `onPreExecute()`, `doInBackground()`, `onProgressUpdate()` 和 `onPostExecute()`; 5. 一个AsyncTask对象的 `execute()` 方法只能被调用一次; Android 1.6 之前,AsyncTask 是**串行**执行任务的;1.6 采用线程池处理**并行**任务;从 3.0 开始,又采用一个线程来**串行**执行任务。 3.0 之后可以用 `executeOnExecutor()` 来并行地执行任务,如果我们希望在3.0之后能并行地执行上面的任务,那么我们应该这样去写: ```java new DownloadFilesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3); ``` 这里的 `AsyncTask.THREAD_POOL_EXECUTOR` 是 AsyncTask 内部定义的一个线程池,我们可以使用它来将 AsyncTask 设置成并行的。 ## 2、AsyncTask源码分析 ### 2.1 AsyncTask 的初始化过程 当初始化一个 AsyncTask 的时候,所有的重载构造方法都会调用下面的这个构造方法。这里做了几件事情: 1. 初始化一个 Handler 对象 mHandler,该 Handler 用来将消息发送到它所在的线程中,通常使用默认的值,即主线程的 Handler; 2. 初始化一个 WorkerRunnable 对象 mWorker。它是一个 `WorkerRunnable` 类型的实例,而 `WorkerRunnable` 又继承自 `Callable`,因此它是一个可以被执行的对象。我们会把在该对象中回调 `doInBackground()` 来将我们的业务逻辑放在线程池中执行。 3. 初始化一个 FutureTask 对象 mFuture。该对象包装了 `mWorker` 并且当 `mWorker` 执行完毕之后会调用它的 `postResultIfNotInvoked()` 方法来通知主线程(不论任务已经执行完毕还是被取消了,都会调用这个方法)。 ```java public AsyncTask(@Nullable Looper callbackLooper) { // 1. 初始化用来发送消息的 Handler mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper() ? getMainHandler() : new Handler(callbackLooper); // 2. 封装一个对象用来执行我们的任务 mWorker = new WorkerRunnable() { public Result call() throws Exception { mTaskInvoked.set(true); Result result = null; try { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 回调我们的业务逻辑 result = doInBackground(mParams); Binder.flushPendingCommands(); } catch (Throwable tr) { mCancelled.set(true); throw tr; } finally { // 发送结果给主线程 postResult(result); } return result; } }; // 3. 初始化一个 FutureTask,并且当它执行完毕的时候,会调用 postResultIfNotInvoked 来将消息的执行结果发送到主线程中 mFuture = new FutureTask(mWorker) { @Override protected void done() { try { // 如果任务没有被触发,也要发送一个结果 postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occurred while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); } } }; } ``` 当这样设置完毕之后,我们就可以使用 `execute()` 方法来开始执行任务了。 ### 2.2 AsyncTask 中任务的串行执行过程 我们从 `execute()` 方法开始分析 AsyncTask, ```java @MainThread public final AsyncTask execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); } @MainThread public final AsyncTask executeOnExecutor(Executor exec, Params... params) { if (mStatus != Status.PENDING) { // 1.判断线程当前的状态 switch (mStatus) { case RUNNING: throw new IllegalStateException(...); case FINISHED: throw new IllegalStateException(...); } } mStatus = Status.RUNNING; onPreExecute(); // 2.回调生命周期方法 mWorker.mParams = params; // 3.赋值给可执行的对象 WorkerRunnable exec.execute(mFuture); // 4.在线程池中执行任务 return this; } ``` 当我们调用 AsyncTask 的 `execute()` 方法的时候会立即调用它的 `executeOnExecutor()` 方法。这里传入了两个参数,分别是一个 `Executor` 和任务的参数 `params`。从上面我们可以看出,当直接调用 execute() 方法的时候会使用默认的线程池 `sDefaultExecutor`,而当我们指定了线程池之后,会使用我们指定的线程池来执行任务。 在 1 处,会对 AsyncTask 当前的状态进行判断,这就对应了前面说的,一个任务只能被执行一次。在 2 处会调用 `onPreExecute()` 方法,如果我们覆写了该方法,那么它就会在这个时候被调用。在 3 处的操作是在为 `mWorker` 赋值,即把调用 `execute` 方法时传入的参数赋值给了 `mWorker`。接下来,会将 `mFuture` 添加到线程池中执行。 当我们不指定任何线程池的时候使用的 `sDefaultExecutor` 是一个串行的线程池,它的定义如下: ```java public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; private static class SerialExecutor implements Executor { final ArrayDeque mTasks = new ArrayDeque(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() { public void run() { try { // 相当于对传入的Runnable进行了一层包装 r.run(); } finally { // 分配下一个任务 scheduleNext(); } } }); // 如果当前没有正在执行的任务,那么就尝试从队列中取出并执行 if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { // 从队列中取任务并使用THREAD_POOL_EXECUTOR执行 if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } } ``` 从上面我们可以看出,我们添加到线程池中的任务实际上并没有直接交给线程池来执行,而是对其进行了处理之后才执行的,SerialExecutor 通过内部维护了双端队列,每当一个 AsyncTask 调用 `execute()` 方法的时候都会被放在该队列当中进行排队。如果当前没有正在执行的任务,那么就从队列中取一个任务交给 `THREAD_POOL_EXECUTOR` 执行;当一个任务执行完毕之后又会调用 `scheduleNext()` 取下一个任务执行。也就是说,实际上 `sDefaultExecutor` 在这里只是起了一个任务调度的作用,任务最终还是交给 `THREAD_POOL_EXECUTOR` 执行的。 这里的`THREAD_POOL_EXECUTOR`也是一个线程池,它在静态代码块中被初始化: ```java static { // 使用指定的参数创建一个线程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); threadPoolExecutor.allowCoreThreadTimeOut(true); THREAD_POOL_EXECUTOR = threadPoolExecutor; } ``` 我们也可以直接将这个静态的线程池作为我们任务执行的线程池而不是放在上面的队列中被串行地执行。 ### 2.3 将任务执行的结果发送到其他线程 上面的 `WorkerRunnable` 中已经用到了 `postResult` 方法,它用来将任务执行的结果发送给 `Handler`: ```java private Result postResult(Result result) { @SuppressWarnings("unchecked") Message message = mHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult(this, result)); message.sendToTarget(); return result; } ``` `mHandler` 会在创建 AsyncTask 的时候初始化。我们可以通过 AsyncTask 的构造方法传入 Handler 和 Looper 来指定该对象所在的线程。当我们没有指定的时候,会使用 AsyncTask 内部的 `InternalHandler` 创建 `Handler`: ```java private final Handler mHandler; public AsyncTask(@Nullable Looper callbackLooper) { // 根据传入的参数创建Handler对象 mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper() ? getMainHandler() : new Handler(callbackLooper); } private static Handler getMainHandler() { synchronized (AsyncTask.class) { if (sHandler == null) { // 使用 InternalHandler 创建对象 sHandler = new InternalHandler(Looper.getMainLooper()); } return sHandler; } } // AsyncTask 内部定义 的Handler 类型 private static class InternalHandler extends Handler { public InternalHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; // 根据传入的消息类型进行处理 switch (msg.what) { case MESSAGE_POST_RESULT: result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } } ``` ## 3、总结 上面我们梳理了 AsyncTask 的大致过程,我们来梳理下: 每当我们实例化一个 AsyncTask 的时候都会在内部封装成一个 Runnable 对象,该对象可以直接放在线程池中执行。这里存在两个线程池,一个是 SerialExecutor 一个是 THREAD_POOL_EXECUTOR,前者主要用来进行任务调度,即把交给线程的任务放在队列中进行排队执行,而时机上所有的任务都是在后者中执行完成的。这个两个线程池都是静态的字段,所以它们对应于整个类的。也就是说,当使用默认的线程池的时候,实例化的 AsyncTask 会一个个地,按照加入到队列中的顺序依次执行。 当任务执行完毕之后,使用 Handler 来将消息发送到主线程即可,这部分的逻辑主要与 Handler 机制相关,可以通过这篇文章来了解:[《Android 消息机制:Handler、MessageQueue 和 Looper》](https://juejin.im/post/5bdec872e51d4551ee2761cb)。 以上就是 AsyncTask 的主要内容。 ------ **如果您喜欢我的文章,可以在以下平台关注我:** - 个人主页:[https://shouheng88.github.io/](https://shouheng88.github.io/) - 掘金:[https://juejin.im/user/585555e11b69e6006c907a2a](https://juejin.im/user/585555e11b69e6006c907a2a) - Github:[https://github.com/Shouheng88](https://github.com/Shouheng88) - CSDN:[https://blog.csdn.net/github_35186068](https://blog.csdn.net/github_35186068) - 微博:[https://weibo.com/u/5401152113](https://weibo.com/u/5401152113) ================================================ FILE: 性能优化/Android性能优化-ANR.md ================================================ # Android 性能优化 - ANR 的原因和解决方案 ## 1、出现 ANR 的情况 满足下面的一种情况系统就会弹出 ANR 提示 1. 输入事件(按键和触摸事件) 5s 内没被处理; 2. BroadcastReceiver 的事件 ( `onRecieve()` 方法) 在规定时间内没处理完 (前台广播为 10s,后台广播为 60s); 3. Service 前台 20s 后台 200s 未完成启动; 4. ContentProvider 的 `publish()` 在 10s 内没进行完。 通常情况下就是主线程被阻塞造成的。 ## 2、ANR 的实现原理 以输入无响应的过程为例(基于 9.0 代码): 最终弹出 ANR 对话框的位置是与 AMS 同目录的类 `AppErrors` 的 `handleShowAnrUi()` 方法。这个类用来处理程序中出现的各种错误,不只 ANR,强行 Crash 也在这个类中处理。 ```java // base/core/java/com/android/server/am/AppErrors.java void handleShowAnrUi(Message msg) { Dialog dialogToShow = null; synchronized (mService) { AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj; // ... Intent intent = new Intent("android.intent.action.ANR"); if (!mService.mProcessesReady) { intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); } mService.broadcastIntentLocked(null, null, intent, null, null, 0, null, null, null, AppOpsManager.OP_NONE, null, false, false, MY_PID, Process.SYSTEM_UID, 0 /* TODO: Verify */); boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0; if (mService.canShowErrorDialogs() || showBackground) { dialogToShow = new AppNotRespondingDialog(mService, mContext, data); proc.anrDialog = dialogToShow; } else { MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR, AppNotRespondingDialog.CANT_SHOW); // Just kill the app if there is no dialog to be shown. mService.killAppAtUsersRequest(proc, null); } } // If we've created a crash dialog, show it without the lock held if (dialogToShow != null) { dialogToShow.show(); } } ``` 不过从发生 ANR 的地方调用到这里要经过很多的类和方法。最初抛出 ANR 是在 `InputDispatcher.cpp` 中。我们可以通过其中定义的常量来寻找最初触发的位置: ```C++ // native/services/inputflinger/InputDispatcher.cpp constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; // 5 sec ``` 从这个类触发的位置会经过层层传递达到 `InputManagerService` 中 ```java // base/services/core/java/com/android/server/input/InputManagerService.java private long notifyANR(InputApplicationHandle inputApplicationHandle, InputWindowHandle inputWindowHandle, String reason) { return mWindowManagerCallbacks.notifyANR( inputApplicationHandle, inputWindowHandle, reason); } ``` 这里的 `mWindowManagerCallbacks` 就是 `InputMonitor` : ```java // base/services/core/java/com/android/server/wm/InputMonitor.java public long notifyANR(InputApplicationHandle inputApplicationHandle, InputWindowHandle inputWindowHandle, String reason) { // ... 略 if (appWindowToken != null && appWindowToken.appToken != null) { final AppWindowContainerController controller = appWindowToken.getController(); final boolean abort = controller != null && controller.keyDispatchingTimedOut(reason, (windowState != null) ? windowState.mSession.mPid : -1); if (!abort) { return appWindowToken.mInputDispatchingTimeoutNanos; } } else if (windowState != null) { try { // 使用 AMS 的方法 long timeout = ActivityManager.getService().inputDispatchingTimedOut( windowState.mSession.mPid, aboveSystem, reason); if (timeout >= 0) { return timeout * 1000000L; // nanoseconds } } catch (RemoteException ex) { } } return 0; // abort dispatching } ``` 然后回在上述方法调用 AMS 的 `inputDispatchingTimedOut()` 方法继续处理,并最终在 `inputDispatchingTimedOut()` 方法中将事件传递给 `AppErrors` ```java // base/services/core/java/com/android/server/am/ActivityManagerService.java public boolean inputDispatchingTimedOut(final ProcessRecord proc, final ActivityRecord activity, final ActivityRecord parent, final boolean aboveSystem, String reason) { // ... if (proc != null) { synchronized (this) { if (proc.debugging) { return false; } if (proc.instr != null) { Bundle info = new Bundle(); info.putString("shortMsg", "keyDispatchingTimedOut"); info.putString("longMsg", annotation); finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info); return true; } } mHandler.post(new Runnable() { @Override public void run() { // 使用 AppErrors 继续处理 mAppErrors.appNotResponding(proc, activity, parent, aboveSystem, annotation); } }); } return true; } ``` 当事件传递到了 `AppErrors` 之后,它会借助 Handler 处理消息也就调用了最初的那个方法并弹出对话框。 参考:[《Android ANR原理分析》](https://www.cnblogs.com/android-blogs/p/5718302.html) ## 3、ANR 的解决办法 上面分析了 ANR 的成因和原理,下面我们分析下如何解决 ANR. ### 1. 使用 adb 导出 ANR 日志并进行分析 发生 ANR的时候系统会记录 ANR 的信息并将其存储到 `/data/anr/traces.txt` 文件中(在比较新的系统中会被存储都 `/data/anr/anr_*` 文件中)。我们可以使用下面的方式来将其导出到电脑中以便对 ANR 产生的原因进行分析: adb root adb shell ls /data/anr adb pull /data/anr/ *在笔者分析 ANR 的时候使用上述指令尝试导出 ANR 日志的时候都出现了 Permission Denied。此时,你可以将手机 Root 之后导出,或者尝试修改文件的读写权限,或者在开发者模式中选择将日志导出到 sdcard 之后再从 sdcard 将日志发送到电脑端进行查看* ### 2. 使用 DDMS 的 traceview 进行分析 在 AS 中打开 DDMS,或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。 TraceView 工具的使用可以参考这篇文章:[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) 这种定位 ANR 的思路是:**使用 TraceView 来通过耗时方法调用的信息定位耗时操作的位置**。 资料: - [《ANR 官方文档》](https://developer.android.com/topic/performance/vitals/anr) - [《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) ### 3. 使用开源项目 ANR-WatchDog 来检測 ANR 项目地址是 [Github-ANR-WatchDog](https://github.com/SalomonBrys/ANR-WatchDog) 该项目的实现原理:创建一个检测线程,该线程不断往 UI 线程 post 一个任务,然后睡眠固定时间,等该线程又一次起来后检測之前 post 的任务是否运行了,假设任务未被运行,则生成 ANRError,并终止进程。 ### 4. 常见的 ANR 场景 1. I/O 阻塞 2. 网络阻塞 3. 多线程死锁 4. 由于响应式编程等导致的方法死循环 5. 由于某个业务逻辑执行的时间太长 ### 5. 避免 ANR 的方法 1. UI 线程尽量只做跟 UI 相关的工作; 2. 耗时的工作 (比如数据库操作,I/O,网络操作等),采用单独的工作线程处理; 3. 用 Handler 来处理 UI 线程和工作线程的交互; 4. 使用 RxJava 等来处理异步消息。 总之,一个原则就是:**不在主线程做耗时操作**。 ================================================ FILE: 性能优化/Android性能优化-内存优化.md ================================================ # Android 性能优化 - 内存优化 ================================================ FILE: 性能优化/Android性能优化-启动优化.md ================================================ # Android 性能优化 - 启动优化 ## 1、基础 ### 1.1 启动的类型 首先是启动的三种类型: 1. **冷启动场景**:后台完全没有任何进程的情况下,启动最慢; 2. **温启动场景**:按返回键退回主界面再从主界面打开的情形,较快; 3. **热启动场景**:按 Home 键退回到主界面再从主界面打开的情形,最快。 应用启动的过程实际上也就是 Activity 启动的流程,所以具体涉及的源码不是我们这里的重点,你可以查找 Activity 启动流程相关的文章来了解源码。 其实优化应用的启动速度无非也就是在那几个生命周期方法中进行优化,不做太多耗时操作等:Application 的生命周期和 Activity 的生命周期。 ### 1.2 启动速度的测量 当然,我们而已通过自己的感觉判断启动的快慢,但量化还是非常重要的,不然你都无法向 PM 交差不是。所以,我们有必要了解下 Android 中的启动速度是如何测量的。 #### 方式 1:使用 ADB 获取启动速度的第一种方式是使用 ADB,使用下面的指令的时候在启动应用的时候会使用 AMS 进行统计。但是缺点是统计时间不够准确: ```shell adb shell am start -n {包名}/{包名}.{活动名} ``` #### 方式 2:代码埋点 在 Application 的 `attachBaseContext()` 方法中记录开始时间,第一个 Activity 的 `onWindowFocusChanged()` 中记录结束时间。缺点是统计不完全,因为在 `attachBaseContext()` 之前还有许多操作。 #### 方式 3:TraceView 在 AS 中打开 DDMS,或者到 SDK 安装目录的 tools 目录下面使用 `monitor.bat` 打开 DDMS。 TraceView 工具的使用可以参考这篇文章:[《Android 性能分析之TraceView使用(应用耗时分析)》](https://blog.csdn.net/android_jianbo/article/details/76608558) 通过 TraceView 主要可以得到两种数据:单次执行耗时的方法以及执行次数多的方法。但 TraceView 性能耗损太大,不能比较正确反映真实情况。 #### 方式 4:Systrace Systrace 能够追踪关键系统调用的耗时情况,如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。 #### 方式 5:Systrace + 插桩 类似于 AOP,通过切面为每个函数统计执行时间。这种方式的好处是能够准确统计各个方法的耗时。 原理就是 ```java public void method() { TraceMethod.i(); // Real work TraceMethod.o(); } ``` #### 方式 6:录屏 录屏方式收集到的时间,更接近于用户的真实体感。可以在录屏之后按帧来进行统计分析。 ## 2、启动优化 ### 2.1 一般解决办法 #### 2.1.1 延迟初始化 一些逻辑,如果没必要在程序启动的时候就立即初始化,那么可以将其推迟到需要的时候再初始化。比如,我们可以使用单例的方式来获取类的实例,然后在获取实例的时候再进行初始化操作。 **但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。可以按照耗时和是否必要将业务划分到四个维度:必要且耗时,必要不耗时,非必要但耗时,非必要不耗时。**然后对应不同的维度来决定是否有必要在程序启动的时候立即初始化。 #### 2.1.2 防止主线程阻塞 一般我们也不会把耗时操作放在主线程里面,毕竟现在有了 RxJava 之后,在程序中使用异步代价并不高。这种耗时操作包括,大量的计算、IO、数据库查询和网络访问等。 另外,关于开启线程池的问题下面的话总结得比较好,除了一般意义上线程池和使用普通线程的区别,还要考虑应用启动这个时刻的特殊性: > 如何开启线程同样也有学问:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService 等都各有利弊;例如通常情况下 ThreadPoolExecutor 比 Thread 更加高效、优势明显,但是特定场景下单个时间点的表现 Thread 会比 ThreadPoolExecutor 好:同样的创建对象,ThreadPoolExecutor 的开销明显比 Thread 大。 > > 来自:https://www.jianshu.com/p/f5514b1a826c #### 2.1.3 布局优化 比如,之前我在使用 Fragment 和 ViewPager 搭配的时候,发现虽然 Fragment 可以被复用,但是如果通过 Adapter 为 ViewPager 的每个项目指定了标题,那么这些标题控件不会被复用。当 ViewPager 的条目比较多的时候,甚至会造成 ANR. 对于这种布局优化相关的东西,可以参考性能优化的 [Android性能优化-布局优化](Android性能优化-布局优化.md) 模块。 #### 2.1.4 使用启动页面防止白屏 这种方法只是治标不治本的方法,就是在应用启动的时候避免白屏,可以通过设置自定义主题来实现。 这种实现方式可以参考我的开源项目 [MarkNote](https://github.com/Shouheng88/MarkNote) 的实现。 ### 2.2 其他借鉴办法 #### 2.2.1 使用 BlockCanary 检测卡顿 BlockCanary 是一个开源项目,类似于 LeakCanary (很多地方也借鉴了 LeakCanary 的东西),主要用来检测程序中的卡顿,项目地址是 [Github-BlockCanary](https://github.com/markzhai/AndroidPerformanceMonitor). 它的原理是对 Looper 中的 `loop()` 方法打处的日志进行处理,通过一个自定义的日志输出 Printer 监听方法执行的开始和结束。可以通过该项目作者的文章来了解这个项目: [BlockCanary — 轻松找出Android App界面卡顿元凶](https://www.jianshu.com/p/cd7fc77405ac) #### 2.2.2 GC 优化 GC 优化的思想就是减少垃圾回收的时间间隔,所以在启动的过程中不要频繁创建对象,特别是大对象,避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现。 #### 2.2.3 类重排 如果我们的代码在打包的时候被放进了不同的 dex 里面,当启动的时候,如果需要用到的类分散在各个 dex 里面,那么系统要花额外的时间到各个 dex 里加载类。因此,我们可以通过类重排调整类在 Dex 中的排列顺序,把启动时用到的类放进主 dex 里。 目前可以使用 [ReDex](https://github.com/facebook/redex) 的 [Interdex](https://github.com/facebook/redex/blob/master/docs/Interdex.md) 调整类在 Dex 中的排列顺序。 可以参考下面这篇文章来了解类重拍在手 Q 中的应用以及他们遇到的各种问题: [Redex 初探与 Interdex:Andorid 冷启动优化](https://mp.weixin.qq.com/s/Bf41Kez_OLZTyty4EondHA?) #### 2.2.4 资源文件重排 对应于类重排,还有资源的重排。可以参考下阿里的资源重排优化方案: [支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能](https://mp.weixin.qq.com/s/79tAFx6zi3JRG-ewoapIVQ) 这种方案的原理时先通过测试找出程序启动过程中需要加载的资源,然后再打包的时候通过修改 7z 压缩工具将上述热点资源放在一起。这样,在系统进行资源加载的时候,这些资源将要用到的资源会一起被加载进程内存当中并缓存,减少了 IO 的次数,同时不需要从磁盘读取文件,来提高应用启动的速度。 #### 2.2.5 类的加载 通过 Hook 来去掉应用启动过程中的 verify 来减少启动过程中的耗时。但是这种方式存在虚拟机兼容的问题,在 ART 虚拟机上面进行 Hook 需要兼容几个版本。 参考资料: - [App startup time](https://developer.android.com/topic/performance/vitals/launch-time) - [Android性能优化(一)之启动加速35%](https://www.jianshu.com/p/f5514b1a826c) - [爱奇艺技术分享:爱奇艺Android客户端启动速度优化实践总结](https://www.jianshu.com/p/bd3930316c8d) ================================================ FILE: 性能优化/Android性能优化-布局优化.md ================================================ # Android 性能优化 - 布局优化 ### 1. 合理选择 ViewGroup 在选择使用 Android 中的布局方式的时候应该遵循:**尽量少使用性能比较低的容器控件,比如 RelativeLayout,但如果使用 RelativeLayout 可以降低布局的层次的时候可以考虑使用**。 Android 中的控件是树状的,降低树的高度可以提升布局性能。RelativeLayout 的布局比 FrameLayout、LinearLayout 等简单,因而可以减少计算过程,提升程序性能。 *注:参见第 9 条,关于 ConstaintLayout 的介绍。* ### 2. 使用 `` 标签复用布局 **多个地方共用的布局可以使用 `` 标签在各个布局中复用** ```xml ``` ### 3. 使用 `` 标签复用父容器 **可以通过使用 `` 来降低布局的层次**。 `` 标签通常与 `` 标签一起使用, `` 作为可以复用的布局的根控件。然后使用 `` 标签引用该布局。 ```xml