Android进阶之路

【收藏】2021年Android跳槽大厂必备宝典(Android

2021-01-05  本文已影响0人  码了个码农

从几十份顶级面试仓库和300多篇高质量面经中总结出一份全面成体系化的Android高级面试题集。

欢迎来到2020年中高级Android大厂面试秘籍,为你保驾护航金三银四,直通大厂的Android高级篇下。

三、Android优秀三方库源码

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

一、网络底层框架:OkHttp实现原理

这个库是做什么用的?

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

为什么要在项目中使用这个库?
这个库都有哪些用法?对应什么样的使用场景?

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

这个库的优缺点是什么,跟同类型库的比较?
这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

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

各个拦截器的作用:

ConnectionPool:

1、判断连接是否可用,不可用则从ConnectionPool获取连接,ConnectionPool无连接,创建新连接,握手,放入ConnectionPool。

2、它是一个Deque,add添加Connection,使用线程池负责定时清理缓存。

3、使用连接复用省去了进行 TCP 和 TLS 握手的一个过程。

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

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

手写拦截器?
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实现原理

这个库是做什么用的?

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

为什么要在项目中使用这个库?

1、功能强大:

2、简洁易用:

3、可扩展性好:

这个库都有哪些用法?对应什么样的使用场景?

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

这个库的优缺点是什么,跟同类型库的比较?
这个库的核心实现原理是什么?如果让你实现这个库的某些核心功能,你会考虑怎么去实现?

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

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

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

1、创建Retrofit实例:

2、创建网络请求接口的实例:

3、发送网络请求:

4、解析数据

5、切换线程:

6、处理结果

Android:主流网络请求开源库的对比(Android-Async-Http、Volley、OkHttp、Retrofit)

www.jianshu.com/p/050c6db5a…

三、响应式编程框架:RxJava实现原理

RxJava 变换操作符 map flatMap concatMap buffer?
RxJava中map和flatmap操作符的区别及底层实现
手写rxjava遍历数组。
你认为Rxjava的线程池与你们自己实现任务管理框架有什么区别?

四、图片加载框架:Glide实现原理

这个库是做什么用的?

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。

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

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

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

1、初始化各式各样的配置信息(包括缓存,请求线程池,大小,图片格式等等)以及glide对象。

2、将glide请求和application/SupportFragment/Fragment的生命周期绑定在一块。

设置请求url,并记录url已设置的状态。

3、Glide&into:

1、首先根据转码类transcodeClass类型返回不同的ImageViewTarget:BitmapImageViewTarget、DrawableImageViewTarget。

2、递归建立缩略图请求,没有缩略图请求,则直接进行正常请求。

3、如果没指定宽高,会根据ImageView的宽高计算出图片宽高,最终执行到onSizeReay()方法中的engine.load()方法。

4、engine是一个负责加载和管理缓存资源的类

当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找(软引用适合当cache,当内存吃紧的时候才会被回收。而weakReference在每次system.gc()就会被回收)(当LruCache存储紧张时,会把最近最少使用的数据放到SoftReference中),如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去硬盘缓存中中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到硬盘缓存中,然后放到LruCache中。

Glide缓存机制大致分为三层:内存缓存、弱引用缓存、磁盘缓存。

取的顺序是:内存、弱引用、磁盘。

存的顺序是:弱引用、内存、磁盘。

三层存储的机制在Engine中实现的。先说下Engine是什么?Engine这一层负责加载时做管理内存缓存的逻辑。持有MemoryCache、Map<Key, WeakReference<EngineResource<?>>>。通过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值来降低内存消耗。

比如:默认值ARGB_8888改为RGB_565,节约一半内存。

(2).设置Options.inSampleSize 缩放比例,对大图片进行压缩 。

(3).设置Options.inPurgeable和inInputShareable:让系统能及时回收内存。

A:inPurgeable:设置为True时,表示系统内存不足时可以被回收,设置为False时,表示不能被回收。

B:inInputShareable:设置是否深拷贝,与inPurgeable结合使用,inPurgeable为false时,该参数无意义。
复制代码

(4).使用decodeStream代替decodeResource等其他方法。

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

Java 引用类型分类:

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

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<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> 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 四类方法:

BitmapFactory.options 参数:

高效加载 Bitmap 的流程:

Bitmap的处理:

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

BitMap的缓存:

1.使用LruCache进行内存缓存。

2.使用DiskLruCache进行硬盘缓存。

实现一个ImageLoader的流程

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

具体为:

Bitmap在decode的时候申请的内存如何复用,释放时机
图片库对比

stackoverflow.com/questions/2…

www.trinea.cn/android/and…

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?

blog.csdn.net/guolin_blog…

blog.csdn.net/lmj62356579…

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

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

五、事件总线框架:EventBus实现原理

六、内存泄漏检测框架:LeakCanary实现原理

这个库是做什么用?

内存泄露检测框架。

为什么要在项目中使用这个库?
这个库都有哪些用法?对应什么样的使用场景?

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

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

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

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

主要分为如下7个步骤:

简单来说就是:

在一个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实现原理

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

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

八、依赖全局管理框架:Dagger2实现原理

九、数据库框架:GreenDao实现原理

数据库框架对比?
数据库的优化
数据库数据迁移问题
数据库索引的数据结构
平衡二叉树

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

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

B-Tree

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

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

B+Tree
规则:
特点:

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+树他们的不同之处如下:

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

结论:

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

四、热修复、插件化、模块化、组件化、Gradle、编译插桩技术

1、热修复和插件化

Android中ClassLoader的种类&特点

用于加载Android Framework层class文件。

用于加载已经安装到系统中的apk中的class文件。

用于加载指定目录中的class文件。

是PathClassLoader和DexClassLoader的父类。

热修补技术是怎样实现的,和插件化有什么区别?

插件化:动态加载主要解决3个技术问题:

插件化是体现在功能拆分方面的,它将某个功能独立提取出来,独立开发,独立测试,再插入到主应用中。以此来减少主应用的规模。

热修复:

原因:因为一个dvm中存储方法id用的是short类型,导致dex中方法不能超过65536个。

代码热修复原理:

相同点:

都使用ClassLoader来实现加载新的功能类,都可以使用PathClassLoader与DexClassLoader。

不同点:

热修复因为是为了修复Bug的,所以要将新的类替代同名的Bug类,要抢先加载新的类而不是Bug类,所以多做两件事:在原先的app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志,还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements,这样才能抢先代替Bug类,完成系统不加载旧的Bug类.。 而插件化只是增加新的功能类或者是资源文件,所以不涉及抢先加载新的类这样的使命,就避过了阻止相关类去打上CLASS_ISPREVERIFIED标志和还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements.

所以插件化比热修复简单,热修复是在插件化的基础上在进行替换旧的Bug类。

热修复原理:

资源修复:

很多热修复框架的资源修复参考了Instant Run的资源修复的原理。

传统编译部署流程如下:

Instant Run编译部署流程如下:

Instant Run中的资源热修复流程:

代码修复:

1、类加载方案:

65536限制:

65536的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用65535个方法。

LinearAlloc限制:

Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

加载流程:

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么需要重启呢?

各个热修复框架的实现细节差异:

2、底层替换方案:

当我们要反射Key的show方法,会调用Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());,最终会在native层将传入的javaMethod在ART虚拟机中对应一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括执行入口、访问权限、所属类和代码执行地址等。

替换ArtMethod结构体中的字段或者替换整个ArtMethod结构体,这就是底层替换方案。

AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容性问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。

Sophix采用的是替换整个ArtMethod结构体,这样不会存在兼容问题。

底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix。

3、Instant Run方案:

什么是ASM?

ASM是一个java字节码操控框架,它能够动态生成类或者增强现有类的功能。ASM可以直接产生class文件,也可以在类被加载到虚拟机之前动态改变类的行为。

Instant Run在第一次构建APK时,使用ASM在每一个方法中注入了类似的代码逻辑:当<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi><mi>h</mi><mi>a</mi><mi>n</mi><mi>g</mi><mi>e</mi><mtext>不为</mtext><mi>n</mi><mi>u</mi><mi>l</mi><mi>l</mi><mtext>时,则调用它的</mtext><mi>a</mi><mi>c</mi><mi>c</mi><mi>e</mi><mi>s</mi><mi>s</mi></mrow><annotation encoding="application/x-tex">change不为null时,则调用它的access</annotation></semantics></math>change不为null时,则调用它的accessdispatch方法,参数为具体的方法名和方法参数。当MainActivity的onCreate方法做了修改,就会生成替换类MainActivity<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>o</mi><mi>v</mi><mi>e</mi><mi>r</mi><mi>r</mi><mi>i</mi><mi>d</mi><mi>e</mi><mtext>,这个类实现了</mtext><mi>I</mi><mi>n</mi><mi>c</mi><mi>r</mi><mi>e</mi><mi>m</mi><mi>e</mi><mi>n</mi><mi>t</mi><mi>a</mi><mi>l</mi><mi>C</mi><mi>h</mi><mi>a</mi><mi>n</mi><mi>g</mi><mi>e</mi><mtext>接口,同时也会生成一个</mtext><mi>A</mi><mi>p</mi><mi>p</mi><mi>P</mi><mi>a</mi><mi>t</mi><mi>c</mi><mi>h</mi><mi>e</mi><mi>s</mi><mi>L</mi><mi>o</mi><mi>a</mi><mi>d</mi><mi>e</mi><mi>r</mi><mi>I</mi><mi>m</mi><mi>p</mi><mi>l</mi><mtext>类,这个类的</mtext><mi>g</mi><mi>e</mi><mi>t</mi><mi>P</mi><mi>a</mi><mi>t</mi><mi>c</mi><mi>h</mi><mi>e</mi><mi>d</mi><mi>C</mi><mi>l</mi><mi>a</mi><mi>s</mi><mi>s</mi><mi>e</mi><mi>s</mi><mtext>方法会返回被修改的类的列表(里面包含了</mtext><mi>M</mi><mi>a</mi><mi>i</mi><mi>n</mi><mi>A</mi><mi>c</mi><mi>t</mi><mi>i</mi><mi>v</mi><mi>i</mi><mi>t</mi><mi>y</mi><mtext>),根据列表会将</mtext><mi>M</mi><mi>a</mi><mi>i</mi><mi>n</mi><mi>A</mi><mi>c</mi><mi>t</mi><mi>i</mi><mi>v</mi><mi>i</mi><mi>t</mi><mi>y</mi><mtext>的</mtext></mrow><annotation encoding="application/x-tex">override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的</annotation></semantics></math>override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderImpl类,这个类的getPatchedClasses方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的change设置为MainActivity<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>o</mi><mi>v</mi><mi>e</mi><mi>r</mi><mi>r</mi><mi>i</mi><mi>d</mi><mi>e</mi><mtext>。最后这个</mtext></mrow><annotation encoding="application/x-tex">override。最后这个</annotation></semantics></math>override。最后这个change就不会为null,则会执行MainActivity<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>o</mi><mi>v</mi><mi>e</mi><mi>r</mi><mi>r</mi><mi>i</mi><mi>d</mi><mi>e</mi><mtext>的</mtext><mi>a</mi><mi>c</mi><mi>c</mi><mi>e</mi><mi>s</mi><mi>s</mi></mrow><annotation encoding="application/x-tex">override的access</annotation></semantics></math>override的accessdispatch方法,最终会执行onCreate方法,从而实现了onCreate方法的修改。

借鉴Instant Run原理的热修复框架有Robust和Aceso。

动态链接库修复:

重新加载so。

加载so主要用到了System类的load和loadLibrary方法,最终都会调用到nativeLoad方法。其会调用JavaVMExt的LoadNativeLibrary函数来加载so。

so修复主要有两个方案:

为什么选用插件化?

在Android传统开发中,一旦应用的代码被打包成APK并被上传到各个应用市场,我们就不能修改应用的源码了,只能通过服务器来控制应用中预留的分支代码。但是很多时候我们无法预知需求和突然发生的情况,也就不能提前在应用代码中预留分支代码,这时就需要采用动态加载技术,即在程序运行时,动态加载一些程序中原本不存在的可执行文件并运行这些文件里的代码逻辑。其中可执行文件包括动态链接库so和dex相关文件(dex以及包含dex的jar/apk文件)。随着应用开发技术和业务的逐步发展,动态加载技术派生出两个技术:热修复和插件化。其中热修复技术主要用来修复Bug,而插件化技术则主要用于解决应用越来越庞大以及功能模块的解耦。详细点说,就是为了解决以下几种情况:

插件化的思想:

安装的应用可以理解为插件,这些插件可以自由地进行插拔。

插件化的定义:

插件一般是指经过处理的APK,so和dex等文件,插件可以被宿主进行加载,有的插件也可以作为APK独立运行。

将一个应用按照插件的方式进行改造的过程就叫作插件化。

插件化的优势:

插件化框架对比:

插件化原理:

Activity插件化:

主要实现方式有三种:

Hook实现方式有两种:Hook IActivityManager和Hook Instrumentation。主要方案就是先用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity替换占坑的Activity。

Hook IActivityManager:

1、占坑、通过校验:

在Android 7.0和8.0的源码中IActivityManager借助了Singleton类实现单例,而且该单例是静态的,因此IActivityManager是一个比较好的Hook点。

接着,定义替换IActivityManager的代理类IActivityManagerProxy,由于Hook点IActivityManager是一个接口,建议这里采用动态代理。

然后,用代理类IActivityManagerProxy来替换IActivityManager。

2、还原插件Activity:

自定义的Callback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动SubActivity的Intent替换为启动TargetActivity的Intent。然后使用反射将Handler的mCallback替换为自定义的CallBack即可。使用时则在application的attachBaseContext方法中进行hook即可。

3、插件Activity的生命周期:

Hook Instrumentation:

Hook Instrumentation实现同样也需要用到占坑Activity,与Hook IActivity实现不同的是,用占坑Activity替换插件Activity以及还原插件Activity的地方不同。

分析:在Activity通过AMS校验前,会调用Activity的startActivityForResult方法,其中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期。并且在ActivityThread的performLaunchActivity中使用了mInstrumentation的newActivity方法,其内部会用类加载器来创建Activity的实例。

方案:在Instrumentation的execStartActivity方法中用占坑SubActivity来通过AMS的验证,在Instrumentation的newActivity方法中还原TargetActivity,这两部操作都和Instrumentation有关,因此我们可以用自定义的Instumentation来替换掉mInstrumentation。具体为:

资源插件化:

资源的插件化和热修复的资源修复都借助了AssetManager。

资源的插件化方案主要有两种:

so的插件化:

so的插件化方案和so热修复的第一种方案类似,就是将so插件插入到NativelibraryElement数组中,并且将存储so插件的文件添加到nativeLibraryDirectories集合中就可以了。

插件的加载机制方案:

2、模块化和组件化

模块化的好处

www.jianshu.com/p/376ea8a19…

分析现有的组件化方案:

很多大厂的组件化方案是以 多工程 + 多 Module 的结构(微信, 美团等超级 App 更是以 多工程 + 多 Module + 多 P 工程(以页面为单元的代码隔离方式) 的三级工程结构), 使用 Git Submodule 创建多个子仓库管理各个模块的代码, 并将各个模块的代码打包成 AAR 上传至私有 Maven 仓库使用远程版本号依赖的方式进行模块间代码的隔离。

组件化开发的好处:

跨组件通信:

跨组件通信场景:

跨组件通信方案分析:

提供服务的业务模块:

在公共服务(CommonService) 中声明 Service 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 Service 接口, 再通过 ARouter API 暴露实现类。

使用服务的业务模块:

通过 ARouter 的 API 拿到这个 Service 接口(多态持有, 实际持有实现类), 即可调用 Service 接口中声明的自定义方法, 这样就可以达到模块之间的交互。 此外,可以使用 AndroidEventBus 其独有的 Tag, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 Tag 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 EventBus。

如何管理过多的路由表?

RouterHub 存在于基础库, 可以被看作是所有组件都需要遵守的通讯协议, 里面不仅可以放路由地址常量, 还可以放跨组件传递数据时命名的各种 Key 值, 再配以适当注释, 任何组件开发人员不需要事先沟通只要依赖了这个协议, 就知道了各自该怎样协同工作, 既提高了效率又降低了出错风险, 约定的东西自然要比口头上说的强。

Tips: 如果您觉得把每个路由地址都写在基础库的 RouterHub 中, 太麻烦了, 也可以在每个组件内部建立一个私有 RouterHub, 将不需要跨组件的路由地址放入私有 RouterHub 中管理, 只将需要跨组件的路由地址放入基础库的公有 RouterHub 中管理, 如果您不需要集中管理所有路由地址的话, 这也是比较推荐的一种方式。

ARouter路由原理:

ARouter维护了一个路由表Warehouse,其中保存着全部的模块跳转关系,ARouter路由跳转实际上还是调用了startActivity的跳转,使用了原生的Framework机制,只是通过apt注解的形式制造出跳转规则,并人为地拦截跳转和设置跳转条件。

多模块开发的时候不同的负责人可能会引入重复资源,相同的字符串,相同的icon等但是文件名并不一样,怎样去重?

3、gradle

gradle熟悉么,自动打包知道么?

如何加快 Gradle 的编译速度?

Gradle的Flavor能否配置sourceset?

Gradle生命周期

4、编译插桩

谈谈你对AOP技术的理解?

说说你了解的编译插桩技术?

完整面试宝典PDF领取方式:点赞+评论后,点击此处获取!

上一篇下一篇

猜你喜欢

热点阅读