5年 Android 面试题

感谢提供面试题汇总 五年面试题汇总

GC原理,有哪几种GC方式

GC原理,有哪几种GC方式

GC回收机制分为四种方式

  • 标记清除算法
  • 复制算法
  • 标记压缩算法
  • 分代算法

GC的出现解放了程序员需要手动回收内存的苦恼,但我们也是要了解GC的,知己知彼,百战不殆嘛。

常见的GC回收算法主要包括引用计数算法、可达性分析法、标记清除算法、复制算法、标记压缩算法、分代算法以及分区算法。

其中,引用计数法和可达性分析法用于判定一个对象是否可以回收,其他的算法为具体执行GC时的算法。

今天来聊聊标记清除算法、复制算法、标记压缩算法、分代算法,主要介绍分代算法。

引用计数法和可达性分析法请移步:

JVM-GC垃圾回收算法-引用计数法
JVM-GC垃圾回收算法-判定一个对象是否是可回收的对象

1、标记清除算法

标记清除法是现在GC算法的基础,目前似乎没有哪个GC还在使用这种算法了。因为这种算法会产生大量的内存碎片。

标记清除算法的执行过程分为两个阶段:标记阶段、清除阶段。

  • 标记阶段会通过可达性分析将不可达的对象标记出来。
  • 清除阶段会将标记阶段标记的垃圾对象清除。

标记阶段如图所示:

Java堆中,黄色对象为不可达对象,在标记阶段被标记。

下面执行回收算法,执行后如图:
在这里插入图片描述从上图可以清晰的看出此算法的缺陷,回收后会产生大量不连续的内存空间,即内存碎片。由于Java在分配内存时通常是按连续内存分配,那么当碎片空间不足以分配给新的对象时,就造成了内存浪费。

2、复制算法

复制算法会将内存空间分为两块,每次只使用其中一块内存。复制算法同样使用可达性分析法标记除垃圾对象,当GC执行时,会将非垃圾对象复制到另一块内存空间中,并且保证内存上的连续性,然后直接清空之前使用的内存空间。然后如此往复。

我们姑且将这两块内存区域称为from区和to区。

如下图所示,r1和r2作为GC Root对象,经过可达性分析后,标记除黄色对象为垃圾对象。
在这里插入图片描述复制过程如下,GC会将五个存活对象复制到to区,并且保证在to区内存空间上的连续性。
在这里插入图片描述最后,将from区中的垃圾对象清除。
在这里插入图片描述综上述,该算法在存货对象少,垃圾对象多的情况下,非常高效。其好处是不会产生内存碎片,但坏处也是显而易见的,就是直接损失了一半的可用内存。

3、标记压缩算法

标记压缩算法可以解决标记清除算法的内存碎片问题。
其算法可以看作三步:

  • 标记垃圾对象
  • 清除垃圾对象
  • 内存碎片整理

其过程如下:

首先标记除垃圾对象(黄色)
在这里插入图片描述清除垃圾对象
在这里插入图片描述内存碎片整理
在这里插入图片描述

4、分代算法

分代算法基于复制算法和标记压缩算法。

首先,标记清除算法、复制算法、标记压缩算法都有各自的缺点,如果单独用其中某一算法来做GC,会有很大的问题。

例如,标记清除算法会产生大量的内存碎片,复制算法会损失一半的内存,标记压缩算法的碎片整理会造成较大的消耗。

其次,复制算法和标记压缩算法都有各自适合的使用场景。

复制算法适用于每次回收时,存活对象少的场景,这样就会减少复制量。

标记压缩算法适用于回收时,存活对象多的场景,这样就会减少内存碎片的产生,碎片整理的代价就会小很多。

分代算法将内存区域分为两部分:新生代和老年代。

根据新生代和老年代中对象的不同特点,使用不同的GC算法。

新生代对象的特点是:创建出来没多久就可以被回收(例如虚拟机栈中创建的对象,方法出栈就会销毁)。也就是说,每次回收时,大部分是垃圾对象,所以新生代适用于复制算法。

老年代的特点是:经过多次GC,依然存活。也就是说,每次GC时,大部分是存活对象,所以老年代适用于标记压缩算法。

新生代分为eden区、from区、to区,老年代是一整块内存空间,如下所示:
在这里插入图片描述
分代算法执行过程
首先简述一下新生代GC的整个过程(老年代GC会在下面介绍):新创建的对象总是在eden区中出生,当eden区满时,会触发Minor GC,此时会将eden区中的存活对象复制到from和to中一个没有被使用的空间中,假设是to区(正在被使用的from区中的存活对象也会被复制到to区中)。

有几种情况,对象会晋升到老年代:

  • 超大对象会直接进入到老年代(受虚拟机参数-XX:PretenureSizeThreshold参数影响,默认值0,即不开启,单位为Byte,例如:3145728=3M,那么超过3M的对象,会直接晋升老年代)
  • 如果to区已满,多出来的对象也会直接晋升老年代
  • 复制15次(15岁)后,依然存活的对象,也会进入老年代

此时eden区和from区都是垃圾对象,可以直接清除。

PS:为什么复制15次(15岁)后,被判定为高龄对象,晋升到老年代呢?
因为每个对象的年龄是存在对象头中的,对象头用4bit存储了这个年龄数,而4bit最大可以表示十进制的15,所以是15岁。

下面从JVM启动开始,描述GC的过程。
JVM刚启动并初始化完成后,几块内存空间分配完毕,此时状态如上图所示。
(1)新创建的对象总是会出生在eden区
在这里插入图片描述(2)当eden区满的时候,会触发一次Minor GC,此时会从from和to区中找一个没有使用的空间,将eden区中还存活的对象复制过去(第一次from和to都是空的,使用from区),被复制的对象的年龄会+1,并清除eden区中的垃圾对象。
在这里插入图片描述(3)程序继续运行,又在eden区产生了新的对象,并产生了一个超大对象,并产生了一个复制后to区放不下的对象
在这里插入图片描述(4)当eden区再次被填满时,会再一次触发Minor GC,这次GC会将eden区和from区中存活的对象复制到to区,并且对象年龄+1,超大对象会直接晋升到老年代,to区放不下的对象也会直接晋升老年代。
在这里插入图片描述(5)程序继续运行,假设经过15次复制,某一对象依然存活,那么他将直接进入老年代。
在这里插入图片描述
(6)老年代 Full GC
在进行Minor GC之前,JVM还有一步操作,他会检查新生代所有对象使用的总内存是否小于老年代最大剩余连续内存,如果上述条件成立,那么这次Minor GC一定是安全的,因为即使所有新生代对象都进入老年代,老年代也不会内存溢出。如果上述条件不成立,JVM会查看参数HandlePromotionFailure[1]是否开启(JDK1.6以后默认开启),如果没开启,说明Minor GC后可能会存在老年代内存溢出的风险,会进行一次Full GC,如果开启,JVM还会检查历次晋升老年代对象的平均大小是否小于老年代最大连续内存空间,如果成立,会尝试直接进行Minor GC,如果不成立,老年代执行Full GC。
在这里插入图片描述eden区和from区的存活对象会复制到to区,超大对象和to区容纳不下的对象会直接晋升老年代。当eden区满时,触发Minor GC,此时判断老年代剩余连续内存已经小于新生代所有对象占用内存总和,假设HandlePromotionFailure参数开启,JVM还会继续判断老年代剩余连续内存是否大于历次晋升老年代对象的平均大小,如图所示,目前老年代还剩2个空间,如果之前平均每次晋升三个对象到老年代,剩余空间小于平均值,会触发Full GC。

老年代回收-标记:
在这里插入图片描述老年代回收-清除:
在这里插入图片描述老年代回收-碎片整理:
在这里插入图片描述

Minor GC存在的问题

Minor GC的问题在于,新生代的对象可能被老年代引用,而这种情况可达性分析是分析不到的,但这种情况的新生代对象是不应该被回收的。

HotSpot虚拟机提供了一个解决方案:卡表。

这种方法会将老年代内存平均分为很多的卡片,每个卡片都包含一部分对象,然后维护一个卡表,卡表是一个数组,每个元素指向一个卡片,并标识出这个卡片中有没有指向新生代的对象,如果有标识为1。这样一来,Minor GC只需要扫描卡表中标识为1的卡片即可,大大提升了效率。

卡表如下图所示:
在这里插入图片描述

注释

[1] HandlePromotionFailure:是一种相对于"判断老年代剩余空间必须大于新生代所有对象占用内存综合"策略更为冒进的一种策略,由于每一次晋升到老年代对象所需要的内存是不一定的,所以如果这个参数开启,会取每一次晋升对象占用内存的平均值作为参照,如果剩余空间大于平均值,就不用执行Full GC。

HashMap原理

HashMap原理

解决Hash冲突的方式有四种

  • 开放定址法
  • 再哈希法
  • 链地址法
  • 建立公共溢出区

HashCode和Hash算法

webview解决内存泄漏

webview内存泄漏

Android内存泄漏的方式有哪些

内存泄漏的场景和解决办法

包体积优化

APK体积优化

事件分发机制

事件分发机制

ViewModel实现原理

ViewModel实现原理

ViewModel是如何在屏幕旋转时保存数据的

实现屏幕旋转时保存ViewModel数据

LiveData工作原理

LiveData工作原理

Lifecle

Lifecle 和 LiveData

浏览器输入一个地址,按下回车,这个过程是什么样的

浏览器输入一个地址,按下回车,这个过程是什么样的

HTTP的三次握手和四次挥手

  • SYN:表示建立连接
  • ACK:响应
  • FIN:关闭连接

1. 三次握手

  • 第一次握手:客户端将标志位SYN置为1,随机产生一个值为seq=J(J的取值范围为=1234567)的数据包到服务器,客户端进入SYN_SENT状态,等待服务端确认;

  • 第二次握手:服务端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务端进入SYN_RCVD状态。

  • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务端,服务端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,完成三次握手,随后客户端A与服务端B之间可以开始传输数据了。

在这里插入图片描述

2. 四次挥手

  • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

  • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。**

  • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

  • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
    在这里插入图片描述

为什么要进行三次握手呢?

  • 其实网络之间的传输并不是想象的那样一帆风顺,客户端发送请求,服务端就能及时收到。比如现在有一个从客户端过来的请求,在某个网络结点长时间的滞留了,但是并没有丢失的情况下,以致延误到连接释放以后的某个时间才到达服务端,这时候客户端便认为这是一个失效的报文段。但是服务端却不这样认为,服务端认为这是客户端刚发起的一个请求,便向客户端发出确认报文段,同意建立连接。这时候如果没有第三次握手,便会造成服务端一直等待客户端发送请求数据,但是客户端已经舍弃了本次请求,因此服务起便会一直等待,从而造成服务区资源的浪费。这样的请求一多,自然而然造成服务器的崩溃。因此,为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误,所以建立tcp连接时需要第三次握手。

那么连接创建了,又怎么中断链接呢?这是时候又要引出tcp协议的另外一个概念,四次挥手。

为什么要进行四次挥手?

  • TCP是全双工模式,中断连接必须要服务端与客户端双方都确认都终止才行,不能单方面终止其中一方,根据四次挥手的过程来看便能够很容易理解了。

那为什么需要等待两个2MSL呢?

  • 客户端给服务端发送的ACK = 1丢失,服务端等待
    1MSL没收到,然后重新发送消息需要1MSL。如果再次接收到服务端的消息,则重启2MSL计时器,发送确认请求。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP四次分手完毕。

简单介绍一下TCP

操作系统之间的通信,是需要一种规则的.而我们就将这种规则称为协议,TCP/IP 是互联网相关各类协议族的总称。

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP/IP协议族按层次可分为应用层,传输层,网络层,数据链路层,物理层。

在这里插入图片描述

TCP/IP,TCP,UDP,IP,Socket 之间的关系

一篇文章看明白 TCP/IP,TCP,UDP,IP,Socket 之间的关系

什么是TCP/IP?
是互联网相关的各类协议族的总称:其中包括 TCP、UDP、IP、HTTP等

什么是 IP?
网络之间互连的协议(IP)是 Internet Protocol 的外语缩写,IP 是在 TCP/IP 协议中网络层的主要协议,任务是仅仅根据源主机和目的主机的地址传送数据

TCP 和 UDP的区别

  • TCP是安全面向连接的传输,建立连接必须经过三次握手,断开连接必须经过四次挥手,如果传输中断会进行重连. —> PS:发送邮件和传输稳定的数据
  • UDP是不安全面向事物的传输,但是能保证自己的数据都发送出去,但是在执行的过程中可能会出现丢包的现象. —> PS:打电话网络不稳定时会出现卡顿就是丢包的现象.

HTTPS原理及解析

HTTPS原理及解析

多线程如何保证线程安全

多线程保证线程安全

  1. 加同步锁 synchronized
  2. volatile
  3. 可以通过synchronized关键字定义同步代码块或者同步方法保障有序性,另外也可以通过Lock接口保障有序性。

锁的分类

  1. 平锁/非公平锁
  2. 可重入锁
  3. 互斥锁/读写锁
  4. 独享锁/共享锁
  5. 分段锁
  6. 偏向锁/轻量级锁/重量级锁

什么是CAS

CAS机制:CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值V1,要修改的新值V2。更新一个变量的时候,只有当变量的预期值V1和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为V2。

什么是原子性

原子性

什么是原子性
原子性是世界上最小单位,具有不可分割性。比如a=0;(a非long和double类型)这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++;这个操作实际上是a=a+1;是可分割的,所以他不是一个原子操作。

原子操作的作用
非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们就称它具有原子性。

MVC、MVP、MVVM的区别

MVC、MVP、MVVM的区别
MVC、MVP、MVVM的区别

Glide、Picasso、Fresco的区别

Glide、Picasso、Fresco的区别

图片的三级缓存

  • 内存缓存
  • 磁盘缓存
  • 网络缓存

图片的三级缓存

动画分类

Android三种动画原理及使用场景

  • 帧动画
    像电影片段一样,由一张一张的图片组成的一组动画效果,效果比较多样化,实现种类多,缺点是资源消耗过大,繁琐。
  • 补间动画
    和帧动画一样,android3.0之前(API11)前的,补件动画的特点是,只能作用于View上,比如button,TextView,linelayout等等上面。并且动画效果只是视觉上的效果,其view组件还是在原来位置。
  • 属性动画
    属性动画是在android3.0之后出来的,其目的就是改善,解决补间动画的不足,所以属性动画的使用的地方就比较广泛,不单单是简单的view上,而是java对象,并且组件的位置也会根据动画效果而改变。实现效果也不再那么单一。

Handler消息机制

handler消息机制
handler流程图
handler中post和send方式区别
用法上:

  1. send:发送的是Message
 Message message = new Message();
 message.what = SEND_UPDATA_TEXT;
 handler.sendMessage(message);
  1. post:发送的是Runnable对象
 handler.post(new Runnable() {
	            @Override
	            public void run() {
	                ......
	            }
	        });

OkHttp面试

okHttp面试题

1、Okhttp 基本实现原理

OkHttp 主要是通过 5 个拦截器和 3 个双端队列(2 个异步队列,1 个同步队列)工作。内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,实现了各层的解耦。

OkHttp 的底层是通过 Socket 发送 HTTP 请求与接受响应,但是 OkHttp 实现了连接池的概念,即对于同一主机的多个请求,可以公用一个 Socket 连接,而不是每次发送完 HTTP 请求就关闭底层的 Socket,这样就实现了连接池的概念。而 OkHttp 对 Socket 的读写操作使用的 OkIo 库进行了一层封装。

执行流程:

  • 通过构建者构建出OkHttpClient对象,再通过newCall方法获得RealCall请求对象.
  • 通过RealCall发起同步或异步请求,而决定是异步还是同步请求的是由线程分发器dispatcher来决定.
  • 当发起同步请求时会将请求加入到同步队列中依次执行,所以会阻塞UI线程,需要开启子线程执行.
  • 当发起异步请求时会创建一个线程池,并且判断请求队列是否大于最大请求队列64,请求主机数是否大于5,如果大于请求添加到异步等待队列中,否则添加到异步执行队列,并执行任务.

2、Okhttp 网络缓存如何实现?
      OKHttp 默认只支持 get 请求的缓存。

  • 第一次拿到响应后根据头信息决定是否缓存。
  • 下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
  • 如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。

3、Okhttp 网络连接怎么实现复用?

HttpEngine 在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,如果无法获取,就会调用createnextConnection()来新建一个Connection对象,这就是 Okhttp 多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象。

在这里插入图片描述4、Dispatcher 的功能是什么?

Dispatcher中文是分发器的意思,和拦截器不同的是分发器不做事件处理,只做事件流向。他负责将每一次Requst进行分发,压栈到自己的线程池,并通过调用者自己不同的方式进行异步和同步处理。 通俗的讲就是主要维护任务队列的作用。

OkHttp 设置了默认的最大并发请求量 maxRequests = 64 和单个 Host 主机支持的最大并发量 maxRequestsPerHost = 5

  • 记录同步任务、异步任务及等待执行的异步任务。
  • 调度线程池管理异步任务。
  • 发起/取消网络请求 API:execute、enqueue、cancel。

Dispatcher 类,该类中维护了三个双端队列(Deque):

  • readyAsyncCalls:准备运行的异步请求
  • runningAsyncCalls:正在运行的异步请求
  • runningSyncCalls:正在运行的同步请求

5、addInterceptor 与 addNetworkInterceptor 的区别?
      二者通常的叫法为应用拦截器网络拦截器
    从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求;
    而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。

1、首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
2、其次,如上文提到除了CallServerInterceptor,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
3、最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

6、Okhttp 拦截器的作用是什么?

1、应用拦截器

拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。

  • RetryAndFollowUpInterceptor :处理错误重试和重定向
  • BridgeInterceptor :应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
  • CacheInterceptor :缓存拦截器,如果命中缓存则不会发起网络请求。
  • ConnectInterceptor :连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。

2、网络拦截器

用户自定义拦截器,通常用于监控网络层的数据传输。

  • CallServerInterceptor :请求拦截器,在前置准备工作完成后,真正发起了网络请求。

7、Okhttp 有哪些优势?

  • 支持 http2,对一台机器的所有请求共享同一个 Socket
  • 内置连接池,支持连接复用,减少延迟
  • 响应缓存可以完全避免网络重复请求
  • 请求失败时自动重试主机的其他 ip,自动重定向
  • 丰富的 API,可扩展性好
  • 支持透明的 gzip 压缩响应体

8、response.body().string() 为什么只能调用一次?

我们可能习惯在获取到Response对象后,先response.body().string()打印一遍 Log,再进行数据解析,却发现第二次直接抛异常,其实直接跟源码进去看就发现,通过source拿到字节流以后,直接调用closeQuietly()方法关闭了,这样第二次再去通过source读取就直接流已关闭的异常了。

public final String string() throws IOException {
  BufferedSource source = source();
  try {
    Charset charset = Util.bomAwareCharset(source, charset());
    return source.readString(charset);
  } finally {
    //这里讲resource给悄悄close了
    Util.closeQuietly(source);
  }
}

解决方案:

  • 内存缓存一份response.body().string();
  • 自定义拦截器处理 Log。

9、Okhttp 运用了哪些设计模式

Okhttp 运用了六种设计模式:

  • 构造者模式(OkhttpClient,Request 等各种对象的创建)
  • 工厂模式(在 Call 接口中,有一个内部工厂 Factory 接口。)
  • 单例模式(Platform 类,已经使用 Okhttp 时使用单例)
  • 策略模式(在 CacheInterceptor 中,在响应数据的选择中使用了策略模式,选择缓存数据还是选择网络访问。)
  • 责任链模式(拦截器的链式调用)
  • 享元模式(Dispatcher 的线程池中,不限量的线程池实现了对象复用)

Retrofit面试

Retrofit面试题

1、 概括

Retrofit就是一个网络请求框架的封装,底层的网络请求默认使用的Okhttp,本身只是简化了用户网络请求的参数配置等,还能与Rxjava相结合,使用起来更加简洁方便。

  • App应用程序通过Retrofit请求网络,实际上是使用Retrofit接口层封装请求参数,之后由OkHttp完成后续的请求操作。
  • 在服务端返回数据之后,OkHttp将原始的结果交给Retrofit,Retrofit根据用户的需求对结果进行解析。
  • 完成数据的转化(converterFactory),适配(callAdapterFactory),通过设计模式进行各种扩展。

2、涉及到的设计模式

  • 构建者模式
  • 工厂模式
  • 代理模式
  • 适配器模式
  • 观察者模式
  • 策略模式
  • 外观模式

3、使用

    @GET("/user/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);

    //call封装了整个okhttp的请求
    
    Retrofit retrofit = new Retrofit.Builder()
                        .baseUrl("https://api.github.com/")
                        .addConverterFactory(GsonConverteractory.create())
                        //.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                        .build();
    //converterFactory
    //后续交个okhttp

4、使用Retrofit的七步骤

  • 添加Retrofit依赖,网络权限
  • 定义接收服务器返回数据的Bean
  • 创建网络请求的接口,使用注解(动态代理,核心)
  • builder模式创建Retrofit实例,converter,calladapter…
  • 创建接口实例,调用具体的网络请求
  • call同步/异步网络请求
  • 处理服务器返回的数据

5、Retrofit网络通信八步骤

      1. 创建Retrofit实例
      2. 定义网络请求接口,并为接口中的方法添加注解
      3. 通过动态代理生成网络请求对象
      4. 通过网络请求适配器将网络请求对象进行平台适配
      5. 通过网络请求执行器,发送网络请求(call)
      6. 通过数据解析器解析数据
      7. 通过回调执行器,切换线程
      8. 用户在主线程处理返回结果

6、代理

为其他对象提供一种代理,用以控制对这个对象的访问

      1、静态

      2、动态

  • 在程序运行时创建的代理方式
  • 无侵入增强
  • jdk动态代理 vs cglib

jdk动态代理

  • 只能为接口动态
  • InvocationHandler 必须要实现
  • invoke 的参数中获取参数
  • invoke 的返回值返回给使用者
  • newProxyInstance 中传入 InvocationHandler

7、builder

platform:

单例获取不同平台
Android平台中MainthreadExecutor

callAdapterFactory:

通过calladapter将原始Call进行封装,找到对应的执行器。如rxjavaCallFactory对应的Observable,转换形式Call --> Observable

converterFactory:

数据解析Converter,将response通过converterFactory转换成对应的数据形式,GsonConverterFactory,FastJsonConverterFactory。

Retrofit:

Retrofit核心类,对外提供接口。通过retrofit.create()创建retrofit实例,外观模式。在create()方法中,使用动态代理模式对请求的接口中方法进行封装(ServiceMethod),初始化OkhttpCall。

ServiceMethod:

核心处理类,解析方法和注解,toRequest()方法中生成HttpRequest。创建responseConverter(将response流转换为String或实体),创建callAdapter

Retrofit:

是对okhttp3.Call的封装调用

8、总结:

  1. 运行期
  2. InvocationHandler 接口和 Proxy 类
  3. 动态代理与静态代理的不同

线程池的理解

线程池的作用

浅拷贝和深拷贝

  • 浅拷贝:不会创建新的对象,拷贝的是内存地址。
  • 深拷贝:必定在内存中创建新的对象,真拷贝。

深浅拷贝

四大组件

Android四大组件

分类

1、activity
2、service
3、broadcast receiver
4、content provider

1、activity

(1)一个Activity通常就是一个单独的屏幕(窗口)。

(2)Activity之间通过Intent进行通信。

(3)android应用中每一个Activity都必须要在AndroidManifest.xml配置文件中声明,否则系统将不识别也不执行该Activity。

2、service

(1)service用于在后台完成用户指定的操作。service分为两种:

(a) started(启动):当应用程序组件(如activity)调用startService()方法启动服务时,服务处于started状态。
(b) bound(绑定):当应用程序组件调用bindService()方法绑定到服务时,服务处于bound状态。

(2)startService()与bindService()区别:

(a) started service(启动服务)是由其他组件调用startService()方法启动的,这导致服务的onStartCommand()方法被调用。当服务是started状态时,其生命周期与启动它的组件无关,并且可以在后台无限期运行,即使启动服务的组件已经被销毁。因此,服务需要在完成任务后调用stopSelf()方法停止,或者由其他组件调用stopService()方法停止。
(b) 使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。

(3)开发人员需要在应用程序配置文件中声明全部的service,使用标签。

(4)Service通常位于后台运行,它一般不需要与用户交互,因此Service组件没有图形用户界面。Service组件需要继承Service基类。Service组件通常用于为其他组件提供后台服务或监控其他组件的运行状态。

3、broadcast receiver

(1)你的应用可以使用它对外部事件进行过滤,只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力,例如闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

(2)广播接收者的注册有两种方法,分别是程序动态注册和AndroidManifest文件中进行静态注册。

(3)动态注册广播接收器特点是当用来注册的Activity关掉后,广播也就失效了。静态注册无需担忧广播接收器是否被关闭,只要设备是开启状态,广播接收器也是打开着的。也就是说哪怕app本身未启动,该app订阅的广播在触发时也会对它起作用。

(4)Android还有一套本地广播机制,就是为了解决广播的安全问题,因为系统全局广播可以被其他任何程序接收到,一些携带关键性数据的广播就可能被其他应用程序截获。而本地广播机制发出的广播只能在应用程序的内部进行传递,并且只能接收来自本应用程序的广播,这样就不存在安全问题了。

5)今天了解了Android的静态注册和动态注册,Android在8.0以后,为了提高效率,删除了静态注册,防止关闭App后广播还在,造成内存泄漏。现在静态注册的广播需要指定包名,而动态注册就没有这个问题。并且,无论是静态注册广播还是动态注册广播,在接收广播的时候都不能拦截广播,否则会报错。
谷歌官网的原文是:应用无法使用其清单注册大部分隐式广播。不过,是不能对大部分的广播进行注册,但还是有些广播可以进行静态注册的,比如对接收Android开机的广播通过静态注册还是能够正常接收的。

4、content provider

1)android平台提供了Content Provider使一个应用程序的指定数据集提供给其他应用程序。其他应用可以通过ContentResolver类从该内容提供者中获取或存入数据。

(2)只有需要在多个应用程序间共享数据是才需要内容提供者。例如,通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处是统一数据访问方式。

(3)ContentProvider实现数据共享。ContentProvider用于保存和获取数据,并使其对所有应用程序可见。这是不同应用程序间共享数据的唯一方式,因为android没有提供所有应用共同访问的公共存储区。

(4)开发人员不会直接使用ContentProvider类的对象,大多数是通过ContentResolver对象实现对ContentProvider的操作。

(5)ContentProvider使用URI来唯一标识其数据集,这里的URI以content://作为前缀,表示该数据由ContentProvider来管理。

5、Intent介绍

Intent是Android系统用来抽象描述要执行的一个操作,也可以在不同组件之间进行沟通和消息传递。
显式的Intent:就是你已经知道要启动的组件名称,比如某个Activity的包名和类名,在Intent中明确的指定了这个组件(Activity),一般来说这种Intent经常用在一个应用中,因为你已经明确的知道要启动的组件名称。
隐式的Intent:就是你不知道要启动的组件名称,只知道一个Intent动作要执行,比如:拍照,录像,查看地图。一般来说这种Intent用在不同的应用之间传递信息。

进程间通信

Android 进程间通信

浅谈观察者模式

23种设计模式之观察者模式

RxJava使用

RxJava

JetPack之Paging

paging

Kotlin之类的继承

kotlin创建类默认是final,需要在class前添加open字段,如:

class A

//修改如下
open class A

Handler是怎么把消息插入到队列中的

Handler是怎么把消息插入到队列中的

在这里插入图片描述1、源码分析

具体分析请见代码注释:

/**
 * 消息队列是以执行时间为序的优先级队列
 *
 * @param msg
 * @param when
 * @return
 */
boolean enqueueMessage(Message msg, long when) {

    //入队消息没有绑定Handler
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    //入队消息已经在使用中
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    //获取同步锁
    synchronized (this) {
        //入队消息正在被取消发送
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            //回收入队消息
            msg.recycle();
            return false;
        }
        //标记入队消息为正在使用中
        msg.markInUse();
        //入队消息的执行时间
        msg.when = when;
        //获取消息队列的队首消息
        Message p = mMessages;
        //是否需要唤醒线程
        boolean needWake;
        //如果队列首部为null,也就是队列为空
        //或如果入队消息的执行时间为0,也就是入队消息需要马上执行
        //或如果入队消息的执行时间小于,也就是早于队首消息的执行时间
        if (p == null || when == 0 || when < p.when) {
            // 新头,唤醒事件队列如果阻塞。
            // New head, wake up the event queue if blocked.
            // 入队消息的指针指向队首消息
            msg.next = p;
            // 入队消息成为新的队首消息
            mMessages = msg;
            // 且如果线程已经阻塞
            // 则需要唤醒
            needWake = mBlocked;
        } else {
            // 插入到队列中间。通常我们不需要唤醒事件队列,除非在队列的顶部有一个屏障,并且消息是队列中最早的异步消息。
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.

            // 如果线程已经阻塞
            // 且如果队列的队首消息的Handler为空,也就是队首消息是同步屏障消息
            // 且如果入队消息是异步的,也就是可以通过同步屏障
            // 则需要唤醒
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            //前一条消息
            Message prev;
            // 循环
            for (; ; ) {
                //获取队列首条消息
                prev = p;
                //获取下一条消息
                p = p.next;
                //如果下一条消息是空的,或者消息的执行时间早于下一条消息的执行时间,则退出循环
                if (p == null || when < p.when) {
                    break;
                }
                // 但如果线程已经阻塞
                // 且如果队列中有异步消息,可以穿过同步屏障
                // 则不需要唤醒

                // 如果线程已经阻塞
                // 且如果线程中没有异步消息,没办法穿过同步屏障
                // 就需要唤醒
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // 将入队消息指向获取到的消息
            msg.next = p; // invariant: p == prev.next
            // 将前一条消息的指针指向入队消息
            prev.next = msg;
        }

        // 我们可以假设mPtr != 0,因为m是假的。
        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            //如果需要唤醒,则进行唤醒
            nativeWake(mPtr);
        }
    }
    return true;
}

2、消息是分哪些情况入队的?如何入队?

我们剖除入队规则、同步锁、同步屏障消息、异步消息、唤醒规则等逻辑,将入队的逻辑代码抽出,得到:

public class Message {
    public Object obj;
    public long when;
    public Message next;
}
public class MessageQueue {
    public Message mMessages;

    public void enqueueMessage(Message msg, long when) {
        msg.when = when;
        Message p = mMessages;
        if (p == null || when == 0 || when < p.when) {
            //往空队列和队列头插入消息
            msg.next = p;
            mMessages = msg;
        } else {
            Message prev;
            for (; ; ) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    //往队列尾和队列中插入消息
                    break;
                }
            }
            msg.next = p;
            prev.next = msg;
        }
    }
}

2.1、往空队列插入消息

在这里插入图片描述

2.2、在队列头插入消息

在这里插入图片描述2.3、在队列尾插入消息
在这里插入图片描述2.4、在队列中插入消息
在这里插入图片描述
3、消息入队时,什么情况下需要主动唤醒线程
3.1、队列中没有任何消息,且线程阻塞

此时新消息入队后便主动唤醒线程,无论新消息是同步消息、异步消息。

3.2、队首的消息执行时间未到,且线程阻塞

如果在阻塞时长未耗尽时,就新加入早于队首消息处理时间的消息,需要主动唤醒线程。 1、如果入队消息的执行时间为0,也就是入队消息需要马上执行。 2、如果入队消息的执行时间小于队首消息的执行时间,也就是入队消息要早于队首消息执行。

3.3、队首消息是同步屏障消息,并且队列中不含有异步消息,且线程阻塞

如果新加入的消息仍然是晚于队首同步障碍器处理时间,那么这次新消息的发布在next()层面上是毫无意义的,我们也不需要唤醒线程。
只有在新加入早于队首同步障碍器处理时间的同步消息时,或者,新加入异步消息时(不论处理时间),才会主动唤醒被next()阻塞的线程。

3.4、队首消息是同步屏障消息,队列中含有异步消息但执行时间未到,切线程阻塞

因为队首同步障碍器的缘故,无论新加入什么同步消息都不会主动唤醒线程。 即使加入的是异步消息也需要其处理时间早于设定好唤醒时执行的异步消息,才会主动唤醒。

在onCreate 方法中怎么拿到控件的宽高

在onCreate 方法中怎么拿到控件的宽高

在Android中,有时我们需要测量控件的宽度和高度进行一些运算,特别是在自适应屏幕的时候,这些计算就变得特别必要,但是,如果我们直接在onCreate,或者onStart、onResume(第一次执行时)方法,去获取控件的宽度和高度时,得出的结果会是0。
原因是,在执行这几个方法时,窗口Window对象并没有创建完成,而只是做了一些初始化的操作,想要获取控件的宽度和高度就要等到onDraw方法执行完成之后。那么我们怎么知道,控件什么时候绘制完成,并切去获取控件的宽度和高度呢。

方式一、Android为我们提供了这样的机制,利用View类中getViewTreeObserver()方法,为控件添加一个观察者,在控件绘制前进行回调,这样我么就可以获取控件绘制完成之后的宽度以及高度:

tv.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
 
			@Override
			public boolean onPreDraw() {
				if (!hasDraw) {
					Log.e(TAG,
							tv.getMeasuredWidth() + "=="
									+ tv.getMeasuredWidth());
					hasDraw = true;
				}
				return true;
			}
		});

方式二:监听View的可视状态的变化,View进行绘制之后,可视状态会改变,这时候我们就可以使用View类中的getViewTreeObserver().addOnGlobalLayoutListener,来给控件添加一个观察者:

tv.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
			
			@Override
			public void onGlobalLayout() {
				Log.e(TAG,
						tv.getMeasuredWidth() + "=="
								+ tv.getMeasuredWidth());
			}
		});

方式三、该方法有肯出现空指针异常,原因未知,不过在给控件设置LayoutParams之后,异常消失。

int w = View.MeasureSpec.makeMeasureSpec(0,
				View.MeasureSpec.UNSPECIFIED);
		int h = View.MeasureSpec.makeMeasureSpec(0,
				View.MeasureSpec.UNSPECIFIED);
		tv.measure(w, h);
		int height = tv.getMeasuredHeight();
		int width = tv.getMeasuredWidth();
		
		Log.e(TAG, width + "==" + height);

方式四、在Activity的onWindowFocusChaged()方法中去获取控件的宽度和高度

APP 和 Activity启动流程

启动流程
Activity启动过程分析

Android 系统启动过程

从系统层看:

  1. linux 系统层
  2. Android系统服务层
  3. Zygote

从开机启动到Home Launcher:

  1. 启动bootloader (小程序;初始化硬件)
  2. 加载系统内核 (先进入实模式代码在进入保护模式代码)
  3. 启动init进程(用户级进程 ,进程号为1)
  4. 启动Zygote进程(初始化Dalvik VM等)
  5. 启动Runtime进程
  6. 启动本地服务(system service)
  7. 启动 HomeLauncher

详细解析

Android系统完整的启动过程,从系统层次角度可分为Linux系统层、Android系统服务层、Zygote进程模型三个阶段;
从开机到启动Home Launcher完成具体的任务细节可分为七个步骤,下面就从具体的细节来解读Android系统完整的初始化过程。

一、启动BootLoader

Android 系统是基于Linux操作系统的,所以它最初的启动过程和Linux一样。当设备通电后首先执行BootLoader引导装载器,BootLoader是在操作系统内核运行之前运行的一段小程序。通过这段小程序初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境引导进入合适的状态,以便为最终调用操作系统内核准备好正确的运行环境。

而Linux系统启动时:

  1. 首先要加载BIOS的硬件信息,并获取第一个启动设备的代号
  2. 读取第一个启动设备的MBR的引导加载程序(lilo、grub等)的启动信息。
  3. 加载核心操作系统的核心信息,核心开始解压缩,并且尝试驱动所有的硬件设备。

在嵌入式系统中,通常不会有像BIOS那样的固件程序,因此整个系统的加载任务都是通过BootLoader完成的。

二、加载系统内核

Linux内核映像通常包括两部分代码,分别为实模式代码和保护模式代码。当BootLoader装载内核映像到代码段内存时,分别放置实模式代码和保护模式代码到不同的位置,然后进入实模式代码执行,实模式代码执行完成后转入保护模式代码。

实模式和保护模式的概念再次不做过多解释,读者可以自行查阅资料。

三、启动Init进程

当系统内核加载完成之后,会首先启动Init守护进程,它是内核启动的第一个用户级进程,它的进程号总是1。 Init进程启动完成之后,还负责启动其他的一些重要守护进程,包括:
Usbd进程(USB Daemon):USB连接后台进程,负责管理USB连接。
adbd 进程(Android Debug Bridge Daemon):ADB连接后台进程,负责管理ADB连接。
debuggerd 进程(Debugger Daemon) :调试器后台进程,负责管理调试请求及调试过程。
rild进程 (Radio Interface Layer Daemon): 无线接口层后台进程,负责管理无线通信服务。

四、启动Zygote进程

Init进程和一些重要的守护进程启动完成之后,系统启动Zygote 进程。Zygote 进程启动后,首先初始化一个Dalvik VM实例,然后为它加载资源与系统共享库,并开启Socket监听服务,当收到创建Dalvik VM实例请求时,会通过COW(copy on write)技术最大程度地复用自己,生成一个新的Dalvik VM实例。Dalvik VM实例的创建方法基于linux系统的fork原理。

其实,我个人理解,Zygote进程就相当于Linux系统中的fork进程。由它可以在系统运行期间,接收到创建虚拟机请求时,孵化Dalvik VM实例。:

五 、启动Runtime进程

在Zygote进程启动完成之后,Init进程会启动Runtime进程。Runtime进程首先初始化服务管理器(Service Manager),并把它注册为绑定服务(Binder services)的默认上下文管理器,负责绑定服务的注册与查找。然后Runtime进程会向Zygote进程发送启动系统服务(System Service)的请求,Zygote进程收到请求后,会“孵化”出一个新的Dalvik VM实例并启动系统服务进程。Runtime进程的启动流程如下图所示:

Runtime进程启动流程图
六、启动本地服务

System Service会首先启动两个本地服务(由C或C++编写的native服务),Surface Flinger和Audio Flinger,这两个本地系统服务向服务管理器注册成为IPC服务对象,以便在需要它们的时候很容易查找到。然后SystemService 会启动一些 Android 系统管理服务,包括硬件服务和系统框架核心平台服务,并注册它们成为IPC服务对象。

本地服务进程的启动流程如下图所示:

SystemService启动本地服务流程图

七、启动Home Laucher

当SystemService加载了所有的系统服务后就意味着系统就准备好了,它会向所有服务发送一个系统准备完毕(systemready) 广播。SystemService系统服务进程的启动流程如图1-6所示。当ActivityManagerService 接收到systemready广播后,会向Zygoute进程发送创建Dalvik 虚拟机实例的请求,Zygoute进程会负责生成一个新的Dalvik 虚拟机实例,然后ActivityManagerService在系统中查找具有属性的Activity,并启动它。ActivityManagerService同时也会使用同样的方法启动Contact(联系人)应用程序。

启动Home Laucher流程图

APk 安装过程

Android应用安装有如下四种方式:

  1. 系统应用安装――开机时完成,没有安装界面
  2. 网络下载应用安装――通过market应用完成,没有安装界面
  3. ADB工具安装――没有安装界面。
  4. 第三方应用安装――通过SD卡里的APK文件安装,有安装界面,由 packageinstaller.apk应用处理安装及卸载过程的界面。

应用安装的流程及路径

应用安装涉及到如下几个目录:

  • system/app ---------------系统自带的应用程序,获得adb root权限才能删除
  • data/app ---------------用户程序安装的目录。安装时把 apk文件复制到此目录
  • data/data ---------------存放应用程序的数据
  • data/dalvik-cache--------将apk中的dex文件安装到dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为原始apk文件大小的四分之一)

安装过程:

复制APK安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录,并data/data目录下创建对应的应用数据目录。

App 启动过程

这里以启动微信为例子说明

  1. Launcher通知AMS 要启动微信了,并且告诉AMS要启动的是哪个页面也就是首页是哪个页面
  2. AMS收到消息告诉Launcher知道了,并且把要启动的页面记下来
  3. Launcher进入Paused状态,告诉AMS,你去找微信吧

上述就是Launcher和AMS的交互过程

  1. AMS检查微信是否已经启动了也就是是否在后台运行,如果是在后台运行就直接启动,如果不是,AMS会在新的进程中创建一个ActivityThread对象,并启动其中的main函数。
  2. 微信启动后告诉AMS,启动好了
  3. AMS通过之前的记录找出微信的首页,告诉微信应该启动哪个页面
  4. 微信按照AMS通知的页面去启动就启动成功了。

Activity 启动过程

Activity 启动过程是由 ActivityMangerService(amS) 来启动的,底层 原理是 Binder实现的 最终交给 ActivityThread 的 performActivity 方法来启动她

ActivityThread大概可以分为以下五个步骤

  1. 通过ActivityClientRecoed对象获取Activity的组件信息
  2. 通过Instrument的newActivity使用类加载器创建Activity对象
  3. 检验Application是否存在,不存在的话,创建一个,保证 只有一个Application
  4. 通过ContextImpl和Activity的attach方法来完成一些初始化操作
  5. 调用oncreat方法

Android内存泄露检测之LeakCanary的使用

LeakCanary的使用

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值