首页 火币pro官网下载文章正文

裸辞-闭关-复习-大厂offer(二)

火币pro官网下载 2022年10月21日 11:02 89 Connor

本文作者

作者: 唐子玄

链接:

上一篇,如果没看过建议从篇 1 开始。

裸辞-闭关-复习-大厂offer

1

引子

2022 年 3 月辞职,没多久上海爆发疫情,蜗居在家准备面试。在经历 1 个月的闭关和 40+ 场 Android 面试后,拿到一些 offer。

总体上说,有如下几种面试题型:

场景题,即“就业务场景给出解决方案”,考察运用知识解决问题的能力。这类题取决于临场应变、长期积累、运气。

项目经历题取决于对工作内容的总结提炼、拔高升华、运气:

力争把默默无闻的“拧螺丝”说成惊天动地的“造火箭”。(这是一门技术活)

但也不可避免地会发生“有些人觉得这是高大上的火箭,有些人觉得不过是矮小下的零件”。面试就好比相亲,甲之蜜糖乙之砒霜是常有的事。除非你优秀到解决了某个业界的难题。

算法题取决于刷题,运气,相较于前两类题,算法题可“突击”的成分就更多了。只要刷题足够多,胜算就足够大。大量刷,反复刷。

基础知识题是所有题型中最能“突击”的,它取决于对“考纲”的整理复习、归纳总结、背诵、运气。Android 的知识体系是庞杂的,对于有限的个人精力来说,考纲是无穷大的。

这不是一篇面经,把面试题公布是不讲武德的。但可以分享整个复习稿,它是我按照自己划定的考纲整理出的全部答案。

整个复习稿分为如下几大部分:

由于篇幅太长,决定把全部内容分成两篇分享给大家。其中,Android 和 Java & Kotlin 已经在第一篇分享过,这一篇的内容是剩下的加粗部分。

2

设计模式/原则 & 架构

设计原则

单一职责原则:关于内聚的原则。高内聚、低耦合的指导方针,类或者方法单纯,只做一件事情。

展开全文

接口隔离原则:关于内聚的原则。要求设计小而单纯的接口(将过大的接口拆分),或者说只暴露必要的接口。

最少知识法则:

• 关于耦合的原则。要求类不要和其他类发生太多关联,达到解耦的效果。

• 从依赖者的角度来说,只依赖应该依赖的对象。

• 从被依赖者的角度说,只暴露应该暴露的方法。

• 对象方法的访问范围应该受到约束:

1. 对象本身的方法。

2. 对象成员变量的方法。

3. 被当做参数传入对象的方法。

4. 在方法体内被创建对象的方法。

5. 不能调用从另一个调用返回的对象的方法。

开闭原则:关于扩展的原则。对扩展开放对修改关闭,做合理的抽象就能达到增加新功能的时候不修改老代码(能用父类的地方都用父类,在运行时才确定用什么样的子类来替换父类),开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段。

里氏替换原则:

• 为了避免继承的副作用,若继承是为了复用,则子类不该改变父类行为,这样子类就可以无副作用地替换父类实例,若继承是为了多态,则因为将父类的实现抽象化,

依赖倒置原则:即是面向接口编程,面向抽象编程,高层模块不该依赖底层模块,而是依赖抽象(比萨店不应该依赖具体的至尊披萨,而应该依赖抽象的披萨接口,至尊披萨也应该依赖披萨接口)

单例模式

目的:在单进程内保证类唯一实例。

静态内部类:虚拟机保证一个类的初始化操作是线程安全的,而且只有使用到的时候才会去初始化,缺点是没办法传递参数。

双重校验:第一校验处于性能考虑,若对象存在直接返回,不需要加锁。第二次校验是为了防止重复构建对象。对象引用必须声明为 volatile,通过保证可见性和防止重排序,保证单例线程安全。因为INSTANCE = new instance不是原子操作,由三个步骤实现1.分配内存2.初始化对象3.将INSTANCE指向新内存,当重排序为1,3,2时,可能让另一个线程在第一个判空处返回未经实例化的单例。

工厂模式

目的:解耦。将对象的使用和对象的构建分割开,使得和对象使用相关的代码不依赖于构建对象的细节。

增加了一层“抽象”将“变化”封装起来,然后对“抽象”编程,并利用”多态“应对“变化”,对工厂模式来说,“变化”就是创建对象。

实现方式:

1. 简单工厂模式

将创建具体对象的代码移到工厂类中的静态方法。

实现了隐藏细节和封装变化,对变化没有弹性,当需要新增对象时需要修改工厂类。

2. 工厂方法模式

在父类定义一个创建对象的抽象方法,让子类决定实例化哪一个具体对象。

特点:1. 只适用于构建一个对象。2. 使用继承实现多态。

3. 抽象工厂模式

定义一个创建对象的接口,把多个对象的创建细节集中在一起。

特点:使用组合实现多态。

建造者模式

目的:简化对象的构建。

它是一种构造复杂对象的方式,复杂对象有很多可选参数,如果将所有可选参数都作为构造函数的参数,则构造函数太长,建造者模式实现了分批设置可选参数。Builder模式增加了构造过程代码的可读性。

Dialog 用到了这个模式。

观察者模式

目的:以解耦的方式进行通信。将被观察者和具体的观察行为解耦。

是一种一对多的通知方式,被观察者持有观察者的引用。

ListView的BaseAdapter中有DataSetObservable,在设置适配器的时候会创建观察者并注册,调用notifydataSetChange时会通知观察者,观察者会requestLayout。

策略模式

目的:将使用算法的客户和算法的实现解耦。

手段:增加了一层“抽象”将“变化”封装起来,然后对“抽象”编程,并利用”多态“应对“变化”,对策略模式来说,“变化”就是一组算法。

实现方式:将算法抽象成接口,用组合的方式持有接口,通过依赖注入动态的修改算法

setXXListener都是这种模式。

装饰者模式

目的:用比继承更灵活的方式为现有类扩展功能。

手段:具体对象持有超类型对象。

~是继承的一种替代方案,避免了泛滥子类。

~增加了一层抽象,这层抽象在原有功能的基础上扩展新功能,为了复用原有功能,它持有原有对象。这层抽象本身是一个原有类型。

~实现了开闭原则。

外观模式

目的:隐藏细节,降低复杂度。

手段:增加了一层抽象,这层抽象屏蔽了不需要关心的子系统调用细节。

降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。

对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

实现方式:外观模式会通过组合的方式持有多个子系统的类,~提供更简单易用的接口(和适配器类似,不过这里是新建接口,而适配器是已有接口)。

通过外观模式,可以让类更加符合最少知识原则。

ContextImpl是外观模式。

适配器模式

意图:将现有对象包装成另一个对象。

手段:增加了一层抽象,这层抽象完成了对象的转换。(具体对象持有另一个而具体对象)。

是一种将两个不兼容接口(源接口和目标接口)适配使他们能一起工作的方式,通过增加一个适配层来实现,最终通过使用适配层而不是直接使用源接口来达到目的。

代理模式

目的:限制对象的访问,或者隐藏访问的细节。

手段:增加了一层抽象,这层抽象拦截了对对象的直接访问。

实现方式:代理类通过组合持有委托对象(装饰者是直接传入对象,而代理通常是偷偷构建对象)。

分类 :代理模式分为静态代理和动态代理。

静态代理:在编译时已经生成代理类,代理类和委托类一一对应。

动态代理:编译时还未生成代理类,只是定义了一种抽象行为(接口),只有当运行后才生成代理类,使用Proxy.newProxyInstance,并传入invocationHandler。

Binder通信是代理模式,Retrofit运用动态代理构建请求。

模板方法模式

目的:复用算法。

手段:新增了一层抽象(父类的抽象方法),这层抽象将算法的某些步骤泛化,让子类有不同的实现。

实现方式:在方法(通常是父类方法)中定义算法的骨架,将其中的一些步骤延迟到子类实现,这样可以在不改变算法结构的情况下,重新定义某些步骤。这些步骤可以是抽象的(表示子类必须实现),也可以不是抽象的(表示子类可选实现,这种方式叫钩子)。

android触摸事件中的拦截事件是钩子。

android绘制中的onDraw是钩子。

命令模式

目的:将执行请求和请求细节解耦。

手段:增加了一层“抽象”将“变化”封装起来,然后对“抽象”编程,并利用”多态“应对“变化”,对命令模式来说,“变化”就是请求细节。新增了一层抽象(命令)。

这层抽象将请求细节封装起来,执行者和这层抽象打交道,就不需要了解执行的细节。因为请求都被统一成了一种样子,所以可以统一管理请求,实现撤销请求,请求队列。

实现方式:将请求定义成命令接口,执行者持有命令接口。

java中的Runnable就是命令模式的一种实现。

桥接模式

目的:提高系统扩展性。

手段:抽象持有另一个抽象。

是适配器模式的泛化模式。

访问者模式

目的:动态地为一类对象提供消费它们的方法。

重载是静态绑定(方法名相同,参数不同),即在编译时已经绑定,方法的参数无法实现运行时多态。

重写是动态绑定(继承),方法的调用者可实现运行时多态。

双分派:a.fun(b)在a和b上都实现运行时多态,实现方法调用者和参数的运行时多态。

编译时注解使用了访问者模式,一类对象是Element,表示构成代码的元素(类,接口,字段,方法),他有一个accept方法传入一个Visitor对象。

架构

关于 MVP,MVVM,MVI,Clean Architecture 的介绍可以点击如何把业务代码越写越复杂?| MVP - MVVM - Clean Architecture。

3

多线程

进程 & 线程

系统按进程分配除CPU以外的系统资源(主存 外设 文件), 系统按线程分配CPU资源。

Android系统进程叫system_server,默认情况下一个Android应用运行在一个进程中,进程名是应用包名,进程的主线程叫ActivityThread。

jvm会等待普通线程执行完毕,但不会等守护线程。

若线程执行发生异常会释放锁。

线程上下文切换:cpu控制权由一个运行态的线程转交给另一个就绪态线程的过程(需要从用户态到核心态转换)。

一对一线程模型:java语言层面的线程会对应一个内核线程。

抢占式的线程调度,即由系统决定每个线程可以被分配到多少执行时间。

阻塞线程的方法

sleep:使线程到阻塞态,但不释放锁,会触发线程调度。

wait:使线程到阻塞态,释放锁(必须先获取锁)。

yield:使线程到就绪态,主动让出cpu,不会释放锁,发生一次线程调度,同优先级或者更高优先级的线程有机会执行。

线程安全三要素

原子性:不会被线程调度器中断的操作。

可见性:一个线程中对共享变量的修改,在其他线程立即可见。

有序性:程序执行的顺序按照代码的顺序执行。

原子操作

除long和double之外的基本类型(int, byte, boolean, short, char, float)的赋值操作。

所有引用reference的赋值操作,不管是32位的机器还是64位的机器。

java.concurrent.Atomic.* 包中所有类的原子操作。

死锁

四个必要条件:

1. 互斥访问资源。

2. 资源只能主动释放,不会被剥夺。

3. 持有资源并且还请求资源。

4. 循环等待 解决方案是:加锁顺序+超时放弃。

线程生命周期

线程从新建状态到就绪状态,就绪态的线程如果获得了cpu执行权就变成了运行态,运行完变成死亡态,如果运行中产生等待锁的情况(sleep,wait),则会进入阻塞态,当阻塞态的进程被唤醒后进入就绪态,参与cpu时间片的竞争,执行完毕死亡态。

线程池

如果创建对象代价大,且对象可被重复利用。则用容器保存已创建对象,以减少重复创建开销,这个容器叫做池。线程的创建就是昂贵的,通过线程池来维护实例。

线程通信:等待通知机制

等待通知机制是一种线程间的通信机制,可以调整多个进程的执行顺序。

需要等待某个资源的线程可以调用 wait,当某资源具备后,可以调用统一对象上的notify。

notify:随机使一个线程进入就绪态,它需要和调用wait是同一个对象(获得锁的线程才能调用)。

notifyAll:唤醒所有等待线程,让他们到就绪队列中。

Condition

是多线程通信的机制,挂起一个线程,释放锁,直到另一个线程通知唤醒,提供了一种自动放弃锁的机制。

await挂起线程的同时释放锁(所以必须先获取锁,否则抛异常),signal 唤醒一个等待的线程。

每个Condition对象只能唤醒调用自己的await方法的那个线程。

如果条件不用 Condition 实现,则线程可能不断地获取锁并释放锁,但因继续执行的条件不满足,cpu 负载打满。使用Condition 让等待线程不消耗cpu。

await 通常配合while{await} 因为被唤醒是从上次挂起的地方执行,还需要再次判断是否满足条件。

await必须在拥有锁的情况下调用,以防止lost wake-up,即在await条件判断和await调用之间notify被调用了。当await条件满足后,还没来得及执行await时发生线程调度,另一个线程调用了notify。然后才轮到await执行,它将错过刚才的notify,因为notify在await之前执行。

interrupt

不会真正中断正在执行的线程,只是通知它你应该被中断了,自己看着办吧。

若线程正运行,则中断标志会被置为true,并不影响正常运行。

如果线程正处于阻塞态,则会收到InterruptedException,就可以在 catch中执行响应逻辑。

若线程想响应中断,则需要经常检查中断标志位,并主动停止,或者是正确处理 InterruptedException。

内存屏障

用于禁止重排序,它分为以下四种:

1. LoadLoad Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。

2. StoreStore Store1; StoreStore; Store2 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。

3. LoadStore Load1; LoadStore; Store2 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。

4. StoreLoad Store1; StoreLoad; Load2 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

volatile

保证变量操作的有序性和可见性。

在每一个volatile写操作前面插入一个StoreStore屏障,可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。

在每一个volatile写操作后面插入一个StoreLoad屏障,避免volatile写与后面可能有的volatile读/写操作重排序。

在每一个volatile读操作后面插入一个LoadLoad屏障,禁止处理器把上面的volatile读与下面的普通读重排序。

在每一个volatile读操作后面插入一个LoadStore屏障,禁止处理器把上面的volatile读与下面的普通写重排序。

volatile就是将共享变量在高速缓存中的副本无效化,这导致线程修改变量的值后需立刻同步到主存,读取共享变量都必须从主存读取。

当volatile修饰数组时,表示数组首地址是volatile的而不是数组元素。

CAS

Compare and Swap。

当前值,旧值,新值,只有当旧值和当前值相同的时候,才会将当前值更新为新值。

Unsafe将cas编译成一条cpu指令,没有函数调用。

aba问题:当前值可能是变为b后再变为a,此a非彼a,通过加版本号能解决。

非阻塞同步:没有挂起唤醒操作,多个线程同时修改一个共享变量时,只有一个线程会成功,其余失败,它们可以选择轮询。

synchronized

隐式加锁释放锁。

可修饰静态方法,实例方法,代码块。

当修饰静态方法的时,锁定的是当前类的 Class 对象(就算该类有多个实例,使用的还是同一把锁)。

当修饰非静态方法的时,锁定的是当前实例对象 this。当 饰代码块时需要指定锁定的对象。

通过将对变量的修改强制刷新到内存,且下一个获取锁的线程必须从内存拿。保证了可见性。

同一时间只有一个线程可以执行临界区,即所有线程是串行执行临界区的。

happen-before 就是释放锁总是在获取锁之前发生。

synchronized特点:

• 可重入:可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。线程可以再次进入已经获得锁的代码段,表现为monitor计数+1。

• 不公平:synchronized 代码块不能够保证进入访问等待的线程的先后顺序。

• 不灵活:synchronized块必须被完整地包含在单个方法里。而一个 Lock 对象可以把它的lock 和 unlock方法的调用放在不同的方法里。

• 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,synchronized是自旋锁。如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。

1.8 之后synchronized性能提升:

• 偏向锁:目的是消除无竞争状态下性能消耗,假定在无竞争,且只有一个线程使用锁的情况下,在 mark word中使用cas 记录线程id(Mark Word存储对象自身的运行数据,在对象存储结构的对象头中)此后只需简单判断下markword中记录的线程是否和当前线程一致,若发生竞争则膨胀为轻量级锁,只有第一个申请偏向锁的会成功,其他都会失败。

• 轻量级锁:使用轻量级锁,不要申请互斥量,只需要用 CAS 方式修改 Mark word,若成功则防止了线程切换。

• 自旋(一种轻量级锁):竞争失败的线程不再直接到阻塞态(一次线程切换,耗时),而是保持运行,通过轮询不断尝试获取锁(有一个轮询次数限制),规定次数后还是未获取则阻塞。进化版本是自适应自旋,自旋时间次数限制是动态调整的。

• 重量级锁:使用monitorEnter和monitorExit指令实现(底层是mutex lock),每个对象有一个monitor。

• 锁膨胀是单向的,只能从偏向->轻量级->重量级。

ReentrantLock

手动加锁手动释放:JVM会自动释放synchronized锁,但可重入锁需要手动加锁手动释放,要保证锁定一定会被释放,就必须将unLock放到finally{}中。手动加锁并释放灵活性更高:

• 可中断锁:lockInterruptibly,未获取则阻塞,但可响应当前线程的interrupt被调用。

• 超时锁:tryLock(long timeout, TimeUnit unit) ,未获取则阻塞,但阻塞超时。

• 非阻塞获取锁:tryLock ,未获取则直接返回。

• 可重入:若已获取锁,无需再次竞争即可重新进入临界区执行,state +1,出来的时候需要释放两次锁 state -1。

• 独占锁:同一时间只能被一个线程获取,其他竞争者得等待(AQS独占模式)。

性能:竞争不激烈,Synchronized的性能优于ReetrantLock,激烈时,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

是AQS的实现类,AQS中有一个Node节点组成双向链表,存放等待的线程对象(被包装成Node)。

获取锁流程:

• 尝试获取锁,公平锁排队逻辑:判断锁是否空闲,若空闲还要判断队列中是否有排在更前面的等待线程,若无则尝试获取锁。若当前独占线程是自己,表示重入,则增加state值。非公平锁抢占逻辑:直接进行CAS state操作(从0到1),若成功则设置当前线程为锁独占线程。若失败会判断当前线程是否就是独占线程若是表示重入,state+1。

• 获取失败则入AQS队列,然后在挂起线程:将线程对象包装成EXCLUSIVE模式的Node节点插入到AQS双向链表的尾部(cas值链尾的next结点+自旋保证一定成功),并不断尝试获取锁,或中断Thread.interrupted。

释放锁流程:

• 释放锁表现为将state减到0。

• 调用unparkSuccessor唤醒线程(非公平时如何唤醒)。

ReentrantReadWriteLock

并发度比ReentrantLock高,因为有两个锁,使用AQS,读锁是共享模式,写锁是独占模式。读多写少的情况下,提供更大的并发度。

可实现读读并发,读写互斥,写写互斥。

使用一个int state记录了两个锁的数量,高16位是读锁,低16位是写锁。

获取写锁过程:除了考虑写锁是否被占用,还要考虑读锁是否被占用,若是则获取锁失败,否则使用cas置state值,成功则置当前线程为独占线程。

读并发也有并发数限制,获取读锁时需验证,并使用ThreadLocal记录当前线程持有锁的数量。

可能发生写饥饿,因为太多读。

锁降级:当一个线程获取写锁后再获取读锁,然后释放写锁。

不支持锁升级是为了保证可见性:多个线程获取读锁,其中任意线程获取写锁并更新数据,这个更新对其他读线程是不可见的。

StampedLock

实现读读并发,读写并发。

在读的时候如果发生了写,应该通过重试的方式来获取新的值,而不应该阻塞写操作。

用二进制位记录每一次获取写锁的记录。

CountdownLatch

用作等待若干并发任务的结束。

内部有一个计数器,当值为0时,在countdownLatch上await的线程就被唤醒。

通过AQS实现,初始化是置AQS.state为n,countdow通过自旋+cas将执行state--效果。

CyclicBarrier

用于同步并发任务的执行进度。

使用ReentranntLock保证count线程安全,每次调用await count--,然后在condition上阻塞,当count为0时,会signalAll。

Semaphore

用于限制并发访问资源线程的个数。

基于AQS,初始化是为state赋值最大并发数,调用acquire时即是cas将state-1,当state小于零时,令牌不足,将线程包装成node结点会入队列,然后挂起。

有公平和非公平两个构造方法。

AbstractQueuedSynchronizer

实现了cas方式竞争共享资源时的线程阻塞等待唤醒机制。

AQS提供了两种资源共享方式1.独占:只有一个线程能获取资源(公平,不公平)2.共享:多个进程可获取资源。

AQS使用了模板方法模式,子类只需要实现tryAcquire和tryRelease,等待队列的维护不需要关心。

AQS使用了CLH 队列:包括sync queue和condition queue,后者只有使用condition的时候才会产生。

持有一个volatile int state代表共享资源,state =1 表示被占用(提供了CAS 更新 state 的方法),其他线程来加锁会失败,加锁失败的线程会放入等待队列(被Unsafe.park挂起)。

等待队列的队头是独占资源的线程。队列是双向链表。

AtomicInteger

线程安全的int值,为了避免在一个变量上使用锁实现同步,这样太重了。

在高并发的情况下,每次值成功更新了都需要将值刷到主存。

自增使用cas+自旋+volatile。

publicfinal intincrementAndGet{

for(;;) { // 自旋

intcurrent = get;

intnext = current + 1;

if(compareAndSet(current, next)) // cas

returnnext;

AtomicIntegerArray

线程安全整形数组。

内部持有final int[] array,保证了使用时已经初始化,并且引用不能改变。

对数组的操作退化为对单个元素的操作,因为数组的内存模型是连续的,并且每个元素所占空间一样大。

使用Unsafe带有volatile的方法进行对单个元素赋值。

AtomicReference

提供对象引用非阻塞原子性并发读写。

对象引用是一个4字节的数字,代表着堆内存中的一个地址。

CopyOnWriteArrayList

实现了线程安全的读写并发,读读并发,但不能实现写写并发(上锁了,synchronized),因为他们操纵是不同的副本。

使用不可变Object[]作为容器。

写时复制数组,写入新数组,引用指向新数组。加锁防止一次写操作导致复制了多个数组副本。

读操作就是普通的获取数组某元素。

适合读多写少,因为写需要复制数组,耗时。

适合集合不大,因为写时要复制数组,耗时,耗内存。

实时性要求不高,因为可能会读到旧数据。(对新数组写,对数组读)。

采用快照遍历,即遍历发起时形成一张当前数组的快照,并且迭代器不允许删除,新增元素。不会发生ConcurrentModificationException,但可能实时性不够。

适用于作为观察者的容器。

ArrayBlockingQueue

大小固定的,线程安全的顺序队列,不能读读,读写,写写并发。

使用Object[]作为容器,环形数组。比复制拷贝效率高。

存取使用同一把锁ReentrantLock保证线程安全+2个condition(写操作可能在notFull条件上阻塞,读操作可能在notEmpty上阻塞)

遍历支持remove及并发读写。

适用于控制内存大小的生产者消费者模式,队列满,则阻塞生产者/有限等待,队列空则阻塞消费者/有限等待。

适用于做缓存,缓存有大小限制,缓存是生产者消费者模型,多线程场景下需考虑线程安全。

LinkedBlockingQueue

线程安全的链队列,实现读写并发,不能读读,写写并发。

存取用两把不同的ReentrantLock,适用于读写高并发场景。

可实现并行存取,速度比ArrayBlockingQueue 快,但有额外的Node结点对象的创建和销毁,可能引发 gc,若消费速度远低于生产速度,则内存膨胀。

SynchronousQueue

以非阻塞线程安全的方式将元素从一个生产线程递交给消费线程。

适用于生产的内容总是可以被瞬间消费的场景,比如容量为Int.MAX_VALUE的线程池,即当新请求到来时,总是可以找到新得线程来执行请求,不管是老的空闲线程,还是新建线程。

存储结构是一个链,使用 cas + 自旋的方式实现线程安全。

PriorityBlockingQueue

使用Object[] 作为容器实现堆。

使用ReentrantLock保证线程安全,读和取同一把锁。

每次存取会引发排序,使用堆排序提高性能。

• 每次写,都写到数组末尾,然后堆向上调整。

• 每次读都读取数组头,并将数组末尾元素放到数组头。然后执行一次向下调整。

ConcurrentLinkedQueue

是一个链式队列,使用非阻塞方式实现并发读写的线程安全,而是使用轮询+CAS保证了修改头尾指针的线程安全。

存储结构是带头尾指针的单链表。

头尾指针和结点next域都使用 volatile 保证可见性。

出队时,通过 cas 保证结点 item 域置空的线程安全,更新头指针也使用了 cas。

入队时,通过 cas 保证结点 next 域指向新结点的线程安全,更新尾指针也使用了 cas。

出于性能考虑,头尾指针的更新都是延迟的。每插入两个结点,更新一下尾指针,每取出两个结点,更新一下头指针。

适用于生产者消费者场景。

入队算法:总是从当前tail指向的尾部向后寻找真正的尾部(因为tail更新滞后,并且可能被另一个入队线程抢占),找到后通过cas值next域。

ConcurrentHashMap

1.7 使用的是开散列表,数组+链表。

1.7 Segment 数组,Segment 是一个ReentrantLock,分段锁,并发数是 Segment 的数量。每个 Segment 持有一个 Entry 数组(桶)。(一个entry就是一条链)。

1.7 定位一个元素需要两次hash,先定位到 Segment 再定位到元素所在链表头。

1.7put先尝试非阻塞的获取锁,失败则自旋重试,并计算出对应桶的位置,到达最大尝试次数后阻塞式的获取。

1.7 ConcurrentHashMap.get不需要上锁是因为键值对实体中将value声明成了volatile,能够在线程之间保持可见性。

1.7 如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再 hash 后插入到新的数组里。

1.7 遍历链表是个相对耗时的操作。

1.8 将重入锁改成synchronized,因为它被优化过了。

1.8 也是开散列表,数组+链表(或者红黑树),当链表长度大于8时,则将链表转换为红黑树,增加查找效率。

1.8 使用cas方式保证只有一个线程初始化成功。

1.8 put操作:对key进行hash得到数组索引,若数组未初始化则初始化,如果索引位置为null 则直接cas写(失败则自旋保持成功),(后面的部分synchronize了)如果索引位置为链头指针,则进行链插入,往后遍历找到相同的key 则覆盖,否则链尾插入,若索引位置是红黑树,则进行红黑树插入。

1.8 锁的粒度更细了,一个桶一个锁。

1.8 Node.next 用volatile修饰。

红黑树

二叉树是一个父节点有两个子节点的递归结构。

二叉排序树是一种特殊的二叉树,它规定左孩子 < 父亲 < 右孩子,它解决了二叉树退化为单链表的情况(查找时间复杂度退化为O(n))。

平衡二叉排序树是一种特殊的二叉排序树。它规定每一个结点的左右子树高度差不大于1。

红黑树是没有那么严格的平衡二叉排序树。因为频繁的调整子树是耗时的。

二叉排序树是二分查找,最大查找次数为树高度。

红黑树插入结点后通过变色和旋转来保持红黑树的平衡。保证了没有任何一条路径会比其他路径长出两倍。

ConcurrentModificationException

当遍历数组的同时删除其中元素就会发生这个异常,这叫fast-fail机制。

因为调用next时会检查modCount和expectModCount是否一致,不一致则抛这个异常。

但单线程下如何解决这个问题:使用iterator.remove,他会同步modCount和expectModeCount。

ThreadPoolExecutor

这是java的线程池。

ThreadPoolExecutor构造参数如下:

1. 核心线程数:线程池中一直存活的线程数量,当新的任务请求到来时,如果线程池中线程数小于核心线程数,则会新建线程,默认情况下核心线程会一直存活,只有当allowCoreThreadTimeOut设置为true时且发生超时才会被销毁。

2. 最大线程数,线程池中线程的上限。

3. keepAlive:非核心线程允许空闲的最大时长,超过空闲时间则会被销毁(当池中线程数>=核心线程数时创建出来的线程都是非核心线程)。

ThreadPoolExecutor线程池管理策略:

if线程池中正在运行的线程数 < corePoolSize

{新建线程来处理任务(即使线程池中有线程处于空闲状态)}

elseif线程池中正在运行的线程数 >= corePoolSize

if缓冲队列未满

任务被放入缓冲队列

else缓冲队列满

ifmaximumPoolSize > 线程池中正在运行的线程数 > corePoolSize

新建线程来处理任务 此时的任务会被立即执行

elseif线程池中正在运行的线程数 = maximunPoolSize

通过handler所指定的策略来处理此任务

拒绝策略(丢弃策略)

ThreadPoolExecutor.AbortPolicy 悄悄地丢弃一个任务

ThreadPoolExecutor.DiscardOldestPolicy 丢弃最旧的任务,重新提交最新的

ThreadPoolExecutor.CallerRunsPolicy 在调用者的线程中执行被拒绝的任务

ThreadPoolExecutor.DiscardPolicy 丢弃当前任务

4

网络

网络分层的好处是下层的可重用性,tcp不需要知道它传输的是。

1. 物理层

二进制在物理媒体上传输。

2. 数据链路层

在物理层的基础上提供差错校验。

3. 网络层(ip)

为数据包路由。

4. 传输层(tcp,udp)

提供端到端接口。

tcp

传输控制协议,是传输层协议,解决数据如何传输,是面向连接的,可靠的点到点传输协议。

tcp头包括 sequence number(32位) 用于标识报文中第一个字节在整个数据流中的序号,确保有序。

tcp头包括 ack number(32位),表示对上一个接收到的sequence number的确认,解决丢包。只有当ack位为1时才有效。

tcp 头部包含滑动窗口大小。

tcp 头部包含 tcp flag,有6个标志位 URG,ACK,PSH,RST,SYN,FIN。

tcp 头部包含两个16位的端口号(源+目的)。

tcp是基于字节流的。

采用确认和超时重传策略保证可靠传输:

• 确认:接收方检测出帧出错是不会返回确认帧并直接丢弃该帧。

• 超时重传:发送方发送数据报后启动倒计时,若规定时间内未收到确认才重传数据报。

提供拥塞控制和流量控制:

• 采用大小可变的滑动窗口实现流量控制,窗口大小即是发送方发送但未收到确认的数据报数量。

• 慢启动:每个rtt将滑动窗口翻倍。

• 拥塞控制对链接是独立的。

• 但拥塞控制会导致tcp队头阻塞(tcp必须接收到完整正确顺序的数据包后才能提交给上层),使得单路 的快。

TCP通信过程太复杂并且开销大,一次TCP交换需要9个包:三个连接包,四个断开包,一个request包,一个响应包。

UDP通信过程简单,只需要一个查询包和一个响应包。

tcp三次握手建立连接

1. 发送方请求建立连接Syn报文,syn位置1(表示链接建立请求) ack位置0,seq number =x。

2. 接收方确认请求 syn位置1,ack位置1,seq number = y ack number = x+1。

3. 发送方确认的确认 ack number = y+1。

为啥不能两次:防止超时的连接请求报文到达服务器再次建立连接。

tcp四次挥手释放连接

4次挥手:发送方请求释放连接(Fin报文)-> 接收方确认(ACK置1)-> 接收方请求释放连接(Fin报文)-> 发送方确认-客户端等待 2MSL(报文最大生存时间) 的时间后依然没有收到回复(服务端没收到ack,则服务端会重新发送fin),则证明服务端已正常关闭,那么客户端也可以关闭连接了。

为啥挥手要四次,因为TCP全双工,客户端请求释放连接时,只表示客户端没东西发了,但服务器还有数据要返回。

tcp粘包,tcp分包

半包:如果数据包太大,导致服务器没有接收完整的包。

粘包:tcp基于字节流,不关心上层传输的具体内容,在一个tcp报文中可能存在多个为了提高效率,所以粘包,接收端粘包:接收端没有及时处理接收缓冲区的数据,读取时出现粘包)。

分包:tcp基于字节流,tcp不关心上层传输的具体内容,一个大的太大)。

粘包分包解决方案:定长消息,用特殊字符标记消息边界,将消息长度写在消息头中。

tcp心跳包

通信双方处于idle状态时确保长链接的有效性,需要发送的特殊数据包给对方(ping),接收方给予回复(pong)。

tcp自带心态机制SO_KEEPALIVE,但不够灵活,所以在应用层上实现心跳。

Netty 使用IdleStateHandler 根据超时时间监听读写事件,若发生超时则会触发回调,这个时候可以发送心跳包。

socket

套接字 = {传输层协议,源地址,源端口,目标地址,目标端口},其中协议可以是tcp或udp,是不同主机进程间通信的端点。

udp

用户数据包协议。

UDP提供的是无连接 无确认 不可靠服务的点到多点传输协议。

udp是基于报文的。

发送前无需握手,发送完无需释放连接,传输效率高。

每个数据包独立发送,不同数据包可能传输路径可能不同。

没有拥塞控制。

有差错校验,对udp头部和数据段都进行校验,服务端通过校验和发现出错时直接丢弃。

udp 依赖网络层的ip,udp数据包被包在ip数据包外层。

5. 应用层

)。

是应用层协议,解决如何封装数据。

无状态协议,服务器对用户操作没有记忆。

,请求头部的该字段决定了链接是否会复用)。

明文通信,可能被窃听;不验证身份,可能被劫持;无法验证报文完整性,可能被篡改。

端口。

链接上发送(发送方不需要等待第一个资源确认了才发送第二个资源)。但接收方只能串行的处理响应,一个慢响应会阻塞所有快请求向上层提交(管道解决了请求的队头阻塞)。

证书: 是服务器下发给客户端的,客户端用证书验证服务端身份。证书需要购买。

证书包含:认证机构(CA)信息,公钥,域名,有效期,指纹(对证书进行hash运算,即证书摘要),指纹算法,数字签名(CA私钥加密的指纹)等。

1.0 每个链接,结束时要关闭链接,临时链接。

1.0 不压缩header,且每次通信都要重复发送head。

1.0 不支持请求优先级。

1.0 必须串行的地完成地发送资源(造成队头阻塞)。

1.1 允许持久链接,接收方只能串行地处理不同请求,两个请求生命周期不能重叠,因为接收方无法确认数据的开始和结束(有效负荷字段写在header中),这会造成队头阻塞,多个并行请求需建立多条 tcp链接,无法复用。关闭链接只要在头部带上Connection:Close。

2.0 支持header压缩,通讯双方缓存一个 header field 表,避免重复 header 传输。

2.0 多路复用,将数据流分解成更小的帧(通过在头部廷加stream id,和帧大小),不同数据流的帧可以交错在一条tcp连接上发送,再根据所属流重新组装,实现了多请求并行传输的效果(时间片),解决了http层的队头阻塞(减轻了服务端的压力,每个客户端只建立了一条链接,服务器可以给更多的客户端建立连接)。

2.0 支持优先级。

加密解密

加密算法分为两类:对称加密和非对称加密。

• 对称加密:加密和解密用的都是相同的秘钥,优点是速度快,缺点是安全性低。常见的对称加密算法有DES、AES等等。

• 非对称加密:非对称加密有一个秘钥对,分为公钥和私钥。一般来说,私钥自己持有,公钥可以公开给对方,优点是安全性比对称加密高,缺点是数据传输效率比对称加密低。采用公钥加密的信息只有对应的私钥可以解密。常见的非对称加密包括RSA等。

数字摘要

是明文摘要成128位密文的过程,比如MD5,SHA1。

数字签名

是用于验证信息完整性的和身份验证。

发送方将内容摘要并用私钥加密并发送,接收方用公钥解密摘要,再对原文求摘要,比对两个摘要,若相同则未被篡改。

数字证书

是为了解决公钥置信的问题。

TLS

是 ssl3.0 的后续版本。

分为 tls记录和tls握手。

tls 实现了加密数据,验证数据完整性,认证身份。

tls握手过程

是一个借助于数字证书协商出对称加密密钥的过程。

• 客户端发出请求,说明支持的协议,客户端生成的随机数,支持的加密方法。

• 服务端返回证书,服务端生成的随机数。

• 客户端验证证书。

• 客户端使用证书中的公钥加密另一个新得随机数。并发送给服务器。

• 生成会话密钥:客户端和服务器分别用三个随机数生成相同的对称密钥。

• 服务器通知握手结束,之后就通过对称密钥通信。

验证过程:

• 客户端 TLS 解析证书。

• 证书是否过期。

• CA是否可靠(查询信任的本地根证书)。

• 证书是否被篡改(用户使用CA根公钥解密签名得到原始指纹,再对证书使用指纹算法得到新指纹,两指纹若不一样,则被篡改)。

• 服务器域名和证书上的域名是否匹配。

QUIC

quic建立在UDP之上,但实现了可靠传输,它更应是TCP 2.0,它包含tcp的所有特性 :可靠性,拥塞控制,流量控制。

quic 将 )。

队头阻塞

一个大的(慢的)响应会阻塞其后面的响应。

链接缓解该问题。

)。

会先将失序数据存在缓冲区,待重传数据到来时才按照正确的顺序提交给上层,此时丢失的包会阻塞后续包提交给上层。

quic 将队头阻塞。

tls队头阻塞:tls加解密是整块进行的,tls记录可能分散在多个tcp包上,若tcp丢包则tls队头阻塞,quic的解决方案是将加解密分散处理,这样会拖慢加解密速度。

一次网络请求

请求dns服务器解析ip地址。

三次握手建立TCP链接。

tls握手。

请求内容封装成分包 在链路上发送出去。

服务器解析报文响应。

关闭链接,四次握手。

网络优化

请求预热:发送无body的head请求,提前建立好tcp,tls链接,省掉dns,tcp,tls时间。

统一域:不同的业务的域名在客户端发出请求之前进行合并(因为若域名不同,请求不同业务时都需要dns解析,且都需要建立不同的tcp链接),使用统一的域,将请求不同的部分往后挪到接口部分,请求到达后端SLB后进行域名还原。ok)。

有了统一的域之后,可以进行网络嗅探,择优进行IP直连,app启动时,拉取域对应的ip列表,并对所有ip进行嗅探ping接口,选择其中最优的ip 最为后续请求的直连ip,不需要进行dns解析。

对于可靠性要求高的请求,先入库,失败后重试。

网络切换时,自动关闭缓存池中现有的链接(客户端网络地址发生变化,原先的链接失效)。

减少数据传输量,protocolBuffer,图片压缩,webp,请求合适大小的图片。

无网环境下, 添加强制缓存的拦截器,对请求添加cache-control:max-age:1年。

5

Ok

Retrofit

Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。

Retrofit将转换成业务数据。

使用建造者模式,构建retrofit实例。

使用工厂模式:Convert.Factory构建序列化/反序列化工厂,将ResponseBody转换成业务层数据,将请求转换成一个requestBody。

使用装饰者模式:通过装饰者模式将响应回调抛到主线程,真正发起请求的是Ok 扩展了该功能。

使用了外观模式,create,隐藏了动态代理生成接口实例,通过Call.Factory生成请求的细节。

Retrofit.crate将接口请求动态代理给了ServiceMethod的invoke方法(查找接口对应的ServiceMethod对象(没找到就当场使用反射遍历接口中的注解,并生成ServiceMethod对象对应一个业务接口,接口中的参数都会成为它的成员变量,存在ConcurrentHashMap中,键是Method)),在该方法中生成retrofit的call对象(内部会生成Ok方法))。

特点:

• 链接池,复用连接。

• 默认支持GZIP,告诉服务器支持gzip压缩格式,请求添加Accept-Encoding: gzip,响应中返回Content-Encoding: gzip(使用哈夫曼算法,重复度越高压缩效果越好)。

• 响应缓存。

• 方便添加拦截器。

Ok

在 Ok中构建。

维护三个队列和一个线程池来并发处理网络请求,分别是同步运行队列,正在运行的异步队列,等待请求异步队列。

持有 ExecutorService ,核心线程数为0,表示不保留空闲线程。最大线程数为 Int.max,表示随时会新建线程,使用同步队列,使得请求的生产不会被阻塞。

五大拦截器

0. 应用拦截器一定会被执行一次

1. RetryAndFollowUpInterceptor

重试重定向拦截器 这个拦截器是一个while(true)的循环,只有请求成功或者重试超过最大次数,没有路由供重试时才会退出。

请求抛出异常并满足重试条件时才重试,收到3xx,需要重定向时会重新构建请求。

2. BridgeInterceptor

将。

3. CacheInterceptor

缓存拦截器。

从DiskLruCache根据请求url获取缓存Response,然后根据一些http头约定的缓存策略决定是使用缓存响应还是发起新的请求。

只缓存 get 请求。

缓存是空间换时间的方法,缓存需要页面置换算法(LRU,FIFO)。

缓存减小服务器压力,客户端更快地显示数据,无网下显示数据。

缓存分为强制缓存和对比缓存:

1. 客户端直接拿数据,若缓存未命中则请求网络并更新数据。

2. 客户端拿数据标识,总是查询网络判断数据是否有。

响应头中包含Cache-Control:max-age,表示缓存过期时间。

响应头中有Last-Modified字段标识资源最后被修改时间,客户端下次发起请求时在请求头中会带上If-Modified-Since,服务器比对如果最后修改时间大于该值则返回200,否则304标识缓存有效。

除了用最后修改时间做判断,还可以用资源唯一标识来判断ETag/If-None-Match,响应头包含ETag,再次请求时带上If-None-Match,服务器比对标识是否相同,相同则304,否则200。

缓存策略:先判断缓存是否过期,若未过期则直接使用,若过期则发起请求,请求头带唯一标识,服务器回200或304,如果没有唯一标识则请求头带上次修改时间,服务器200或304。

在无网环境下即是缓存过期,依然使用缓存,要添加 应用拦截器,重构request修改cache-control字段为FORCE_CACHE:

publicclassForceCacheInterceptorimplementsInterceptor{

@Override

publicResponse intercept(Chain chain)throwsIOException {

Request.Builder builder = chain.request.newBuilder;

if(!NetworkUtils.internetAvailable) {

builder.cacheControl(CacheControl.FORCE_CACHE);

returnchain.proceed(builder.build);

ok);

若服务器不支持header头缓存字段,则可以添加网络拦截器,在CacheInterceptor收到响应之前修改response的header。

publicclassCacheInterceptorimplementsInterceptor{

@Override

publicResponse intercept(Chain chain)throwsIOException {

Request request = chain.request;

Response response = chain.proceed(request);

Response response1 = response.newBuilder

.removeHeader( "Pragma")

.removeHeader( "Cache-Control")

//cache for 30 days

.header( "Cache-Control", "max-age="+ 3600* 24* 30)

.build;

returnresponse1;

4. ConnectInterceptor

连接拦截器。

建立连接及连接上的流。

维护连接池,以复用连接。

一个物理链接上有多个流(逻辑上的请求响应对),一个物理链接上的多个流是并发的,但有数量限制,一个流上有多个分配,分配是并发的。

获取连接流程:

• 复用已分配的连接(重定向再次请求)。

• 无已分配链接,则从链接池那一个新得链接,通过主机名和端口(并不是池中的链接就能复用,除了host之外的字段要都相等,比如dns,协议,代理)。

• 尝试其他路由再从连接池中获取连接,若找到则进行dns查询。

• 如果缓存池中没有链接,则新建链接(tcp+tls握手,sockect.connect+connectTls),这是耗时的,过程中可能有连接池可能有新的可用连接 所以再次尝试从连接池获取连接,如果成功则释放刚建立的链接,否则把新建连接入池。

连接复用

tcp连接建立需要三次握手和四次挥手。

连接池实现链接缓存,实现同一地址的链接复用。

连接池以队列方式存储链接ArrayDeque,链接池中同一个地址最多维护5个空闲链接,空闲链接最多存活5分钟。

连接清理

五分钟定时任务,每五分钟遍历所有链接,并找到其中空闲时间最长的,如果空闲时间超过keep-alive(5分钟),或者空闲链接超过了阈值(5个)则清除这个链接。

5. NetworkInterceptor

网络拦截器。

在连接建立完成和发送请求之间。

可能不被调用,比如缓存命中,或者多次调用重定向。

6. CallServerInterceptor

请求拦截器。

将请求和响应分装成 )。

1 写入请求头 - 2 写入请求体 - 3 读取响应头 - 4 读取响应体 如果响应头中 Connection:close,则在当前链接上设置标志位,表示该链接不能再被复用。

RealCall

如何检测重复请求:使用一个AtomicBoolean 作为请求过的标志位,每次执行 execute之前就会检查。

如何发起请求:

1. 请求被封装成 RealCall 对象,异步请求会进一步会封装成一个 Runnable。

2. 同步请求直接将请求在拦截器责任链上传递(并加到同步请求队列汇总)。

3. 异步请求会缓存到一个准备请求队列中,并检查当前并发请求数(同一个域最多5个并发,不同域最多64个),若未超阈值,则将请求出队入线程池执行(将请求在责任链上传递) 同一链接上的最大并发数据流是Int.max。

请求如何在责任链上传递

责任链持有一组拦截器和当前拦截器索引,通过每次复制一条新责任链且索引+1,实现传递 发起请求并获取响应就是在请求和响应在责任链上u型传递的过程。

6

Glide

特点

会根据控件大小进行下采样,以解码出符合需求的大小,对内存更友好。

内存缓存+磁盘缓存。

感知生命周期,取消任务,防止内存泄漏。

感知内存吃紧,进行回收。

BitmapPool,防止内存抖动的进行bitmap变换。

定义请求优先级。

手写一个图片库注意事项

获取资源:异步并发下载图片,最大化利用cpu资源。

资源解码:按实际需求异步解码,多线程并发是否能加快解码速度。

资源变换:使用资源池,复用变换的资源,避免内存抖动。

缓存:磁盘缓存原始图片,或变换的资源。内存缓存刚使用过的资源,使用lru策略控制大小。

感知生命周期:避免内存泄漏。

感知内存吃紧:清理缓存。

Glide 数据加载流程

RequestBuilder 构建 Request和 Target,将请求委托给RequestManager,RequestManager触发Request.begin,然后调用Engine.load加载资源,若有内存缓存则返回,否则启动异步任务加载磁盘缓存,若无则从网络加载。

DecodeJob 负责加载数据(可能从磁盘,或网络,onDataFetcherReady),再进行数据解码(onDataFetcherReady),再进行数据变换(Transformation),写ActiveResource,(将变换后的数据回调给Target),将变换后的资源写文件(ResourceEncoder)。

预加载

preload,加载到一个PreloadTarget,等资源加载好了,就调用clear,将资源从ActiveResource移除存到Lrucache中。

感知内存吃紧

注册ComponentCallbacks2,实现细粒度内存管理:

1. onLowMemory{清除内存}

2. onTrimMemory{修剪内存}

memoryCache.trimMemory(level); // 内存缓存

bitmapPool.trimMemory(level); // bitmap池

arrayPool.trimMemory(level); // 字节数组池

可以设置在onTrimMemory时,取消所有正在进行的请求。

BitmapPool

BitmatPool是 Glide 维护了一个图片复用池,LruBitmapPool 使用 Lru 算法保留最近使用的尺寸的 Bitmap。

api19 后使用bitmap的字节数和config作为key,而之前使用宽高和congif,所以19以后复用度更高。

用类似LinkedHashMap存储,键值对中的值是一组Bitmap,相同字节数的Bitmap 存在一个List中(这样设计的目的是,将Lru策略运用在Bitmap大小上,而不是单个Bitmap上),控制BitmapPool大小通过删除数据组中最后一个Bitmap。

BitmapPool 大部分用于Bitmap变换和gif加载时。

ArrayPool

是一个采用Lru策略的数组池,用于解码时候的字节数组的复用。

清理内存意味着清理MemoryCache,BitmapPool,ArrayPool。

缓存

默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:

• 活动资源 (Active Resources)- 现在是否有另一个 View 正在展示这张图片?

• 内存缓存 (Memory cache)- 该图片是否最近被加载过并仍存在于内存中?

• 资源类型(Resource)- 该图片是否之前曾被解码、转换并写入过磁盘缓存?

• 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?

在 Glide v4 里,所有缓存键都包含至少两个元素 活动资源,内存缓存,资源磁盘缓存的缓存键还包含一些其他数据,包括:必选:Model 可选:签名 宽度和高度 可选的变换(Transformation) 额外添加的任何 选项(Options) 请求的数据类型 (Bitmap, GIF, 或其他)。

磁盘缓存策略

如果缓存策略是AUTOMATIC(默认),对于网络图片只缓存原始数据,加载本地资源是存储变换过的数据,如果加载不同尺寸的图片,则会获取原始缓存并在此基础上做变换。

如果缓存策略是ALL,会缓存原始图片以及每个尺寸的副本。

如果缓存策略是SOURCE,只会缓存变换过的资源,如果另一个界面换一个尺寸显示图片,则会重新拉取网络 可通过自定义Key实现操控缓存命中策略(混入自己的值,比如修改时间)。

内存缓存

内存缓存分为两级:

1. 活跃图片ActiveResource。

• 使用HashMap存储正在使用资源的弱引用。

• 资源被包装成带引用计数的 EngineResource,标记引用资源的次数(当引用数不为0时阻止被回收或降级,降级即是存储到LruCache中)。

• 这一级缓存没有大小限制,所以使用了资源的弱引用。

• 存:每当下载资源后会在 onEngineJobComplete中存入ActiveResource,或者LruCache命中后,将资源从中LruCache移除并存入ActiveResource。

• 取:每当资源释放时,会降级到 LruCache中(请求对应的context onDestroy了或者被gc了)。

• 开一个后台线程,监听 ReferenceQueue,不停地从中获取被gc的资源,将其从ActiveResource中移除,并重新构建一个新资源将其降级为LruCache。

• ActiveResource是为了缓解LruCache中缓存造成压力,因为LruCache中没有命中的缓存只有等到容量超限时才会被清除,强引用即使内存吃紧也不会被gc,现在当LruCache命中后移到ActiveResource,弱引用持有,当内存吃紧时能被回收。

2. LruCache

• 使用 LinkedHashMap 存储从活跃图片降级的资源,使用Lru算法淘汰最近最少使用的。

• 存:从活跃图片降级的资源(退出当前界面,或者 ActiveResource资源被回收)。

• 取:网络请求资源之前,从缓存中取,若命中则直接从 LruCache中移除了。

内存缓存只会缓存经过转换后的图片。

内存缓存键根据10多个参数生成,url,宽高。

磁盘缓存

会将源数据或经过变换的数据存储在磁盘,在内存中用LinkedHashMap记录一组Entry,Entry内部包含一组文件,文件名即是key,并且有开启后台线程执行删除文件操作以控制磁盘缓存大小。

写磁盘缓存即是触发Writer将数据写入磁盘,并在内存构建对应的File缓存在LinkedHashMap中。

根据缓存策略的不同,可能存储源数据和经过变换的数据。

感知生命周期

构造RequestManager时传入context,可以是app的,activity的,或者是view的。

向界面添加无界面Fragment(SupportRequestManagerFragment),Fragment把生命周期传递给Lifecycle,Fragment持有RequestManager,RequestManager监听Lifecycle,RequestManager向RequestTracker传递生命周期以暂停加载,RequestTracker遍历所有正在进行的请求,并暂停他们(移除回调resourceReady回调)。

当绑定context destroy时,RequestManager会将该事件传递给RequestTracker,然后触发该请求Resource的clear,再调用Engine.release,将resource降级到LruCache。

通过HashMap结构保存无界面Fragment以避免重复创建。

取消请求

通过移除回调,设置取消标志位实现:无法取消已经发出的请求,会在DecodeJob的异步任务的run方法中判断,如果cancel,则返回。移除各种回调,会传递到DataFetcher,。并没有断开链接。

感知网络变化

通过ConnectivityManager 监听网络变化,当网络恢复时,遍历请求列表,将没有完成的任务继续开始。

Transformation

所有的BitmapTransformation 都是从BitmapPool 拿到一个bitmap,然后将在原有bitmap基础上应用一个matrix再画到新bitmap上。

变换也是一个key,用以在缓存的时候做区别。

RecycleView图片错乱

异步任务+视图复用导致。

解决方案:设置占位图+回收表项时取消图片加载(或者新得加载开始时取消旧的加载)+imageview加tag判断是否是自己的图片如果不是则先调用clear。

Glide 缓存失效

是因为 Key 发生变化,Url是生成key的依据,Url可能发生变化比如把token追加在后面。

自定义生成key的方式,继承GlideUrl重写getCacheKey。

自定义加载

定义一个Model类用于包装需要加载的数据。

定义一个Key的实现类,用于实现第一步的Model中的数据的签名用于区分缓存。

定义一个DataFetcher的实现类,用于告诉Glide音频封面如何加载,并把加载结果回调出去。

定义一个ModelLoader的实现类用于包装DataFetcher。

定义一个ModelLoaderFactory的实现类用于生成ModelLoader实例。

将自定义加载配置到AppGlideModule中。

Glide线程池

磁盘缓存线程池,一个核心线程:用于io图片编码。

加载资源线程池,最多不超过4个核心线程数,用于处理网络请求,图片解码转码。

动画线程池,最多不超过2个线程。

磁盘缓存清理线程池。

ActiveResource开启一个后台线程监听ReferenceQueue所有线程池都默认采用优先级队列。

加载Gif流程

读取流的前三个字节,若判断是gif,则会命中gif解码器-将资源解码成GifDrawable,它持有GifFrameLoader会将资源解码成一张张Bitmap并且传递给DelayTarget的对象,该对象每次资源加载完毕都会通过handler发送延迟消息回调 onFrameReady 以触发GifDrawable.invalidataSelf重绘。加载下一帧时会重新构建DelayTarget。

请求优先级

通过给加载线程池配置优先级队列,加载任务DecodeJob 实现了compareTo方法,将priority相减。

图片加载优化

服务器存多种尺寸的图片。

自定义AppGlideModule,按设备性能好坏设定MemoryCategory.HIGH,LOW,NORMAL,内存缓存和bitmapPool的大小系数,以及图片解码格式,ARGB_8888,RGB_565。

RecyclerView在onViewRecycled中调用clear ,因为recyclerView会默认会缓存5个同类表项,如果类型很多,内存中会持有表项,如果这些表项都包含图片,Glide 的ActiveResource会膨胀。导致gc。

如果RecyclerView 包含一个很长的itemView,超过一屏,其中包含很多照片,最好把长itemView拆成多个itemView。

使用thumbnail,加载一个缩略图,最好是一个独立的链接,如果是本地的也不差。

使用preload,将资源提前加载到内存中。

大部分情况下RESOURCE ,即缓存经过变换的图片上是最好选择,节约内存和磁盘。对于gif资源只缓存原始资源DATA,因为gif是多张图每次编码解码反而耗时。

使用Glide实现变换,因为有BitmapPool供复用。

最后推荐一下我做的网站,玩Android: wanandroid.com,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!

Android 写业务不用架构会怎么样?

Android13适配,看这篇就够了!

Android阴影实现的几种方案

点击关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

┏(^0^)┛明天见!

发表评论

火币交易所(huobi) | 火币全球站官网入口 备案号:川ICP备66666666号