Android 高级开发面试集锦(整理相关面试知识点,持续更新。。。)

1,157 阅读45分钟

前序

写这篇文章的目的也是在于最近自己看了很多关于Android面试相关的优秀文章,想把这些文章里我想理解与学习的汇总起来,以便自我的学习与巩固,面对公司面试可以夸夸其谈。
最新看到的两篇比较好的面试总结
Android 攒了一个月的面试题及解答

又攒了一个月的Android面试题

大概将从三个方面梳理知识点:

  • Java 相关面试知识点
  • Android 相关面试知识点
  • 常用的数据结构与算法

Java 相关面试知识点

1.JVM内存区划分

JDK8 JVM划分为:虚拟机栈+堆内存+本地方法栈+元空间

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。

从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)

具体可看这篇博文 终于搞懂了java8的内存结构,再也不纠结方法区和常量池了!

2.垃圾回收算法 GC机制

垃圾回收需要完成两件事:找到垃圾,回收垃圾。
回收区域:只针对堆、方法区;线程私有区域数据会随线程结束销毁,不用回收
找到垃圾一般的话有两种方法:

  • 引用计数法: 当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中(jdk8之后在堆中)的静态对象和常量池中的常量。

回收垃圾的话有这么四种方法:

  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的碎片化。

  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要两倍的内存。

  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会将存活的对象压缩,避免了内存的碎片化。

  • 分代算法: 分代算法将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率。

新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法。

2.java 类加载过程

Java 中类加载分为 3 个步骤:加载、链接、初始化。

加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。
验证。 链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。

  • 验证。 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如 果验证出错,则会报VerifyError。
  • 准备。 这一步会创建静态变量,并为静态变量开辟内存空间。
  • 解析。 这一步会将符号引用替换为直接引用。

初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。

3.Java中引用类型的区别,具体的使用场景

Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。

  • 强引用: 强引用指的是通过 new 对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。

  • 软引用: 软引用是通过 SoftRefrence 实现的,它的生命周期比强引用短,在内存不足,抛出 OOM 之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。

  • 弱引用: 弱引用是通过 WeakRefrence 实现的,它的生命周期比软引用还短,GC 只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。

  • 虚引用: 虚引用是通过 FanttomRefrence 实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在 finalize 后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。

4.在Java中,List,Map,ArrayList,LinkedList与HashMap,ConcurrentHashMap的区别

由Collection接口派生的两个接口是List和Set.map是继承map接口
List接口
List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。
和下面要提到的Set不同,List允许有相同的元素。
除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。
实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

LinkedList类
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(...));

ArrayList类
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。 size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
和LinkedList一样,ArrayList也是非同步的(unsynchronized)。
Map接口
请注意,Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个value。Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。
HashMap类
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和nullkey。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

ConcurrentHashMap类
ConcurrentHashMap是Java 中支持高并发,高吞吐量的hashMap实现。ConcurrnetHashMap是基于线程安全的一个类,使用了锁分离技术之前是分段加锁后面使用在Node节点加锁,ConcurrentHashMap对读操作没有加锁,对写操作只加锁一小部分,保证其并发性。
在针对于Collections.synchronizedMap、ConcurrentHashMap、Hashtable,进行性能测试。模拟1000个并发,每个测试1000次操作,循环测试100轮,最后读写对应的时间分别为:(6544ms,707ms),(5960ms,650ms),(6719ms,713ms)(此数据依据当前运行条件,有浮动,但是得出的结论是,ConcurrentHashMap比其余两个在读和取上都会快一些)。 android ConcurrentHashMap的使用
ConcurrentHashMap 能否保证绝对的线程安全?
关于ConcurrentHashMap,你必须知道的事
CopyOnWriteArrayList你了解多少?

CopyOnWriteArrayList类
CopyOnWriteArrayList 是一个线程安全的随机访问列表,实现了 List 接口
优点:可以在多线程环境下操作 List、读的效率很高
缺点:就是可能读取的不是最新的值、每次写需要创建个新数组,占用额外内存。
CopyOnWriteArrayList 的读操作(比如get())也不会阻塞其他操作;写操作则是通过复制一份,对复制版本进行操作,不会影响原来的数据。和 Vector 相对效率提高不少。
批量操作(比如 addAll(), clear())是原子的,也就是说不存在重排序导致的未赋值就访问等情况。
如果需要在遍历时操作列表,其中一种解决办法就是使用 CopyOnWriteArrayList ,它的迭代器永远不会抛出 ConcurrentModificationException异常。原因在于:在创建一个迭代器时,它会拷贝一份列表数据,这样即使操作列表也不会影响迭代器,缺点和前面一样,可能无法反映数据的最新状态。
android 多线程 — 并发集合 CopyOnWriteArrayList、ConcurrentHashMap

5.Java中synchronized关键字以及对象锁和类锁的区别

synchronized的用法:synchronized修饰方法和synchronized修饰代码块

注意:

  • 1 无论是修饰方法还是修饰代码块都是对象锁,当一个线程访问一个带synchronized方法时,由于对象锁的存在,所有加synchronized的方法都不能被访问(前提是在多个线程调用的是同一个对象实例中的方法)
  • 2 无论是修饰静态方法还是锁定某个对象,都是类锁.一个class其中的静态方法和静态变量在内存中只会加载和初始化一份,所以,一旦一个静态的方法被申明为synchronized,此类的所有的实例化对象在调用该方法时,共用同一把锁,称之为类锁。
    Java synchronized 详解

synchronized和lock区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

  在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

java中volatile、synchronized和lock解析

6.String使用equals和==比较的区别

"=="操作符的作用
1、用于基本数据类型的比较
2、判断引用是否指向堆内存的同一块地址。

equals的作用
用于判断两个变量是否是对同一个对象的引用,即堆中的内容是否相同,返回值为布尔类型

String类型比较不同对象内容是否相同,应该用equals,因为==用于比较引用类型和比较基本数据类型时具有不同的功能。
对象不同,内容相同,"=="返回false,equals返回true
同一对象,"=="和equals结果相同

String字符串拼接问题,到底什么时候会走StringBuilder?

7.简单理解HTTP HTTPS 以及TCP 和UDP 的区别

计算机网络:OSI、TCP、UDP、IP、HTTP/HTTPS知识总结
简单理解HTTP HTTPS 以及TCP 和UDP 的区别
TCP 为什么三次握手而不是两次握手(正解版) HTTP、TCP、IP协议常见面试题
腾讯内推面试题,怎么做到精通http,tcp/ip协议

什么是TCP/IP
TCP/IP 是一组用于实现网络互连的通信协议。
TCP/IP全称是Transmission Control Protocol/Internet Protocol。 IP地址共32位,4字节。

HTTP:
HyperText Transport Protocol)是超文本传输协议的缩写,它用于传送WWW方式的数据

HTTP与HTTPS有什么区别?
HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。

简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

HTTPS和HTTP的区别主要如下:

1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

TCP/IP一般指的是TCP/IP协议簇,主要包括了多个不同网络间实现信息传输涉及到的各种协议 主要包括以下几层:

应用层:主要提供数据和服务。比如HTTP,FTP,DNS等
传输层:负责数据的组装,分块。比如TCP,UDP等
网络层:负责告诉通信的目的地,比如IP等
数据链路层:负责连接网络的硬件部分,比如以太网,WIFI等

TCP的三次握手和四次挥手,为什么不是两次握手?为什么挥手多一次呢? 客户端简称A,服务器端简称B

1)TCP建立连接需要三次握手
A向B表示想跟B进行连接(A发送syn包,A进入SYN_SENT状态)
B收到消息,表示我也准备好和你连接了(B收到syn包,需要确认syn包,并且自己也发送一个syn包,即发送了syn+ack包,B进入SYN_RECV状态)
A收到消息,并告诉B表示我收到你也准备连接的信号了(A收到syn+ack包,向服务器发送确认包ack,AB进入established状态)开始连接。

2)TCP断开连接需要四次挥手

A向B表示想跟B断开连接(A发送fin,进入FIN_WAIT_1状态)
B收到消息,但是B消息没发送完,只能告诉A我收到你的断开连接消息(B收到fin,发送ack,进入CLOSE_WAIT状态)
过一会,B数据发送完毕,告诉A,我可以跟你断开了(B发送fin,进入LAST_ACK状态) A收到消息,告诉B,可以他断开(A收到fin,发送ack,B进入closed状态)

3)为什么挥手多一次 其实正常的断开和连接都是需要四次:

A发消息给B
B反馈给A表示正确收到消息
B发送消息给A
A反馈给B表示正确收到消息。

但是连接中,第二步和第三步是可以合并的,因为连接之前A和B是无联系的,所以没有其他情况需要处理。而断开的话,因为之前两端是正常连接状态,所以第二步的时候不能保证B之前的消息已经发送完毕,所以不能马上告诉A要断开的消息。这就是连接为什么可以少一步的原因。

4)为什么连接需要三次,而不是两次。正常来说,我给你发消息,你告诉我能收到,不就代表我们之前通信是正常的吗?

简单回答就是,TCP是双向通信协议,如果两次握手,不能保证B发给A的消息正确到达。

TCP 协议为了实现可靠传输, 通信双方需要判断自己已经发送的数据包是否都被接收方收到, 如果没收到, 就需要重发。

TCP是怎么保证可靠传输的?

序列号和确认号。比如连接的一方发送一段80byte数据,会带上一个序列号,比如101。接收方收到数据,回复确认号181(180+1),这样下一次发送消息就会从181开始发送了。

所以握手过程中,比如A发送syn信号给B,初始序列号为120,那么B收到消息,回复ack消息,序列号为120+1。同时B发送syn信号给A,初始序列号为256,如果收不到A的回复消息,就会重发,否则丢失这个序列号,就无法正常完成后面的通信了。
这就是三次握手的原因。

TCP和UDP的区别?
TCP提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接(三次握手),之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

UDP 是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。

所以总结下来就是:
TCP 是面向连接的,UDP 是面向无连接的
TCP数据报头包括序列号,确认号,等等。相比之下UDP程序结构较简单。
TCP 是面向字节流的,UDP 是基于数据报的
TCP 保证数据正确性,UDP 可能丢包
TCP 保证数据顺序,UDP 不保证

可以看到TCP适用于稳定的应用场景,他会保证数据的正确性和顺序,所以一般的浏览网页,接口访问都使用的是TCP传输,所以才会有三次握手保证连接的稳定性。而UDP是一种结构简单的协议,不会考虑丢包啊,建立连接等。优点在于数据传输很快,所以适用于直播,游戏等场景。

Android 相关面试知识点

1.Android事件分发机制

Android事件分发机制,大表哥带你慢慢深入
Android面试题精选:讲一讲 Android 的事件分发机制

2.Android 自定义View

Android视图绘制流程完全解析,带你一步步深入了解View
Android图形系统(三)-View绘制流程
Android自定义View篇之(一)View绘制流程
View的绘制流程
自定义控件:
1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。
2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。
3、完全自定义控件:这个View上所展现的内容全部都是我们自己绘制出来的。比如说制作水波纹进度条。

View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,layout方法又回调OnLayout,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。

非UI线程可以更新UI吗?
可以,当访问UI时,ViewRootImpl会调用checkThread方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常。执行onCreate方法的那个时候ViewRootImpl还没创建,无法去检查当前线程.ViewRootImpl的创建在onResume方法回调之后。

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

非UI线程是可以刷新UI的,前提是它要拥有自己的ViewRoot,即更新UI的线程和创建ViewRoot的线程是同一个,或者在执行checkThread()前更新UI。 还有一种情况:当View没有更新宽高时,不会走到checkThread()

3.Handler 详解

面试官带你学安卓 - Handler 这些知识点你都知道吗
Android Handler消息机制原理最全解读
Handler内存泄漏详解及其解决方案

系统为什么提供Handler就是为了切换线程,主要就是为了解决在子线程无法访问UI的问题。
那么为什么系统不允许在子线程中访问UI呢?
因为Android的UI控件不是线程安全的,所以采用单线程模型来处理UI操作,通过Handler切换UI访问的线程即可。

那么为什么不给UI控件加锁呢?
因为加锁会让UI访问的逻辑变得复杂,而且会降低UI访问的效率,阻塞线程执行。

Handler是怎么获取到当前线程的Looper的
大家应该都知道Looper是绑定到线程上的,他的作用域就是线程,而且不同线程具有不同的Looper,也就是要从不同的线程取出线程中的Looper对象,这里用到的就是ThreadLocal。

假设我们不知道有这个类,如果要完成这样一个需求,从不同的线程获取线程中的Looper,是不是可以采用一个全局对象,比如hashmap,用来存储线程和对应的Looper?

所以需要一个管理Looper的类,但是,线程中并不止这一个要存储和获取的数据,还有可能有其他的需求,也是跟线程所绑定的。所以,我们的系统就设计出了ThreadLocal这种工具类。

ThreadLocal的工作流程是这样的:我们从不同的线程可以访问同一个ThreadLocal的get方法,然后ThreadLocal会从各自的线程中取出一个数组,然后再数组中通过ThreadLocal的索引找出对应的value值。

Handler的原理
Android中主线程是不能进行耗时操作的,子线程是不能进行更新UI的。所以就有了handler,它的作用就是实现线程之间的通信。 handler整个流程中,主要有四个对象,handler,Message,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象。

对于Message:

在线程之间传递的消息,它的内部持有Handler和Runnable的引用以及消息类型。可以使用what、arg1、arg2字段携带一些整型数据,使用obj字段携带Object对象;其中有一个obtain()方法,该方法的内部是先通过消息池获取消息,没有再创建,实现了对message对象的复用。其内部有一个target引用,就是对Handler对象的引用,在Looper.loop方法中的消息处理就是通过message的target引用来调用Handler的dispatchMessage()方法来实现消息的处理。

对于Message Queue:

指的是消息队列,是通过一个 单链表 的数据结构维护消息列表的,在插入和删除有优势。其中主要包括两个操作:插入和读取,读取操作本身伴随着删除操作。插入操作是enqueueMessage()方法,就是插入一条消息到MessageQueue中;读取操作是next()方法,它是一个无限循环,如果有消息就返回并从单链表中移除;没有消息就一直阻塞(此时主线程会释放CPU进入休眠状态)。

对于Looper:

Looper在消息机制中进行消息循环,像一个泵,不断地从MessageQueue中查看是否有新消息并提取,交给handler处理。Handler机制一定要Looper,在线程中通过Looper.prepare()为当前线程创建一个Looper,并使用Looper.loop()来开启消息的读取。为什么在平常Activity主线程使用时没有使用到Looper呢?因为对于主线程(UI线程),会自动创建一个Looper 驱动消息队列获取消息,所以Looper可以通过getMainLooper获取到主线程的Looper。 通过quit/quitSafely可以退出Looper,区别在于quit会直接退出,quitSafely会把消息队列已有的消息处理完毕后才退出Looper。

对于Handler:

Handler可以发送和接收消息。发送消息(就是往MessageQueue里面插入一条Message)通过post方法和send方法,而post方法最终也是通过send方法来发送的,最终就会调用sendMessageAtTime这个方法(内部就是调用MessageQueue的enqueueMessage()方法,往MessageQueue里面插入一条消息),同时也会给msg的target赋值为handler本身,进入MessageQueue中。处理消息就是Looper调用loop()方法进入无限循环,获取到消息后就会调用msg.target(Handler本身)的dispatchMessage()方法,进而调用handlerMessage()方法处理消息。

4.Android 线程池的实现原理

1.降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
2.提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
3.提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

java多线程池的支持——ThreadPoolExecutor#####
java的线程池支持主要通过ThreadPoolExecutor来实现,我们使用的ExecutorService的各种线程池策略都是基于ThreadPoolExecutor实现的,所以ThreadPoolExecutor十分重要。要弄明白各种线程池策略,必须先弄明白ThreadPoolExecutor。

Android线程池实现原理
深入理解java线程池—ThreadPoolExecutor
Java线程池的四种实现方法及实现原理及分析
搞懂线程池原理 这篇足够了

5.Android Binder机制

在Linux中,为了避免一个进程对其他进程的干扰,进程之间是相互独立的。在一个进程中其实还分为用户空间和内核空间。这里的隔离分为两个部分,进程间的隔离和进程内的隔离。

既然进程间存在隔离,那其实也是存在着交互。进程间通信就是 IPC,用户空间和内核空间的通信就是系统调用。 Linux 为了保证独立性和安全性,进程之间不能直接相互访问,Android 是基于 Linux 的,所以也是需要解决进程间通信的问题。

其实 Linux 进程间通信有很多方式,比如管道、socket 等等。为什么 Android 进程间通信采用了Binder而不是 Linux 已有的方式,主要是有这么两点考虑:性能和安全

性能。 在移动设备上对性能要求是比较严苛的。Linux传统的进程间通信比如管道、socket等等进程间通信是需要复制两次数据,而Binder则只需要一次。所以Binder在性能上是优于传统进程通信的。

安全。 传统的 Linux 进程通信是不包含通信双方的身份验证的,这样会导致一些安全性问题。而Binder机制自带身份验证,从而有效的提高了安全性。

Binder 是基于 CS 架构的,有四个主要组成部分。

  • Client。 客户端进程。
  • Server。 服务端进程。
  • ServiceManager。 提供注册、查询和返回代理服务对象的功能。
  • Binder 驱动。 主要负责建立进程间的 Binder 连接,进程间的数据交互等等底层操作。

Binder 机制主要的流程是这样的:

  • 服务端通过Binder驱动在 ServiceManager 中注册我们的服务。
  • 客户端通过Binder驱动查询在 ServiceManager 中注册的服务。
  • ServiceManager 通过 inder 驱动返回服务端的代理对象。
  • 客户端拿到服务端的代理对象后即可进行进程间通信。

6.Android性能优化

Android APP性能优化(最新总结)
Android性能优化之APK瘦身详解(瘦身73%)
Android性能优化(一)之启动加速35%
Android apk瘦身实践
Android 性能优化:使用 TraceView 找到卡顿的元凶
Android Systrace 使用详解
Android 性能优化:使用 Lint 优化代码、去除多余资源
Android 性能优化之内存检测、卡顿优化、耗电优化、APK瘦身
今日头条APK瘦身之路
Android性能优化全方面解析
Android 性能优化最佳实践
Android 性能优化必知必会(2019-12-3日更新)
深入探索 Android 包体积优化(匠心制作)
深入探索 Android 内存优化(炼狱级别)

Android 性能监控框架
rabbit-client

微信APM性能监控框架
Matrix

AndroidGodEye是一个可以在PC浏览器中实时监控Android性能数据指标的工具,你可以通过wifi/usb连接手机和pc,通过pc浏览器实时监控手机性能。 AndroidGodEye

LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为github.com/square/leak…
LeakCanary原理解析

BlockCanary是国内开发者MarkZhai开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用
BlockCanaryEx主要是在BlockCanary的基础上,提供了更加详细的dump信息。
Android检测应用卡顿

BlockCanary库可以帮助你记录应用发生卡顿时的堆栈信息和CPU信息

Android 中的性能优化在我看来分为以下几个方面:内存优化、布局优化、网络优化、安装包优化。

a.内存优化: Android的内存优化在我看来分为两点:避免内存泄漏、扩大内存,其实就是开源节流。 其实内存泄漏的本质就是较长生命周期的对象引用了较短生命周期的对象。
常见的内存泄漏

单例模式导致的内存泄漏。 最常见的例子就是创建这个单例对象需要传入一个 Context,这时候传入了一个 Activity 类型的 Context,由于单例对象的静态属性,导致它的生命周期是从单例类加载到应用程序结束为止,所以即使已经 finish 掉了传入的 Activity,由于我们的单例对象依然持有 Activity 的引用,所以导致了内存泄漏。解决办法也很简单,不要使用 Activity 类型的 Context,使用 Application 类型的 Context 可以避免内存泄漏。

静态变量导致的内存泄漏。 静态变量是放在方法区中的,它的生命周期是从类加载到程序结束,可以看到静态变量生命周期是非常久的。最常见的因静态变量导致内存泄漏的例子是我们在 Activity 中创建了一个静态变量,而这个静态变量的创建需要传入 Activity 的引用 this。在这种情况下即使 Activity 调用了 finish 也会导致内存泄漏。原因就是因为这个静态变量的生命周期几乎和整个应用程序的生命周期一致,它一直持有 Activity 的引用,从而导致了内存泄漏。

非静态内部类导致的内存泄漏。非静态内部类导致内存泄漏的原因是非静态内部类持有外部类的引用,最常见的例子就是在 Activity 中使用 Handler 和 Thread 了。使用非静态内部类创建的 Handler 和 Thread 在执行延时操作的时候会一直持有当前Activity的引用,如果在执行延时操作的时候就结束 Activity,这样就会导致内存泄漏。解决办法有两种:第一种是使用静态内部类,在静态内部类中使用弱引用调用Activity。第二种方法是在 Activity 的 onDestroy 中调用 handler.removeCallbacksAndMessages 来取消延时事件。

使用资源未及时关闭导致的内存泄漏。常见的例子有:操作各种数据流未及时关闭,操作 Bitmap 未及时 recycle 等等。

使用第三方库未能及时解绑。有的三方库提供了注册和解绑的功能,最常见的就 EventBus 了,我们都知道使用 EventBus 要在 onCreate 中注册,在 onDestroy 中解绑。如果没有解绑的话,EventBus 其实是一个单例模式,他会一直持有 Activity 的引用,导致内存泄漏。同样常见的还有 RxJava,在使用 Timer 操作符做了一些延时操作后也要注意在 onDestroy 方法中调用 disposable.dispose()来取消操作。

属性动画导致的内存泄漏。常见的例子就是在属性动画执行的过程中退出了 Activity,这时 View 对象依然持有 Activity 的引用从而导致了内存泄漏。解决办法就是在 onDestroy 中调用动画的 cancel 方法取消属性动画。

WebView 导致的内存泄漏。WebView 比较特殊,即使是调用了它的 destroy 方法,依然会导致内存泄漏。其实避免WebView导致内存泄漏的最好方法就是让WebView所在的Activity处于另一个进程中,当这个 Activity 结束时杀死当前 WebView 所处的进程即可,我记得阿里钉钉的 WebView 就是另外开启的一个进程,应该也是采用这种方法避免内存泄漏。

b.布局优化: 布局优化的本质就是减少 View 的层级。常见的布局优化方案如下

  • 在 LinearLayout 和 RelativeLayout 都可以完成布局的情况下优先选择 RelativeLayout,可以减少 View 的层级
  • 将常用的布局组件抽取出来使用 < include >标签
  • 通过 < ViewStub >标签来加载不常用的布局
  • 使用 < Merge >标签来减少布局的嵌套层次

c.网络优化: 常见的网络优化方案如下

  • 尽量减少网络请求,能够合并的就尽量合并
  • 避免 DNS 解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新 IP 的方式,或者在 IP 方式访问失败时切换到域名访问方式。
  • 大量数据的加载采用分页的方式
  • 网络数据传输采用 GZIP 压缩
  • 加入网络数据的缓存,避免频繁请求网络
  • 上传图片时,在必要的时候压缩图片

Android 网络优化,使用 HTTPDNS 优化 DNS,从原理到 OkHttp 集成

d.安装包优化: 安装包优化的核心就是减少 apk 的体积,常见的方案如下

7.Android网络优化及检测

  • 速度:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;4.IP 直连省去 DNS 解析时间
  • 成功率:1.失败重试策略;
  • 流量:1.GZIP 压缩(okhttp 自动支持);2.Protocol Buffer 替代 json;3.优化图片/文件流量;5.文件下载断点续传 ;6.缓存
  • 协议层的优化,比如更优的 http 版本等
  • 监控:Charles 抓包、Network Monitor 监控流量

8.Android屏幕渲染机制

Android屏幕渲染机制

9.Android UI卡顿优化

BlockCanary 也是一个第三方检测UI卡顿的库,项目集成后Block也会自动检测应用运行期间的UI卡顿,并将之输出给我们。
监控Android卡顿的可视化工具
Android UI性能优化 检测应用中的UI卡顿

10.Android APK 打包流程

  • aapt 打包资源文件生成 R.java 文件;aidl 生成 java 文件
  • 将 java 文件编译为 class 文件
  • 将工程及第三方的 class 文件转换成 dex 文件
  • 将 dex 文件、so、编译过的资源、原始资源等打包成 apk 文件
  • 签名
  • 资源文件对齐,减少运行时内存

11.Android App 安装过程

  • 首先要解压 APK,资源、so等放到应用目录
  • Dalvik 会将 dex 处理成 ODEX ;ART 会将 dex 处理成 OAT;
  • OAT 包含 dex 和安装时编译的机器码

12.Android IPC 方式

  • Intent extras、Bundle:要求传递数据能被序列化,实现 Parcelable、Serializable ,适用于四大组件通信
  • 文件共享:适用于交换简单的数据实时性不高的场景
  • AIDL:AIDL 接口实质上是系统提供给我们可以方便实现 BInder 的工具
    • Android Interface Definition Language,可实现跨进程调用方法
    • 服务端:将暴漏给客户端的接口声明在 AIDL 文件中,创建 Service 实现 AIDL 接口并监听客户端连接请求
    • 客户端:绑定服务端 Service ,绑定成功后拿到服务端 Binder 对象转为 AIDL 接口调用
    • RemoteCallbackList 实现跨进程接口监听,同个 Binder 对象做 key 存储客户端注册的 listener
    • 监听 Binder 断开:1.Binder.linkToDeath 设置死亡代理;2. onServiceDisconnected 回调
  • Messenger:基于 AIDL 实现,服务端串行处理,主要用于传递消息,适用于低并发一对多通信
  • ContentProvider:基于 Binder 实现,适用于一对多进程间数据共享
  • Socket:TCP、UDP,适用于网络数据交换。

13.Android 广播动态注册和静态注册

  • 动态注册广播不是常驻型广播,也就是说广播跟随 Activity 的生命周期。注意在 Activity 结束前,移除广播接收器。 静态注册是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。
  • 当广播为有序广播时:优先级高的先接收(不分静态和动态)。同优先级的广播接收器,动态优先于静态
  • 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。
  • 当广播为默认广播时:无视优先级,动态广播接收器优先于静态广播接收器。同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后册的。

14.Android Framework 相关

15.Android OKHttp源码解析

OKHttp源码解析
Android面试题:okhttp
Android OkHttp完全解析 是时候来了解OkHttp了

16.Android Glide源码分析

Glide源码分析
面试官:简历上最好不要写Glide,不是问源码那么简单 郭林-Android图片加载框架最全解析(一),Glide的基本用法

17.Android APK资源加载流程

Android APK资源加载流程

18.Android应用启动流程

应用进程不存在的情况下,从点击桌面应用图标,到应用启动(冷启动),大概会经历以下流程:

  • Launcher startActivity
  • AMS startActivity
  • Zygote fork 进程
  • ActivityThread main()
    4.1. ActivityThread attach
    4.2. handleBindApplication
    4.3 attachBaseContext
    4.4. installContentProviders
    4.5. Application onCreate
  • ActivityThread 进入loop循环
  • Activity生命周期回调,onCreate、onStart、onResume… 整个启动流程我们能干预的主要是 4.3、4.5 和6,应用启动优化主要从这三个地方入手。理想状况下,这三个地方如果不做任何耗时操作,那么应用启动速度就是最快的,但是现实很骨感,很多开源库接入第一步一般都是在Application onCreate方法初始化,有的甚至直接内置ContentProvider,直接在ContentProvider中初始化框架,不给你优化的机会。

今日头条APP启动很快,原来是做了这些优化?-讲解了MultiDex原理
Android手机从开机到APP启动经过的流程

19.Android ClassLoader加载原理

ClassLoader.loadClass -> DexPathList.loadClass -> 遍历dexElements数组 ->DexFile.loadClassBinaryName

不管是 PathClassLoader还是DexClassLoader,都继承自BaseDexClassLoader

  • 构造方法通过传入dex路径,创建了DexPathList。
  • ClassLoader的findClass方法最终是调用DexPathList 的findClass方法
  • DexPathList里面定义了一个dexElements 数组,findClass方法中用到
  • findClass方法逻辑很简单,就是遍历dexElements 数组,拿到里面的DexFile对象,通过DexFile的loadClassBinaryName方法加载一个类。
  • 最终创建Class是通过native方法

双亲委托机制

  • 当前类加载器首先检查目标类是否已经被加载过,有则直接返回
  • 当前类加载器会先委托父类加载器加载目标类,如果未设置父加载器,则检查辅助加载器是否支持查询加载目标类
  • 只有上述加载器找不到目标类的时候,才会调用当前类加载器(Child) 查询路径寻找目标类。

以上这么做的好处是:一方面防止目标类的重复加载,另外一方面 主要考虑安全因素,防止有人重写原生类,比如说java.lang.String这样的数据类型,替换原生的String类,加载到JVM中,造成严重的安全问题。

双亲委托机制 在Android热修复领域中也有着广泛的应用。每个ClassLoader可以有多个dex文件,每个dex文件是一个Element,多个dex文件组成一个dexElements,类加载器寻找类的时候,会遍历dexElements中的dex文件,再通过dex文件遍历目标类。由于双亲委托机制的存在,寻找到目标类后就直接返回,不再寻找其他dex文件下该目标类,热修复的原理就是hook住ClassLoader,使其先加载修复后的目标类,而存在的BUG的目标类不会被加载。

android动态加载ClassLoader机制
Android Classloader动态加载分析
Android ClassLoader加载过程源码分析

20.Android热修复

Android热修复总结
Android热修复(一):底层替换、类加载原理总结 及 DexClassLoader类加载机制源码探索
Android热更新方案Robust
Android热更新方案Robust开源,新增自动化补丁工具

21.Android全面插件化RePlugin流程与源码解析

Android全面插件化RePlugin流程与源码解析

22. Android MVC、MVP、MVVM

MVC、MVP、MVVM,我到底该怎么选?

最后梳理下大牛的博客:

史上最全的Android面试题集锦

Android 面试题(附答案) | 掘金技术征文
面试字节跳动Android研发岗
Android 高级开发面试题以及答案整理
国内一线互联网公司内部面试题库 Android 面试准备进行曲 (Android基础进阶 一 )v1.3
互联网大型公司(阿里腾讯百度等)android面试题目(有答案)

Android 中高级面试必知必会
2018大厂Android面试经验 | 掘金技术征文
2018届android校招面试总结:百度,大疆,乐视,知乎
Android性能优化
面试某度Android岗,以为没过但意外收到Offer,我却拒绝了
一位Android资深工程师对移动端架构的思考

android源码下载地址 (git clone android.googlesource.com/platform/fr…)

面试相关资料网站

常用的数据结构与算法知识点

Java8种排序算法下饭总结

链表相关算法题

单链表的结点结构

  • data域:存储数据元素信息的域称为数据域; 
  • next域:存储直接后继位置的域称为指针域,它是存放结点的直接后继的地址(位置)的指针域(链域)。
  • data域+ next域:组成数据ai的存储映射,称为结点;

注意:

  • ①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。
  • ②每个结点只有一个链域的链表称为单链表(Single Linked List)。

1.单链表反转

结题思路参照:【图文解析】反转一个单链表

编写思路: 所谓的单链表反转就是将链表的指针方向改变。但是,由于单链表没有指向前一个结点的指针,所以我们定义一个指向前一个结点的指针pre,用于存储每一个节点的前一个结点。接下来还需要定义一个保存当前结点的指针cur,以及下一个节点的next。定义好之后,遍历单链表,将当前结点的指针指向前一个结点,之后定义三个指针向后移动,直至遍历到最后一个结点为止。
如果不考虑空间复杂度,我们可以借助栈(先进后出)来实现

 public static Node reverseListNode(Node head){
        //单链表为空或只有一个节点,直接返回原单链表
        if (head == null || head.getNext() == null){
            return head;
        }
        //前一个节点指针
        Node preNode = null;
        //当前节点指针
        Node curNode = head;
        //下一个节点指针
        Node nextNode = null;
        while (curNode != null){
            nextNode = curNode.getNext();//nextNode 指向下一个节点
            curNode.setNext(preNode);//将当前节点next域指向前一个节点
            preNode = curNode;//preNode 指针向后移动
            curNode = nextNode;//curNode指针向后移动
        }
        return preNode;
    }

2.两个单链表相交,返回相交的第一个节点

结题思路参照:
两个单链表相交,返回相交的第一个节点
判断两个链表是否相交并找出第一个相交节点
此题结题思路:
数据结构的链表定义中存储了指向下一个元素的指针,如果当两个链表有交点时,即两个链表会在各自链表中的某个结点,同时指向了相同的下一个结点,即如下图所示:

图中可以看出,从链表一和链表二的相交处开始,后续结点都为相同结点。

此外,还要记住以下几点:

①相交的两个单链表要么均有环,要么都没有环
②一个有环的单链表和一个无环的单链表不可能相交

3.二分查找算法

结题思路参照:
java实现二分查找算法
二分查找(binary search),也称折半搜索,是一种在 有序数组 中 查找某一特定元素 的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

时间复杂度:折半搜索每次把搜索区域减少一半,时间复杂度为O(log n)。(n代表集合中元素的个数)
空间复杂度: O(1)。虽以递归形式定义,但是尾递归,可改写为循环。

4.二叉树前序、中序、后序遍历

结题思路参照:
二叉树前序、中序、后序遍历相互求法

5.找出一个无序数组中出现超过一半次数的数字

结题思路参照:
数组中出现次数超过一半的数字

6.必须知道的八大种排序算法【java实现】

7.K个一组翻转链表

结题思路参照:
K个一组翻转链表

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {

        if(head ==null || head.next==null){
            return head;
        }

        //创建一个头节点
        ListNode dummyNode = new ListNode(-1);
        dummyNode.next = head;

        ListNode pre = dummyNode;
        ListNode begin = dummyNode.next;
        ListNode end = null;
        ListNode pNext = null;

        while(begin !=null){
            end=begin;
            //找到begin开始第k个节点
            for(int i=1;i< k;i++){
                if(end.next !=null){
                    end= end.next;
                }else{
                    //链表长度小于K个则返回
                    return dummyNode.next;
                }
            }
            //保存下一个开始节点
            pNext = end.next;

            end.next=null;
            //K个节点的链表反转
            pre.next = reverseList(begin);
            
            //把链表接上
            begin.next= pNext;
            
            //链表往后移
            pre = begin;
            begin = pNext;

        }

        return dummyNode.next;
    }

    //递归反转链表
    public ListNode reverseGroup(ListNode head){
        if(head ==null || head.next==null){
            return head;
        }

        ListNode p = reverseGroup(head.next);
        head.next.next = head.next;
        head.next=null;
        return p;
    }

    //对不带头节点的单链表翻转
    public ListNode reverseList(ListNode head) {
        //排除空表和单节点情况
        if (head == null || head.next == null) {
            return head;
        }
        ListNode pre = null;
        ListNode cur = head;
        ListNode next = null;
       
        while(cur !=null){
            //记录下一个节点
            next = cur.next;
            cur.next =pre;
            pre = cur;
            cur = next;
        }
        head =pre;
        return head;
    }
}

算法题未完待续。。。(后续不断添加)