Android 高级面试题(三)

1、你项目中用到哪些开源库?说说其实现原理?

这个库是做什么用的?

网络底层库,它是基于http协议封装的一套请求客户端,虽然它也可以开线程,但根本上它更偏向真正的请求,跟HttpClient, HttpUrlConnection的职责是一样的。其中封装了网络请求get、post等底层操作的实现。

为什么要在项目中使用这个库?
  • OkHttp 提供了对最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接。
  • 如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率。
  • OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。
  • OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。
  • 当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址。
这个库都有哪些用法?对应什么样的使用场景?

get、post请求、上传文件、上传表单等等。

这个库的优缺点是什么,跟同类型库的比较?
  • 优点:在上面
  • 缺点:使用的时候仍然需要自己再做一层封装。
这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

OkHttp内部的请求流程:使用OkHttp会在请求的时候初始化一个Call的实例,然后执行它的execute()方法或enqueue()方法,内部最后都会执行到getResponseWithInterceptorChain()方法,这个方法里面通过拦截器组成的责任链,依次经过用户自定义普通拦截器、重试拦截器、桥接拦截器、缓存拦截器、连接拦截器和用户自定义网络拦截器以及访问服务器拦截器等拦截处理过程,来获取到一个响应并交给用户。其中,除了OKHttp的内部请求流程这点之外,缓存和连接这两部分内容也是两个很重要的点,掌握了这3点就说明你理解了OkHttp。

各个拦截器的作用:
  • interceptors:用户自定义拦截器
  • retryAndFollowUpInterceptor:负责失败重试以及重定向
  • BridgeInterceptor:请求时,对必要的Header进行一些添加,接收响应时,移除必要的Header
  • CacheInterceptor:负责读取缓存直接返回(根据请求的信息和缓存的响应的信息来判断是否存在缓存可用)、更新缓存
  • ConnectInterceptor:负责和服务器建立连接
  • ConnectionPool:
    • 判断连接是否可用,不可用则从ConnectionPool获取连接,ConnectionPool无连接,创建新连接,握手,放入ConnectionPool。
    • 它是一个Deque,add添加Connection,使用线程池负责定时清理缓存。
    • 使用连接复用省去了进行 TCP 和 TLS 握手的一个过程。
      • networkInterceptors:用户定义网络拦截器
      • CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据
你从这个库中学到什么有价值的或者说可借鉴的设计思想?

使用责任链模式实现拦截器的分层设计,每一个拦截器对应一个功能,充分实现了功能解耦,易维护。

手写拦截器?
OKhttp针对网络层有哪些优化?
网络请求缓存处理,okhttp如何处理网络缓存的?
HttpUrlConnection 和 okhttp关系?
Volley与OkHttp的对比:

Volley:支持HTTPS。缓存、异步请求,不支持同步请求。协议类型是Http/1.0, Http/1.1,网络传输使用的是 HttpUrlConnection/HttpClient,数据读写使用的IO。 OkHttp:支持HTTPS。缓存、异步请求、同步请求。协议类型是Http/1.0, Http/1.1, SPDY, Http/2.0, WebSocket,网络传输使用的是封装的Socket,数据读写使用的NIO(Okio)。 SPDY协议类似于HTTP,但旨在缩短网页的加载时间和提高安全性。SPDY协议通过压缩、多路复用和优先级来缩短加载时间。

Okhttp的子系统层级结构图如下所示:

网络配置层:利用Builder模式配置各种参数,例如:超时时间、拦截器等,这些参数都会由Okhttp分发给各个需要的子系统。 重定向层:负责重定向。 Header拼接层:负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。 HTTP缓存层:负责读取缓存以及更新缓存。 连接层:连接层是一个比较复杂的层级,它实现了网络协议、内部的拦截器、安全性认证,连接与连接池等功能,但这一层还没有发起真正的连接,它只是做了连接器一些参数的处理。 数据响应层:负责从服务器读取响应的数据。 在整个Okhttp的系统中,我们还要理解以下几个关键角色:

OkHttpClient:通信的客户端,用来统一管理发起请求与解析响应。 Call:Call是一个接口,它是HTTP请求的抽象描述,具体实现类是RealCall,它由CallFactory创建。 Request:请求,封装请求的具体信息,例如:url、header等。 RequestBody:请求体,用来提交流、表单等请求信息。 Response:HTTP请求的响应,获取响应信息,例如:响应header等。 ResponseBody:HTTP请求的响应体,被读取一次以后就会关闭,所以我们重复调用responseBody.string()获取请求结果是会报错的。 Interceptor:Interceptor是请求拦截器,负责拦截并处理请求,它将网络请求、缓存、透明压缩等功能都统一起来,每个功能都是一个Interceptor,所有的Interceptor最 终连接成一个Interceptor.Chain。典型的责任链模式实现。 StreamAllocation:用来控制Connections与Streas的资源分配与释放。 RouteSelector:选择路线与自动重连。 RouteDatabase:记录连接失败的Route黑名单。

自己去设计网络请求框架,怎么做?
从网络加载一个10M的图片,说下注意事项?
http怎么知道文件过大是否传输完毕的响应?
谈谈你对WebSocket的理解?
WebSocket与socket的区别?
这个库是做什么用的?

Retrofit 是一个 RESTFul 的 HTTP 网络请求框架的封装。Retrofit 2.0 开始内置 OkHttp,前者专注于接口的封装,后者专注于网络请求的高效。

为什么要在项目中使用这个库?
  • 功能强大:
    • 支持同步、异步
    • 支持多种数据的解析 & 序列化格式
    • 支持RxJava
  • 简洁易用:
    • 通过注解配置网络请求参数
    • 采用大量设计模式简化使用
  • 可扩展性好:
    • 功能模块高度封装
    • 解耦彻底,如自定义Converters
这个库都有哪些用法?对应什么样的使用场景?

任何网络场景都应该优先选择,特别是后台API遵循RESTFul API设计风格 & 项目中使用到RxJava。

这个库的优缺点是什么,跟同类型库的比较?
  • 优点:在上面
  • 缺点:扩展性差,高度封装所带来的必然后果,如果服务器不能给出统一的API形式,会很难处理。
这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

Retrofit主要是在create方法中采用动态代理模式(通过访问代理对象的方式来间接访问目标对象)实现接口方法,这个过程构建了一个ServiceMethod对象,根据方法注解获取请求方式,参数类型和参数注解拼接请求的链接,当一切都准备好之后会把数据添加到Retrofit的RequestBuilder中。然后当我们主动发起网络请求的时候会调用okhttp发起网络请求,okhttp的配置包括请求方式,URL等在Retrofit的RequestBuilder的build()方法中实现,并发起真正的网络请求。

你从这个库中学到什么有价值的或者说可借鉴的设计思想?

内部使用了优秀的架构设计和大量的设计模式,在我分析过Retrofit最新版的源码和大量优秀的Retrofit源码分析文章后,我发现,要想真正理解Retrofit内部的核心源码流程和设计思想,首先,需要对它使用到的九大设计模式有一定的了解,下面我简单说一说:

  • 创建Retrofit实例:
    • 使用建造者模式通过内部Builder类建立了一个Retroift实例。
    • 网络请求工厂使用了工厂方法模式。
  • 创建网络请求接口的实例:
    • 首先,使用外观模式统一调用创建网络请求接口实例和网络请求参数配置的方法。
    • 然后,使用动态代理动态地去创建网络请求接口实例。
    • 接着,使用了建造者模式 & 单例模式创建了serviceMethod对象。
    • 再者,使用了策略模式对serviceMethod对象进行网络请求参数配置,即通过解析网络请求接口方法的参数、返回值和注解类型,从Retrofit对象中获取对应的网络的url地址、网络请求执行器、网络请求适配器和数据转换器。
    • 最后,使用了装饰者模式ExecuteCallBack为serviceMethod对象加入线程切换的操作,便于接受数据后通过Handler从子线程切换到主线程从而对返回数据结果进行处理。
  • 发送网络请求:
    • 在异步请求时,通过静态delegate代理对网络请求接口的方法中的每个参数使用对应的ParameterHanlder进行解析。
  • 解析数据
  • 切换线程:使用了适配器模式通过检测不同的Platform使用不同的回调执行器,然后使用回调执行器切换线程,这里同样是使用了装饰模式。
  • 处理结果
Android:主流网络请求开源库的对比(Android-Async-Http、Volley、OkHttp、Retrofit)
RxJava 变换操作符 map flatMap concatMap buffer?
  • map:【数据类型转换】将被观察者发送的事件转换为另一种类型的事件。
  • flatMap:【化解循环嵌套和接口嵌套】将被观察者发送的事件序列进行拆分 & 转换 后合并成一个新的事件序列,最后再进行发送。
  • concatMap:【有序】与 flatMap 的 区别在于,拆分 & 重新合并生成的事件序列 的顺序与被观察者旧序列生产的顺序一致。
  • buffer:定期从被观察者发送的事件中获取一定数量的事件并放到缓存区中,然后把这些数据集合打包发射。
手写rxjava遍历数组。
你认为Rxjava的线程池与你们自己实现任务管理框架有什么区别?
这个库是做什么用的?

Glide是Android中的一个图片加载库,用于实现图片加载。

为什么要在项目中使用这个库?
  1. 多样化媒体加载:不仅可以进行图片缓存,还支持Gif、WebP、缩略图,甚至是Video。
  2. 通过设置绑定生命周期:可以使加载图片的生命周期动态管理起来。
  3. 高效的缓存策略:支持内存、Disk缓存,并且Picasso只会缓存原始尺寸的图片,内Glide缓存的是多种规格,也就是Glide会根据你ImageView的大小来缓存相应大小的图片尺寸。
  4. 内存开销小:默认的Bitmap格式是RGB_565格式,而Picasso默认的是ARGB_8888格式,内存开销小一半。
这个库都有哪些用法?对应什么样的使用场景?
  1. 图片加载:Glide.with(this).load(imageUrl).override(800, 800).placeholder().error().animate().into()。
  2. 多样式媒体加载:asBitamp、asGif。
  3. 生命周期集成。
  4. 可以配置磁盘缓存策略ALL、NONE、SOURCE、RESULT。
这个库的优缺点是什么,跟同类型库的比较?

库比较大,源码实现复杂。

这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?
  • Glide&with:
    • 初始化各式各样的配置信息(包括缓存,请求线程池,大小,图片格式等等)以及glide对象。
    • 将glide请求和application/SupportFragment/Fragment的生命周期绑定在一块。
  • Glide&load:
    • 设置请求url,并记录url已设置的状态。
  • Glide&into:
    • 首先根据转码类transcodeClass类型返回不同的ImageViewTarget:BitmapImageViewTarget、DrawableImageViewTarget。
    • 递归建立缩略图请求,没有缩略图请求,则直接进行正常请求。
    • 如果没指定宽高,会根据ImageView的宽高计算出图片宽高,最终执行到onSizeReay()方法中的engine.load()方法。
    • engine是一个负责加载和管理缓存资源的类
      • 常规三级缓存的流程:强引用->软引用->硬盘缓存
      • 当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找(软引用适合当cache,当内存吃紧的时候才会被回收。而weakReference在每次system.gc()就会被回收)(当LruCache存储紧张时,会把最近最少使用的数据放到SoftReference中),如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去硬盘缓存中中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到硬盘缓存中,然后放到LruCache中。
  • Glide的三层缓存机制:
    • Glide缓存机制大致分为三层:内存缓存、弱引用缓存、磁盘缓存。
    • 取的顺序是:内存、弱引用、磁盘。
    • 存的顺序是:弱引用、内存、磁盘。

三层存储的机制在Engine中实现的。先说下Engine是什么?Engine这一层负责加载时做管理内存缓存的逻辑。持有MemoryCache、Map>>。通过load()来加载图片,加载前后会做内存存储的逻辑。如果内存缓存中没有,那么才会使用EngineJob这一层来进行异步获取硬盘资源或网络资源。EngineJob类似一个异步线程或observable。Engine是一个全局唯一的,通过Glide.getEngine()来获取。

需要一个图片资源,如果Lrucache中有相应的资源图片,那么就返回,同时从Lrucache中清除,放到activeResources中。activeResources map是盛放正在使用的资源,以弱引用的形式存在。同时资源内部有被引用的记录。如果资源没有引用记录了,那么再放回Lrucache中,同时从activeResources中清除。如果Lrucache中没有,就从activeResources中找,找到后相应资源引用加1。如果Lrucache和activeResources中没有,那么进行资源异步请求(网络/diskLrucache),请求成功后,资源放到diskLrucache和activeResources中。

Glide源码机制的核心思想:

使用一个弱引用map activeResources来盛放项目中正在使用的资源。Lrucache中不含有正在使用的资源。资源内部有个计数器来显示自己是不是还有被引用的情况,把正在使用的资源和没有被使用的资源分开有什么好处呢??因为当Lrucache需要移除一个缓存时,会调用resource.recycle()方法。注意到该方法上面注释写着只有没有任何consumer引用该资源的时候才可以调用这个方法。那么为什么调用resource.recycle()方法需要保证该资源没有任何consumer引用呢?glide中resource定义的recycle()要做的事情是把这个不用的资源(假设是bitmap或drawable)放到bitmapPool中。bitmapPool是一个bitmap回收再利用的库,在做transform的时候会从这个bitmapPool中拿一个bitmap进行再利用。这样就避免了重新创建bitmap,减少了内存的开支。而既然bitmapPool中的bitmap会被重复利用,那么肯定要保证回收该资源的时候(即调用资源的recycle()时),要保证该资源真的没有外界引用了。这也是为什么glide花费那么多逻辑来保证Lrucache中的资源没有外界引用的原因。

你从这个库中学到什么有价值的或者说可借鉴的设计思想?

Glide的高效的三层缓存机制,如上。

Glide如何确定图片加载完毕?
Glide使用什么缓存?
Glide内存缓存如何控制大小?
计算一张图片的大小

图片占用内存的计算公式:图片高度 * 图片宽度 * 一个像素占用的内存大小。所以,计算图片占用内存大小的时候,要考虑图片所在的目录跟设备密度,这两个因素其实影响的是图片的宽高,Android会对图片进行拉升跟压缩。

加载bitmap过程(怎样保证不产生内存溢出)

由于Android对图片使用内存有限制,若是加载几兆的大图片便内存溢出。Bitmap会将图片的所有像素(即长x宽)加载到内存中,如果图片分辨率过大,会直接导致内存OOM,只有在BitmapFactory加载图片时使用BitmapFactory.Options对相关参数进行配置来减少加载的像素。

BitmapFactory.Options相关参数详解:

  1. Options.inPreferredConfig值来降低内存消耗。
    1. 比如:默认值ARGB_8888改为RGB_565,节约一半内存。
  2. 设置Options.inSampleSize 缩放比例,对大图片进行压缩 。
  3. 设置Options.inPurgeable和inInputShareable:让系统能及时回收内存。
    A:inPurgeable:设置为True时,表示系统内存不足时可以被回收,设置为False时,表示不能被回收。
    B:inInputShareable:设置是否深拷贝,与inPurgeable结合使用,inPurgeable为false时,该参数无意义。
    
  4. 使用decodeStream代替decodeResource等其他方法。

Android中软引用与弱引用的应用场景。

Android 应用的开发中,为了防止内存溢出,在处理一些占用内存大而且生命周期较长的对象时候,可以尽量应用软引用和弱引用技术。

  • 1、软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软 / 弱引用。
  • 2、如果只是想避免 OOM 异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
  • 3、可以根据对象是否经常使用来判断选择软引用还是弱引用。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
Android里的内存缓存和磁盘缓存是怎么实现的。

内存缓存基于LruCache实现,磁盘缓存基于DiskLruCache实现。这两个类都基于Lru算法和LinkedHashMap来实现。

LRU算法可以用一句话来描述:LRU是Least Recently Used的缩写,最近最少使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被访问到的可能性也很小,则这类数据项会被优先淘汰掉。

LruCache原理

之前,我们会使用内存缓存技术实现,也就是软引用或弱引用,在Android 2.3(APILevel 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。

其实LRU缓存的实现类似于一个特殊的栈,把访问过的元素放置到栈顶(若栈中存在,则更新至栈顶;若栈中不存在则直接入栈),然后如果栈中元素数量超过限定值,则删除栈底元素(即最近最少使用的元素)。

它的内部存在一个 LinkedHashMap 和 maxSize,把最近使用的对象用强引用存储在 LinkedHashMap 中,给出来 put 和 get 方法,每次 put 图片时计算缓存中所有图片的总大小,跟 maxSize 进行比较,大于 maxSize,就将最久添加的图片移除,反之小于 maxSize 就添加进来。

LruCache的原理就是利用LinkedHashMap持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。

详细来说就是LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队头元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队尾。

LruCache put方法核心逻辑

在添加过缓存对象后,调用trimToSize()方法,来判断缓存是否已满,如果满了就要删除近期最少使用的对象。trimToSize()方法不断地删除LinkedHashMap中队头的元素,即近期最少访问的,直到缓存大小小于最大值(maxSize)。

LruCache get方法核心逻辑

当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,保持整个队列是按照访问顺序排序的。

为什么会选择LinkedHashMap呢?

这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。

LinkedHashMap原理

LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry,并添加两个属性 Entry before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。

DisLruCache原理

DiskLruCache与LruCache原理相似,只是多了一个journal文件来做磁盘文件的管理,如下所示:

libcore.io.DiskLruCache
1
1
1
DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519

注:这里的缓存目录是应用的缓存目录/data/data/pckagename/cache,未root的手机可以通过以下命令进入到该目录中或者将该目录整体拷贝出来:

//进入/data/data/pckagename/cache目录
adb shell
run-as com.your.packagename 
cp /data/data/com.your.packagename/
//将/data/data/pckagename目录拷贝出来
adb backup -noapk com.your.packagename

我们来分析下这个文件的内容:

第一行:libcore.io.DiskLruCache,固定字符串。 第二行:1,DiskLruCache源码版本号。 第三行:1,App的版本号,通过open()方法传入进去的。 第四行:1,每个key对应几个文件,一般为1. 第五行:空行 第六行及后续行:缓存操作记录。 第六行及后续行表示缓存操作记录,关于操作记录,我们需要了解以下三点:

DIRTY 表示一个entry正在被写入。写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。注意单独只有DIRTY状态的记录是非法的。 当手动调用remove(key)方法的时候也会写入一条REMOVE记录。 READ就是说明有一次读取的记录。 CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字。

Bitmap 压缩策略

加载 Bitmap 的方式:

  • BitmapFactory 四类方法:
    • decodeFile( 文件系统 )
    • decodeResourece( 资源 )
    • decodeStream( 输入流 )
    • decodeByteArray( 字节数 )
  • BitmapFactory.options 参数:
    • inSampleSize 采样率,对图片高和宽进行缩放,以最小比进行缩放(一般取值为 2 的指数)。通常是根据图片宽高实际的大小/需要的宽高大小,分别计算出宽和高的缩放比。但应该取其中最小的缩放比,避免缩放图片太小,到达指定控件中不能铺满,需要拉伸从而导致模糊。
    • inJustDecodeBounds 获取图片的宽高信息,交给 inSampleSize 参数选择缩放比。通过 inJustDecodeBounds = true,然后加载图片就可以实现只解析图片的宽高信息,并不会真正的加载图片,所以这个操作是轻量级的。当获取了宽高信息,计算出缩放比后,然后在将 inJustDecodeBounds = false,再重新加载图片,就可以加载缩放后的图片。
  • 高效加载 Bitmap 的流程:
    • 将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 true 并加载图片
    • 从 BitmapFactory.Options 中取出图片原始的宽高信息,对应于 outWidth 和 outHeight 参数
    • 根据采样率规则并结合目标 view 的大小计算出采样率 inSampleSize
    • 将 BitmapFactory.Options 的 inJustDecodeBounds 设置为 false 重新加载图片
Bitmap的处理:

当使用ImageView的时候,可能图片的像素大于ImageView,此时就可以通过BitmapFactory.Option来对图片进行压缩,inSampleSize表示缩小 \( 2^{(inSampleSize-1)} \)倍。BitMap的缓存:

  1. 使用LruCache进行内存缓存。
  2. 使用DiskLruCache进行硬盘缓存。
实现一个ImageLoader的流程

同步异步加载、图片压缩、内存硬盘缓存、网络拉取

  • 1.同步加载只创建一个线程然后按照顺序进行图片加载
  • 2.异步加载使用线程池,让存在的加载任务都处于不同线程
  • 3.为了不开启过多的异步任务,只在列表静止的时候开启图片加载

具体为:

  • 1、ImageLoader作为一个单例,提供了加载图片到指定控件的方法:直接从内存缓存中获取对象,如果没有则用一个ThreadPoolExecutor去执行Runnable任务来加载图片。ThreadPoolExecutor的创建需要指定核心线程数CPU数+1,最大线程数CPU数*2+1,线程闲置超市时长10s,这几个关键数据,还可以加入ThreadFactory参数来创建定制化的线程。
  • 2、ImageLoader的具体实现loadBitmap:先从内存缓存LruCache中加载,如果为空再从磁盘缓存中加载,加载成功后记得存入内存缓存,如果为空则从网络中直接下载输出流到磁盘缓存,然后再从磁盘中加载,如果为空并且磁盘缓存没有被创建的话,直接通过BitmapFactory的decodeStream获取网络请求的输入流获取Bitmap对象。
  • 3、v4包的LruCache可以兼容到2.2版本,LruCache采用LinkedHashMap存储缓存对象。创建对象只需要提供缓存容量并重写sizeOf方法:作用是计算缓存对象的大小。有时需要重写entryRemoved方法,用于回收一些资源。
  • 4、DiskLruCache通过open方法创建,设置缓存路径,缓存容量。缓存添加通过Editor对象创建输出流,下载资源到输出流完成后,commit,如果失败则abort撤回。然后刷新磁盘缓存。缓存查找通过Snapshot对象获取输入流,获取FileDescriptor,通过FileDescriptor解析出Bitmap对象。
  • 5、列表中需要加载图片的时候,当列表在滑动中不进行图片加载,当滑动停止后再去加载图片。
Bitmap在decode的时候申请的内存如何复用,释放时机
图片库对比
Fresco与Glide的对比:

Glide:相对轻量级,用法简单优雅,支持Gif动态图,适合用在那些对图片依赖不大的App中。 Fresco:采用匿名共享内存来保存图片,也就是Native堆,有效的的避免了OOM,功能强大,但是库体积过大,适合用在对图片依赖比较大的App中。

Fresco的整体架构如下图所示:

DraweeView:继承于ImageView,只是简单的读取XML文件的一些属性值和做一些初始化的工作,图层管理交由Hierarchy负责,图层数据获取交由负责。 DraweeHierarchy:由多层Drawable组成,每层Drawable提供某种功能(例如:缩放、圆角)。 DraweeController:控制数据的获取与图片加载,向pipeline发出请求,并接收相应事件,并根据不同事件控制Hierarchy,从DraweeView接收用户的事件,然后执行取消网络请求、回收资源等操作。 DraweeHolder:统筹管理Hierarchy与DraweeHolder。 ImagePipeline:Fresco的核心模块,用来以各种方式(内存、磁盘、网络等)获取图像。 Producer/Consumer:Producer也有很多种,它用来完成网络数据获取,缓存数据获取、图片解码等多种工作,它产生的结果由Consumer进行消费。 IO/Data:这一层便是数据层了,负责实现内存缓存、磁盘缓存、网络缓存和其他IO相关的功能。 纵观整个Fresco的架构,DraweeView是门面,和用户进行交互,DraweeHierarchy是视图层级,管理图层,DraweeController是控制器,管理数据。它们构成了整个Fresco框架的三驾马车。当然还有我们 幕后英雄Producer,所有的脏活累活都是它干的,最佳劳模👍

理解了Fresco整体的架构,我们还有了解在这套矿建里发挥重要作用的几个关键角色,如下所示:

Supplier:提供一种特定类型的对象,Fresco里有很多以Supplier结尾的类都实现了这个接口。 SimpleDraweeView:这个我们就很熟悉了,它接收一个URL,然后调用Controller去加载图片。该类继承于GenericDraweeView,GenericDraweeView又继承于DraweeView,DraweeView是Fresco的顶层View类。 PipelineDraweeController:负责图片数据的获取与加载,它继承于AbstractDraweeController,由PipelineDraweeControllerBuilder构建而来。AbstractDraweeController实现了DraweeController接口,DraweeController 是Fresco的数据大管家,所以的图片数据的处理都是由它来完成的。 GenericDraweeHierarchy:负责SimpleDraweeView上的图层管理,由多层Drawable组成,每层Drawable提供某种功能(例如:缩放、圆角),该类由GenericDraweeHierarchyBuilder进行构建,该构建器 将placeholderImage、retryImage、failureImage、progressBarImage、background、overlays与pressedStateOverlay等 XML文件或者Java代码里设置的属性信息都传入GenericDraweeHierarchy中,由GenericDraweeHierarchy进行处理。 DraweeHolder:该类是一个Holder类,和SimpleDraweeView关联在一起,DraweeView是通过DraweeHolder来统一管理的。而DraweeHolder又是用来统一管理相关的Hierarchy与Controller DataSource:类似于Java里的Futures,代表数据的来源,和Futures不同,它可以有多个result。 DataSubscriber:接收DataSource返回的结果。 ImagePipeline:用来调取获取图片的接口。 Producer:加载与处理图片,它有多种实现,例如:NetworkFetcherProducer,LocalAssetFetcherProducer,LocalFileFetchProducer。从这些类的名字我们就可以知道它们是干什么的。 Producer由ProducerFactory这个工厂类构建的,而且所有的Producer都是像Java的IO流那样,可以一层嵌套一层,最终只得到一个结果,这是一个很精巧的设计👍 Consumer:用来接收Producer产生的结果,它与Producer组成了生产者与消费者模式。 注:Fresco源码里的类的名字都比较长,但是都是按照一定的命令规律来的,例如:以Supplier结尾的类都实现了Supplier接口,它可以提供某一个类型的对象(factory, generator, builder, closure等)。 以Builder结尾的当然就是以构造者模式创建对象的类。

Bitmap如何处理大图,如一张30M的大图,如何预防OOM?

使用BitmapRegionDecoder动态加载图片的显示区域。

Bitmap对象的理解。
对inBitmap的理解。
自己去实现图片库,怎么做?(对扩展开发,对修改封闭,同时又保持独立性,参考Android源码设计模式解析实战的图片加载库案例即可)
写个图片浏览器,说出你的思路?
这个库是做什么用?

内存泄露检测框架。

为什么要在项目中使用这个库?
  • 针对Android Activity组件完全自动化的内存泄漏检查,在最新的版本中,还加入了android.app.fragment的组件自动化的内存泄漏检测。
  • 易用集成,使用成本低。
  • 友好的界面展示和通知。
这个库都有哪些用法?对应什么样的使用场景?

直接从application中拿到全局的 refWatcher 对象,在Fragment或其他组件的销毁回调中使用refWatcher.watch(this)检测是否发生内存泄漏。

这个库的优缺点是什么,跟同类型库的比较?

检测结果并不是特别的准确,因为内存的释放和对象的生命周期有关也和GC的调度有关。

这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

主要分为如下7个步骤:

  • 1、RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
  • 2、然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
  • 3、如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
  • 4、HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
  • 5、从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
  • 6、HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
  • 7、这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。

简单来说就是:在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存(主要就是创建一个HprofParser解析器去解析出对应的引用内存快照文件snapshot)。

源码分析中一些核心分析点:

  • AndroidExcludedRefs:它是一个enum类,它声明了Android SDK和厂商定制的SDK中存在的内存泄露的case,根据AndroidExcludedRefs这个类的类名就可看出这些case都会被Leakcanary的监测过滤掉。buildAndInstall()(即install方法)这个方法应该仅仅只调用一次。
  • debuggerControl : 判断是否处于调试模式,调试模式中不会进行内存泄漏检测。为什么呢?因为在调试过程中可能会保留上一个引用从而导致错误信息上报。
  • watchExecutor : 线程控制器,在 onDestroy() 之后并且主线程空闲时执行内存泄漏检测。
  • gcTrigger : 用于 GC,watchExecutor 首次检测到可能的内存泄漏,会主动进行 GC,GC 之后会再检测一次,仍然泄漏的判定为内存泄漏,最后根据heapDump信息生成相应的泄漏引用链。
  • gcTrigger的runGc()方法:这里并没有使用System.gc()方法进行回收,因为system.gc()并不会每次都执行。而是从AOSP中拷贝一段GC回收的代码,从而相比System.gc()更能够保证进行垃圾回收的工作。
    Runtime.getRuntime().gc();
    

子线程延时1000ms;System.runFinalization(); install方法内部最终还是调用了application的registerActivityLifecycleCallbacks()方法,这样就能够监听activity对应的生命周期事件了。

在RefWatcher#watch()中使用随机的UUID保证了每个检测对象对应的key 的唯一性。

在KeyedWeakReference内部,使用了key和name标识了一个被检测的WeakReference对象。在其构造方法中将弱引用和引用队列 ReferenceQueue 关联起来,如果弱引用reference持有的对象被GC回收,JVM就会把这个弱引用加入到与之关联的引用队列referenceQueue中。即 KeyedWeakReference 持有的 Activity 对象如果被GC回收,该对象就会加入到引用队列 referenceQueue 中。

使用Android SDK的API Debug.dumpHprofData() 来生成 hprof 文件。

在HeapAnalyzerService(类型为IntentService的ForegroundService)的runAnalysis()方法中,为了避免减慢app进程或占用内存,这里将HeapAnalyzerService设置在了一个独立的进程中。

你从这个库中学到什么有价值的或者说可借鉴的设计思想?
leakCannary中如何判断一个对象是否被回收?如何触发手动gc?c层实现?
BlockCanary原理:

该组件利用了主线程的消息队列处理机制,应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。

ButterKnife对性能的影响很小,因为没有使用使用反射,而是使用的Annotation Processing Tool(APT),注解处理器,javac中用于编译时扫描和解析Java注解的工具。在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器不能改变读入的Java类,比如不能加入或删除Java方法。

AOP IoC 的好处以及在 Android 开发中的应用

数据库框架对比?
数据库的优化
数据库数据迁移问题
数据库索引的数据结构
平衡二叉树
  • 1、非叶子节点只能允许最多两个子节点存在。
  • 2、每一个非叶子节点数据分布规则为左边的子节点小当前节点的值,右边的子节点大于当前节点的值(这里值是基于自己的算法规则而定的,比如hash值)。
  • 3、树的左右两边的层级数相差不会大于1。

使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加变成线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找。

目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构。

B-Tree

B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个)。

  • 排序方式:所有节点关键字是按递增次序排列,并遵循左小右大原则。
  • 子节点数:非叶节点的子节点数>1,且<=M ,且M>=2,空树除外(注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉)。
  • 关键字数:枝节点的关键字数量大于等于ceil(m/2)-1个且小于等于M-1个(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2)。
  • 所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子。

B树相对于平衡二叉树的不同是,每个节点包含的关键字增多了,把树的节点关键字增多后树的层级比原来的二叉树少了,减少数据查找的次数和复杂度。

B+Tree
规则:
  • 1、B+跟B树不同B+树的非叶子节点不保存关键字记录的指针,只进行数据索引。
  • 2、B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样。
  • 3、B+树叶子节点的关键字从小到大有序排列,左边结尾数据都会保存右边节点开始数据的指针。
  • 4、非叶子节点的子节点数=关键字数(来源百度百科)(根据各种资料 这里有两种算法的实现方式,另一种为非叶节点的关键字数=子节点数-1(来源维基百科),虽然他们数据排列结构不一样,但其原理还是一样的MySQL 的B+树是用第一种方式实现)。

特点:
  1. B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快。
  2. B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定。
  3. B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
  4. B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

B*Tree

B*树是B+树的变种,相对于B+树他们的不同之处如下:

  • 1、首先是关键字个数限制问题,B+树初始化的关键字初始化个数是cei(m/2),b树的初始化个数为(cei(2/3 m))。
  • 2、B+树节点满时就会分裂,而B*树节点满时会检查兄弟节点是否满(因为每个节点都有指向兄弟的指针),如果兄弟节点未满则向兄弟节点转移关键字,如果兄弟节点已满,则从当前节点和兄弟节点各拿出1/3的数据创建一个新的节点出来。

在B+树的基础上因其初始化的容量变大,使得节点空间使用率更高,而又存有兄弟节点的指针,可以向兄弟节点转移关键字的特性使得B*树分解次数变得更少。

结论:
  • 相同思想和策略:从平衡二叉树、B树、B+树、B*树总体来看它们贯彻的思想是相同的,都是采用二分法和数据平衡策略来提升查找数据的速度。
  • 不同的方式的磁盘空间利用:不同点是他们一个一个在演变的过程中通过IO从磁盘读取数据的原理进行一步步的演变,每一次演变都是为了让节点的空间更合理的运用起来,从而使树的层级减少达到快速查找数据的目的;

还不理解请查看:平衡二叉树、B树、B+树、B*树 理解其中一种你就都明白了