java基础
ThreadLocal的理解
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- ThreadLocal的作用是变量的线程隔离,同一个ThreadLocal在不同的线程调用get()将返回不同的value
- 隔离是通过ThreadLocalMap实现的,每个线程都有自己的ThreadLocalMap,ThreadLocalMap中存储了本线程用到的ThreadLocal和value
- ThreadLocalMap本质是一个Entry数组,Entry的key是ThreadLocal的弱引用,value就是ThreadLocal对象set的值。
String StringBuilder StringBuffer
- String:字符串常量,不可变对象,每次对String类进行修改都会生成一个新的String对象
- StringBuilder:字符串变量,非线程安全,性能高,速度快
- StringBuffer:字符串变量,线程安全,因为有同步操作,性能开销,速度慢。
synchronized和lock的区别
- lock是一个接口,synchronized是一个java关键字,synchronized是内置语言实现
- 发生异常时,synchronized会自动释放锁,不会发生死锁现象;lock不会自动释放,如果没有手动释放,会发生死锁。因此使用lock需要在finally代码块中手动释放所
- lock在等待锁时,可以响应中断,synchronized不行
- lock可以知道获取锁有没有成功,synchronized不行
- 性能上,在高并发的场景下,lock的性能要比synchronized高。但jdk1.6对synchronized加入了很多优化措施,性能基本不比lock差。
读写锁ReadWriteLock、ReetrantReadWriteLock
1.Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock读写锁的效率明显高于synchronized关键字
3.ReetrantReadWriteLock读写锁的实现中,读锁使用共享模式;写锁使用独占模式,换句话说,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的
4.ReetrantReadWriteLock读写锁的实现中,需要注意的,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
多线程之间通信 - 生产者消费者模型
- 互斥临界资源
- 如果临界资源饱和,生产者线程阻塞等待,等待消费者消费
- 如果临界资源为空,消费者线程阻塞等待,等待生产者生产
public class Test {
private Resource mResource = new Resource();
private Object mLock = new Object();
// 临界资源
static class Resource {
private Queue<String> queue = new LinkedList();
// 生产内容
public void produce() {
queue.add("product");
}
// 消费内容
public void consume() {
queue.poll();
}
public boolean isFull() {
return queue.size() >= 100;
}
public boolean isEmpty() {
return queue.size() == 0;
}
}
// 生产者
class ThreadA extends Thread {
public ThreadA(String name) {
super(name);
}
@override
public void run() {
while (true) {
sychronized(mLock) {
if(mResource.isFull()) {
mLock.wait();
}
mResource.produce();
mLock.notify();
}
}
}
}
// 消费者
class ThreadB extends Thread {
public ThreadB(String name) {
super(name);
}
@override
public void run() {
while (true) {
sychronized(mLock) {
if(mResource.isEmpty()) {
mLock.wait();
}
mResource.consume();
mLock.notify();
}
}
}
}
}
ArrayList remove 方法原理
- 向前覆盖拷贝
- array[–size] = null
Atomic类如何保证原子性
jdk1.5 atomic包中提供了一些原子类,这些类可以保证,在多线程的情况下,当某个线程执行atomic方法时,不会被其它线程打断。
使用CAS(compare and swap)操作保证原子性,CAS实现了区别与synchronized同步锁的乐观锁,乐观锁每次都会乐观的认为数据不会修改,所以不会上锁,然后更新时会判断在此期间数据有没有发生更新。
优点: CAS在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的
缺点: 如果并发越高,CAS检查失败的次数就越高,如果CAS长时间不成功,会增加CPU的开销,因此CAS不适合竞争十分频繁的场景。
HashMap底层实现原理
- 内部有个哈希桶数组,put()时会先根据key的hashcode计算bucket数组的索引;
- 如果没发生冲突,直接将value放到bucket中;
- 如果发生了冲突,HashMap使用的是链表法来解决哈希冲突,即以链表的形式放到bucket后;
- JDK1.8还做了优化:如果链表长度过长,超过8,链表结构会转换成红黑树结构;
- 如果bucket满了(负载因子),就会resize,将bucket扩容2倍;
HashMap,HashTable,ConcurrentHashMap对比
- HashMap是非线程安全的,HashTable是线程安全的
- HashMap建制允许null,HashTable不允许
- 因为线程同步的问题,HashMap性能要比HashTable高
- ConcurrentHashMap也是线程安全,但性能要比HashTable高,因为HashTable简单使用了synchronized完成线程同步,锁住整个对象,当数据增加到一定程度是,HashTable性能急剧下降。
- ConcurrentHashMap做了一些优化,它将bucket数组划分成了一系列segment,在每个segment上用lock进行保护,比synchronized锁更加精细化,并发性要更好。
- ConcurrentHashMap键值对也不允许null
HashMap成环
如果HashMap发生了扩容,hashmap需要将原先的entry打散,在将entry放到扩容后的buckets数组中,重新构建链表,在高并发场景下entry.next容易相互引用对方,导致链表成环。这样在get时就容易发生死循环。
Boolean占几个字节
java虽然定义了boolean这种数据类型,但是只对它提供了非常有限的支持。在Java虚拟机中没有任何供boolean值专用的字节码指令
对于boolean值,在编译之后都使用JVM的int数据类型来代替
对于boolean数组将会被编码成JVM的byte数组
volatile关键字作用
- 保证变量操作的线程可见性,一个线程修改结果另一个线程能马上看到
- 禁止jvm指令重排,所以在非必要的情况下,不要使用volatile关键字
- 注意:volatile只能确保可见性,无法确保原子性,实现同步还得要sychronize关键字。
Exception和Error
- Exception和Error都是Throwable的子类
- Error一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。
- Exception表示可处理的异常,尽量处理这种异常,使程序恢复运行。Exception分为RuntimeException和CheckedException。RuntimeException不是必须try-catch,CheckedException必须要try-catch
线程池
线程池优点,为什么要用线程池
- 线程是稀缺资源,创建线程对系统来说是一种开销。
- 使用线程池可以减少创建和销毁线程的次数,达到工作线程重复使用的目的。
参数含义:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolsize:线程池核心线程数,
- maximumPoolSize:线程池最大线程数
- keepAliveTime:非核心线程,空闲存活时间
- unit:时间单位
- workQueue:存放任务的阻塞队列
- threadFactory:创建线程的工厂
- handler:线程池饱和策略
线程池工作流程:
- 提交一个任务,如果存活的核心线程数小于corePoolSize,则会创建一个核心线程。
- 如果核心线程数已满,提交的任务会被放到workQueue里面排队
- 如果核心线程数满了,workqueue也满了,判断线程时是否达到maximumPoolSize,如果没达到,则创建非核心线程。
- 如果都满了,还有新任务过来,直接采用拒绝策略。
线程池异常处理:
在执行任务时发生异常,线程内部会处理,我们可能无法感知。为了感知异常,有如下方式:
- 主动try-catch
- submit方法可以返回一个future对象,利用future对象的get()可以感知异常
- 为工作者线程设置UncaughtExceptionHandler,即:Thread t = getCurrentThread(); t.UncaughtExceptionHandler
常见的线程池:
- newFixedThreadPool:固定数目线程池
核心线程数和最大线程数一样;
keepAliveTime为0;
阻塞队列为无界队列LinkedBlockingQueue(链表结构阻塞队列,无界) - newSingleThreadExecutor:单线程线程池
核心线程数为1;
最大线程数为1;
keepAliveTime为0;
阻塞队列是LinkedBlockingQueue; - newCachedThreadPool:可缓存的线程池
核心线程数为0;
最大线程数为Integer.MAX_VALUE
阻塞队列是SynchronousQueue (同步队列)
非核心线程空闲存活时间为60秒,由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源 - newScheduledThreadPool:周期线程池
最大线程数为Integer.MAX_VALUE
阻塞队列是DelayedWorkQueue(定时延迟任务队列)
keepAliveTime为0
线程池工作队列:
- ArrayBlockingQueue:有界队列,数组实现
- LinkedBlockingQueue:无界队列,链表实现
- DelayQueue:任务定时执行,周期执行的延迟队列
- PriorityBlockingQueue:具有优先级的无界阻塞队列
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作都必须阻塞等待另一个线程的移除操作
线程池拒绝策略:
- AbortPolicy:默认策略,抛出一个异常
- DiscardPolicy:直接丢弃任务
- DiscarOldestPolicy:丢弃队列中最老的任务,把这个新任务交给线程池
- CallerRunsPolicy:交给线程池,调用所在的线程处理
classloader加载模型
java默认提供三种类加载其
- bootstrap classloader启动类加载器
- Extension classloader扩展类加载器
- app classloader 系统应用类加载器
classloader使用双亲委托模型来搜索类的。
问:为什么使用双亲委托模型?
答:这样可以避免重复加载,当父类已经加载该类的时候,就没有必要使用子classloader在加载一次。
GC
-
jvm内存结构
方法区,堆区,虚拟机栈,本地方法栈,程序计数器 -
可达性分析
以GC root作为起点进行搜索,如果GC root和一个对象没有可达路径,则判定该对象为垃圾 -
GC算法
(1) 标记-清除 算法
标记阶段:标记出所有需要被回收的对象
清除阶段:回收被标记对象所占用的空间
算法优缺点:
优点,简单。缺点,容易产生内存碎片。(2) 复制 算法
将内存按容量分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将活着的对象复制到另外一块上面,然后
再把已使用的内存一次性清理掉。
算法优缺点:
运行高效,解决了内存碎片的问题,但内存使用缩减到原来的一半。(3) 标记-整理 算法
和标记-清除算法类似,但在标记完成之后,它不清理可回收对象,而是将存活对象向内存一端移动,然后清理掉端边界以外内存。
算法优缺点:
在标记-清除的基础上,增加了对象移动,因此成本高
但,解决了内存碎片(4) 分代收集 算法
根据对象存活的生命周期,将内存划分为若干个不同区域。一般情况下,将堆区划分为老年代,新生代,永久代。
老年代:每次gc,只有少量的对象被回收,一般使用标记-整理算法,标记-清除算法。
新生代:每次gc,有大量的对象需要被回收,一般采用复制算法。
永久代:方法区纳入gc分代收集,并命名为永久代。
Minor GC和Full GC触发条件
Minor GC触发条件:
- 当Eden区(新生代里的一个去)满时,触发Minor GC。
Full GC触发条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
GC中Stop the world
简称STW,是在GC算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。
是Java中的一种全局暂停现象,native代码可以执行,但不能与JVM交互。
Android知识
理解ACTION_CANCEL事件
当我们收到CANCEL事件时,表明父类想要接管这个事件。举个例子:当我们手指在listview或者scrollview上滑动时,我们肯定是想滚动ListView或者ScrollView,而不是去触摸内部Button。
通常我们在自定义ViewGroup时,ViewGroup在消费事件时,必要的时候也会向子view发送个CANCEL事件,表情我们ViewGroup需要接管Touch事件。
对CANCEL事件的处理逻辑一般和UP事件处理逻辑一致。
Android为什么要用Binder机制
- 性能角度,Binder只需要拷贝一次数据,性能仅次于共享内存
- 安全角度,对通信双方有身份校验机制
- 稳定性,Binder基于C/S架构开发,架构清晰明朗,Server端和Client端相互独立,稳定性高。而共享内存,需要考虑访问临界资源的同步问题,一不小心就能引发死锁,Binder架构要由于共享内存。
内存泄露查找定位方法
- 利用Memory Profiler分析,有个图表显示了内存的使用状况。根据图表如果观察到内存有泄漏,工具也可以查看到java堆和内存分配,查看到没有释放的类,以及内存对象的引用路径。
- 利用开源框架:leak-canary分析
Serializable和Parcelable的区别
序列化:如果想要传输一个对象,比如在网络上传播传输对象,在本地存储对象,需要将对象进行序列化和反序列化。
区别:
- Serializable时java自带接口,Parcelable是Android专用接口;
- Serializable序列化有大量的IO操作,开销大;
- Parcelable方式是将一个完整的对象进行分解,分解后每一部分都是Intent所支持的类型,效率要比Serializable高。
- Serializable方式较为简单,但会把整个对象序列化,效率比Parcelable低,所以Android推荐使用Parcalable方式实现在Intent传递对象功能;
ThreadLocal - Looper线程唯一
ThreadLocal是一个创建线程局部变量的类,使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。
Http 请求整体流程
- DNS解析,获取域名对应的ip地址
- 建立tcp连接
- 发送Http请求
- server发送http响应
- server关闭连接。
Https 加密流程,哪些环节是对称加密,哪些环节是非对称加密
https采用了对称加密+非对称加密结合的方式保护C/S之间通信安全
流程:
- 客户端请求server证书(证书里包含公钥)
- 客户端用这个公钥对“对称密钥”加密,发送给server,server用私钥解密获取对称密钥
- 之后的数据传输就用这个对称密钥加密传输。
也就是说,一开始通过几次握手双方确认对称密钥,之后双方的交互通过对称密钥进行加密通信。
依赖注入方式有哪些?
- 构造方法注入
- set 注入
- 接口注入
Android最常见的依赖注入场景:findViewById,setOnClickListener的场景。即,通过注解,依赖注入框架会去获取view对象实例,再利用反射把view实例注入进去,简化了很多findViewById,setOnClickListener这样的模板代码。
App 优化方面:性能优化,布局优化等
-
内存优化
LruCache存储对象;
inSamplesize去压缩图像,因为分辨率大的图片放到很小的组件上,不会给视觉带来极大的好处,反而浪费了宝贵的内存。
二级缓存;内存缓存,disk缓存,
不要循环创建大量临时对象,防止频繁gc导致内存抖动; -
UI布局优化
布局层级不能太深,尽量扁平化
一次测量,布局,绘制的过程不能超过16ms,否则导致丢帧
过度绘制,主要background背景色的绘制,不要绘制冗余的背景
viewstub优化,展现优先级比较低的控件。
Android 系统如何检测 ANR
ANR定义:UI线程阻塞超过5s就会出发ANR。
检测方式:
- 检测消息队列中一个runnable执行时长。
- 通过Choreographer检测两帧之间的渲染时间间隔。
Android 中 SharedPreferences 的 commit 和 apply 之间的区别
- commit同步提交到内存缓存和磁盘缓存;
- apply是提交到内存缓存后异步提交到磁盘缓存,apply没有返回值,不会提示失败;
- 效率apply比commit高,如果不关心返回值建议使用apply。如果需要确保提交成功后做后续操作还是需要用commit
MVC MVP MVVM 各自优缺点
MVC: 试图,模型,控制器:
1. view传送指令给controller
2. controller完成业务逻辑,更新model
3. model将新数据发送给view
MVP: 将controller换成了presenter
1. 各部分通信都是双向的。包括view<->presenter,presenter<->model
2. view层做的非常薄,不部署业务逻辑,称为被动试图;Presenter做的非常厚,所有逻辑都部署在里面
MVVM: presenter改名为viewmodel
区别就是:数据采用双向绑定。view的变动反应到viewmodel,反之亦然;
LiveData的坑
1. postValue()的坑,存在数据丢失问题
存在问题: postValue是先把数据先存在mPendingData中,然后往主线程抛一个runnable,在runnable里调用setValue来把存储的值设置上去并回调给观察者。如果在这个过程中子线程连续多次postValue,这时只会更新mPendingData,并不会再次抛新的runnable。这样后面的值就会把前面的值覆盖掉,导致数据丢失。
解决方案: 每次postValue都往主线程抛一个新的runnable出来,在runnable的run()方法中调用liveData.setValue(data)
2. setValue()的坑,非激活状态的观察者,收不到数据更新
存在问题: setValue()后,如果观察者的生命周期处于非激活状态,它就直接return,不会回调。待观察者激活之后再把最新的值回调给他,这样在观察者激活之前中间的值就全部丢失了。
解决方案: 1. 自己造一个观察者,可以收到LiveData的所有数据(LiveData.observeForever()实现);2. 这个观察者还得感知生命周期,在Active时,直接调用observer.onChange(t);在InActive()时将数据暂存起来,等到active后再把暂存的数据传出去。
各种图片格式之间区别及优缺点,JPG、PNG等
- jpg:有损压缩;体积小;清晰度低;jpg因为体积小,显示可接受,应用场景多
- png:无损压缩;体积大;清晰度高;png体积大,应用场景没jpg多
三次握手四次挥手细节,为什么要四次挥手
三次握手:
- client向server发送一个请求连接报文段;
- server收到client请求连接后,如果同意连接会想client发送确认。
- client收到server的确认后,需要再次发送确认。
四次挥手:
假设连接主动断开由client发起
- client向server发送FIN,表明client数据已经结束,关闭了由client到server的数据传送
- server收到FIN后,发送ACK给client
- server发送FIN给client,关闭server到client的数据通道
- client收到FIN发送ACK给server。
问:为什么需要三次握手
答:为了防止已经失效的连接请求报文段又传送到服务端,产生错误。比如client发送了一个请求连接的报文段,在网络节点中滞留了很久,延误到连接释放之后的某个时间到达server,这个失效的报文段到达server后,server误以为这是一个client的请求,于是就向client发送同意建立连接。如果没有三次握手的话,只要server发出确认,连接就建立了,这肯定是有问题的。
问:为什么需要四次挥手
答:TCP协议是全双工的,需要四次挥手主要是为了保证通信双方可以独立关闭自己的通信通道。
设计模式基本原则
- 单一原则,一个类和一个方法职责单一
- 里氏替换原则,多态性,所有引用基类的地方,都必须能透明使用子类对象;
- 接口隔离,使用多个专门接口,而不使用单一总接口;
- 开闭原则,一个软件实体对拓展开放,对修改关闭。尽量在不修改原有代码的情况下进行拓展。
- 依赖倒转,面向接口编程,细节依赖抽象,抽象不应该依赖细节。
- 迪米特法则,一个软件实体应尽可能少的和其他实体发生作用。
APK 打包整体流程
- 打包res资源文件,生成R.java
- 处理aidl文件,生成相应的java文件
- 编译项目源代码,生成class文件
- 转换所有class文件(包括自己写,第三方库的),生成class.dex文件
- 将res文件,assets文件,class.dex一起打包生成apk
- 对apk进行签名
- 对签名后的文件对齐处理,保证内存映射访问apk文件时会更快,减少内存的运行。
App启动流程
- Android中每个app都运行在一个独立的进程中。
- 进程会在其需要的时候被启动,比如:在launcher中点击app的icon;其他组件调取你app的组件。
- 启动app都会调用startActivity(intent),以Launcher为例,点击一个icon,Launcher会调用startActivity(),启动app
- 代码会进入AMS,AMS会通知原activity pause;
- 原activity pause后,会通知AMS
- AMS启动一个进程,执行该进程的ActivityThread的Main()方法
- 在main()方法创建MainLooper出来,然后调用AMS的attachApplication()方法
- AMS调用bindApplication,创建appliction,调用onCreate()
- AMS调用scheduleLaunchActivity,创建Activity,调用attach,调用onCreate, OnResume等生命周期方法,添加View到WindowManager
- 通知AMS, Activity进入resumed()状态
- 新app已经启动,通知原app Stop
Glide图片加载框架
- 跟踪生命周期,通过添加一个没有视图的Fragment实现
- 图片压缩功能,根据组件大小去压缩图片
- 缓存(内存缓存 + Disk缓存),内存缓存是LruCache+弱引用实现的。正在使用的图片用弱引用缓存,非正在使用的图片用LruCache实现。
Disk缓存,Glide缺省没有使用DiskLruCache,是Glide自己实现了一套。Disk缓存策略: NONE, SOURCE, RESULT, ALL - 回调与监听。回调:自定义Target功能;监听:添加Listener
- 图片变换(Transform),原始图片下载下来后,显示之前可以对图片进行变换,包括:大小,圆角化,虚化…
- 自定义模块,Glide允许对组件进行替换,比如网络加载组件可以替换成OkHttp的。Disk缓存可以替换成DiskLruCache。
Glide网络请求并发调度
glide的网络请求线程池:newSourceExecutor,这个与Executors的newFixedThreadPool类似,都是固定大小的线程池,不过任务队列不同。
glide使用的任务队列为:PriorityBlockingQueue,是一种基于优先级的阻塞队列,插入一个任务需要实现Comparable接口的compareTo方法对任务提供排序依据。
glide将加载请求和Target(ImageView)相关联,某个ImageVIew开始加载请求前,会先将该ImageView之前关联的请求清除。
当列表(ListView/RecyclerView)快速滚动时,同时执行网络请求的数量为线程池的corePoolsize,其余的请求会放到队列中等待执行,虽然队列长度可能会一下增加到几十,但随着列表中view的复用,队列中大部分请求都会被取消掉。也就是说,快速滚动过程中很多列表项的请求都会被略过。这样的机制保证了不会过度消耗资源导致滑动卡顿
Glide加载一张图片的流程
-
with(),创建RequestManager,添加没有视图的Fragment,跟踪Activity的生命周期。
-
load(),load的重载方法特别多,支持URL,String,File等多重加载方式。根据load传入了类型获取ModelLoader,如果是String类型会得到StreamStringLoader。随后会创建出DrawableRequestBuilder,这个类里面维护了图片加载的配置信息,像placeholder,error,是否使用缓存等等。
-
into()
(1) 首先会创建出target对象,如果你传入的ImageView,那么对应的Target对象就是:GlideDrawableImageViewTarget;(2) 接下来创建Request对象,Request对象Glide主要是用来执行图片加载请求,调用Request.begin()
(3) Glide首先获取Target的size,因为Glide要根据这个size对下载的图片压缩。获取到size会进入Target的onSizeReady(with, height)回调。
(4) 接下来创建EngineJob开启线程加载图片了。通过HttpUrlConnection下载图片,获取InputStream。
(5) 通过解码器GifBitmapWrapperResourceDecoder,对InputStream进行解析。内部会根据文件头信息得出ImageType,如果ImageType是GIF:就会走解析Gif的逻辑,decodeGifWrapper();否则就走解析图片的逻辑:decodeBitmapWrapper()。细节就是InputStream解析成Gif对象或者Bitmap对象。
(6) 解析完成后,解析的结果是一个Resource对象,随后将Resource对象回调一层一层回调出去,最终会走到Target.onResourceReady(Resource),Target拿到Resource对象后做图片显示逻辑。
Glide如何加载Gif的
- Glide请求gif后,得到网络的输入流inputstream,接下来就是Gif的解码逻辑
- 随后会代码会走到:BitmapWrapperResourceDecode类中的decodeStream()
- decodeStream()首先会从inputstream中解析出ImageType,如果ImageType是GIF,后面就会走gif图的decode逻辑,代码在ByteBufferGifDecoder类中的decode方法。
- 首先会把gif图的首帧解析出来,生成一个位图bitmap
- 然后会生成一个GifDrawable对象,传入的参数有:首帧Bitmap,GifDecoder,widh,height等图片参数信息。
- 最后用GifDrawable对象,创建出了GifDrawableResource对象,并返回。
- 然后回调onLoadComplete(Resource resource),至此就完成了gif资源的解析过程。
- Gif的播放逻辑是在GifDrawable内部完成,细节没了解过。
RecyclerView复用机制
复用单位是:ViewHolder
涉及到的缓存:
- ScrapList(mAttachedScrap,mChangedScrap)
- CachedViews
- RecyclerViewPool
复用场景:
- Feed流下拉刷新场景:下拉刷新后,调用adapter.notifyxxx(),触发recyclerview布局子view,在布局子view之前,recyclerview会先把所有子view设置成scrap状态(设置状态位,parent=null),然后添加进mAttachedScrap集合中。在布局子view时,会从Recycler缓存中寻找ViewHolder。在这种场景下几乎都是复用mAttachedScrap
- 滚动复用场景:滚出去的子view,会进入CachedViews,但在这之前,会先判断CachedViews有没有足够空间,如果没有空间了,就把最老的ViewHolder从CachedViews挪到RecyclerViewPool中。所以在普通的滚动复用的情况下,ViewHolder的复用主要来自于mCacheViews集合