update time 2021年04月01日19:39:16 ,阅读时间30分钟, 文章版本:V 1.4。主要收集在面试过程中普遍问到的基础知识(面试收集 主要来自于bilibili 嵩恒 蚂蚁金服等互联网公司)
由于总结的东西很多很乱,所以知识点并没有深入探讨,很多小标题的东西都可以写成一篇单独的总结,这里偷懒直接放在一起汇总了。
中级Android面试总结之Android篇 链接
- 网络
- TCP UDP
- HTTP HTTPS
- TCP UDP
- Java基础
- 内存模型
- 类加载过程
- 线程/进程
- sleep wait 等 区别
- 多线程
- 线程池
- synchronized 原理
- 锁问题
- Jvm 调用方法过程
- GC清理算法
- JAVA反射
- JAVA泛型
- 数据结构
- ArrayList 、LinkedList 和SparseArray
- HashMap HashTable TreeMap LinkedHashMap ArrayMap
- 常用设计模式及源码使用
参考文章
网络
网络模块 面试主要涉及到 应用层(HTTP DNS等) 和 传输层 (TCP UDP)的东西,
TCP UDP
TCP 连接 :传输可靠;有序;面向字节流;速度慢;较重量;全双工; 适用于文件传输、浏览器等
全双工:A 给 B 发消息的同时,B 也能给 A 发
半双工:A 给 B 发消息的同时,B 不能给 A 发
UDP 无连接 :传输不可靠;无序;面向报文;速度快;轻量; 适用于即时通讯、视频通话等
三次握手四次挥手
参考文章
- 握手:
- 建立连接,客户端发送syn包(syn=x)到服务器,客户端进入SYN_SENT状态,等待服务器确认;
- 服务器收到syn包,首先确认客户的SYN(ack=x+1),自己向客户端发送一个SYN包(syn=y),此时服务器进入SYN_RECV状态;
- 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),发送完毕后,客户端和服务器双方进入ESTABLISHED(TCP连接成功)状态,握手完成!
SYN:同步序列编号(Synchronize Sequence Numbers)。
- 挥手:
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。所以服务器结束TCP连接的时间要比客户端早一些。
HTTP HTTPS
HTTP 是超文本传输协议,明文传输;HTTPS 使用 SSL 协议对 HTTP 传输数据进行了加密
HTTP 默认 80 端口;HTTPS 默认 443 端口
优点:安全
缺点:费时、SSL 证书收费,加密能力还是有限的,但是比 HTTP 更加安全,也是大势所趋。
Get 参数放在 url 中;Post 参数放在 request Body 中
Get 可能不安全,因为参数放在 url 中,并且对于传送的数据长度有限制。
Java基础
内存模型
- 栈:储存局部变量 (线程私有 使用完毕就会释放)
- 堆:储存 new 出来的东西、static类型变量 成员方法等 (线程共享 使用完毕 等待gc清理)
- 方法区: 对象的运行过程 (线程共享)
- 本地方法区:为系统方法使用 (线程私有)
- 寄存器:为CPU提供
- 程序计数器:指向当前线程正在执行的指令的地址,确保多线程下正常运行(线程私有)
参考文章 全面理解java内存模型
类加载过程
- 加载:获取类的二进制字节流;生成方法区的运行时存储结构;在内存中生成 Class 对象
- 验证:确保该 Class 字节流符合虚拟机要求
- 准备:初始化静态变量(基本类型 、引用类型初始值null、final static值)
- 解析:将常量池的符号引用替换为直接引用 (类方法、变量直接指向内存引用地址或偏移量)
- 初始化:执行静态块代码、类变量赋值 (new 类型)
- 使用
- 卸载
线程/进程
进程:进程是系统进行资源分配和调度的一个独立单位 (拥有独立内存空间),一个app就是一个进程,进程包含线程。
线程:是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一些在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
sleep wait 等 区别
wait(): 当一个线程执行到wait()方法时,它就进入到一个等待池中,同时释放对象锁,使得其他线程可以访问。用户可以使用notify,notifyAll或者指定睡眠时间来唤醒当前等待池中的线程。
sleep():该函数是Thread的静态函数,作用是使调用线程进入阻塞状态(blocke)。因为sleep()是Thread类的Static方法,因为它不能改变对象的机制。所以,调用sleep方法时,线程虽然休眠了,但是对象的机制并没有被释放,其他线程无法访问这个对象
注意:wait方法,notify方法,notifyAll方法必须放在synchronized block中,否则会抛出异常。
join():等待目标线程执行完成之后再继续执行
yield():线程礼让,线程进入就绪状态(ready)。目标线程由运行状态转换为就绪状态,也就是让出执行权限,让其他线程得以优先执行,但其他线程能否优先执行是未知的。
多线程
synchronized 参考文章
线程池
线程池参考文章
synchronized 原理
Synchronized可以把任何一个非null对象作为"锁",其中都有一个监视器锁:monitor。Synchronized的语义底层是通过一个monitor的对象来完成。
- 同步代码块(synchronize(this)的方式)会执行 monitorenter 开始,motnitorexit 结束,当线程进入monitor,如果为0 则改为1 并分配使用,如果为1 则需要挂起等待。其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
- 同步代码块采用( synchronized void method()的方式)调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
synchronized 在JDK 1.6以后的优化
自适应自旋锁 :指一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放(自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定),而不是进入线程挂起或睡眠状态(因为为了很短的等待时间就去挂起唤醒会 更低效)。
锁消除:但是在有些情况下,JVM检测到不存在共享数据竞争,JVM会对这些同步锁进行锁消除。
锁粗化 :就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
偏向锁、轻量级锁:轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时通过标识,避免再走各种加锁/解锁流程,达到进一步提高性能。
synchronized 添加在非静态方法上为 对象锁,如果在静态方法上为类锁。
锁问题
可重入锁 :已经获取到锁后,再次调用同步代码块/尝试获取锁时不必重新去申请锁,可以直接执行相关代码。 ReentrantLock 和 synchronized 都是可重入锁
公平锁 : 等待时间最久的线程会优先获得锁 , synchronized lock 默认都为非公平锁
锁主要分为:悲观锁 (线程一旦得到锁,其他线程就挂起等待。用于写入频繁的 synchronized)和 乐观锁(假设没有冲突,不加锁,更新数据时判断该数据是否过期,过期的话则不进行数据更新,适用于读取操作频繁的场景,比如 AtomicInteger、AtomicLong、AtomicBoolean)
锁的状态依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
volatile :只能用来修饰变量,适用修饰可能被多线程同时访问的变量,相当于轻量级的synchronized,保证有序性和 可见性(变量位于主内存中,每个线程还有自己的工作内存,变量在自己线程的工作内存中有份拷贝,线程直接操作的是这个拷贝被 volatile 修饰的变量改变后会立即同步到主内存)。
synchronized :是java 的关键字(悲观锁),自动释放锁,并且无法中断锁。保证 原子性、可见性 和 有序性。
Lock : java 中的一个接口,lock 需要手动释放,所以需要写到 try catch 块中并在 finally 中释放锁,可以手动中断锁。
双重检查单例加volatile的原因 : 将instance =newInstance(); 创建实例分为三个过程 ,1.分配内存 2.初始化 3.将instance指向分配的内存空。但是如果 在另一个线程中想要使用instance,发现instance!=null,但是实际上instance还未初始化完毕这个问题。
Jvm 调用方法过程
- 在运行前确认方法中的 局部变量的数量、操作栈大小 和 方法参数数量
- JVM在执行方法中 会将 局部变量,操作数,方法返回地址等信息 保存在 栈帧中,一个线程中 只有位于顶部的栈帧处于活动中
GC清理算法
- 程序计数法
- 程序可达性
清理算法
- 复制回收算法 :内存使用率50%,只使用一半内存, 地址连贯,多用于新生代。
- 标记清除算法:使用率100%,地址碎片化,多用于老年代
- 标记整理算法:在标记清除基础上,对于存活对象进行整理,多用于碎片较多的老年代。
JAVA反射
反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
反射过程:获取反射的 Class 对象、通过反射创建类对象、通过反射获取类属性方法及构造器。
获取类的 Class 对象实例
Class clz = Class.forName("com.Apple");
根据 Class 对象实例获取 Constructor 对象
Constructor appleConstructor = clz.getConstructor();
使用 Constructor 对象的 newInstance 方法获取反射类对象
Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
获取方法的 Method 对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
利用 invoke 方法调用方法
setPriceMethod.invoke(appleObj, 14);
反射 参考文章
JAVA泛型
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
- 泛型类: 如List、Set、Map。
- 泛型接口:如interface Generator
,主要用于生产器 - 泛型方法:如public
T showKeyName(Generic container)
数据结构
ArrayList 、LinkedList 和SparseArray
ArrayList :基于动态数组实现,所以查找快(O1) 增删慢 (On),如果创建方法无参数,默认数组长度为10,添加元素如果需要扩容则是新创建一个 数组,进行数据的转移拷贝。删除的时候 如果删除成功,后续元素需要通过System.arraycopy 进行元素移动(相当的低效)。改查 则比较迅速。
LinkedList : 底层基于双向链表,维持了 last 和 next 两个节点,所以 查找慢 (On) 增删快(O1),链表的查找是循环的(不过查找的过程是 先判断index是靠近哪一段 然后再进行查找 可以理解为 O(n/2)),但是速度还是慢。在添加和删除中,因为添加是直接放到链表尾部 但是删除存在 先循环一遍,然后删除的情况,不过 相对于ArrayList的复制要好的很多了。
SparseArray : key 只能为整数型,内部也是两个数组,一个存key,一个存val , 添加的时候 key 不用装箱,val则需要如果是基本类型,查找key还是用的二分法查找。也就是说它的时间复杂度还是O(logN)
HashMap HashTable TreeMap LinkedHashMap ArrayMap
- HashMap : jdk8后由数组、链表和红黑树组成,基于散列表实现。当数组中的数据出现 hash冲突的时候启动链表,当链表中的 个数超过8个 将链表转为红黑树。允许 key val 为NULL,key和 val存储数据可以为任何类型(非基本类型 会自动装箱)。HashMap并不是线程安全的,可以通过 Collections.synchronizedMap(new HashMap)的方式获得线程的Hahsmap,或者使用 下边的ConcurrentHashMap。
HashMap 源码参考文章
TreeMap : 相比较于HashMap,TreeMap实现SortedMap接口,所以TreeMap是有序的!HashMap是无序的。
HashTable : 其实主体和 HashMap类似,但是写入和 读取方法 添加了 synchronize 可以做到 线程安全,key 和val 不能为null,但是效率没有HashMap高。
ArrayMap :key val 可以为 任意类型(非基本类型,会自动装箱),中有两个数组,一个存储key 的hash值,另外一个交叉存储 key val 数据(key val key val .... 形式存储)
LinkedHashMap: LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
- ConcurrentHashMap :ConcurrentHashMap由多个segment 组成,每个segment 包含一个Entity 的数组。这里比HashMap 多了一个segment 类。该类继承了ReentrantLock 类,所以本身是一个锁。当多线程对ConcurrentHashMap 操作时,不是完全锁住map, 而是锁住相应的segment 。这样提高了并发效率。缺点:当遍历ConcurrentMap中的元素时,需要获取所有的segment 的锁,使用遍历时慢。锁的增多,占用了系统的资源。使得对整个集合进行操作的一些方法
ConcurrentHashMap 和 HashMap不多的博客推荐 传送门
常用设计模式及源码使用
- 单例模式
初始化比较复杂,并且程序中只需要一个。避免重复创建消耗内存
Android中 获取WindowManager服务引用 WindowManager wm = (WindowManager)getSystemService(getApplication().WINDOW_SERVICE);
l另外一种不错实现单例的方式 使用 eunm,
public class Singleton {
private static volatile Singleton s;
private Singleton(){};
public static Singleton getInstance() {
if(s == null) {
synchronized (Singleton.class) {
if(s == null) {
s = new Singleton();
}
}
}
return s;
}
}
- 创建者模式
创建某对象时,需要设定很多的参数(通过setter方法),但是这些参数必须按照某个顺序设定
Android 中 创建所有的 Dialog 中使用的
public class TestClient {
private int index;
private String name;
public TestClient() {
this(new Builder());
}
public TestClient(Builder builder){
this.index = builder.index;
this.name = builder.name;
}
public static final class Builder {
private int index;
private String name;
public Builder() {
this.index = 1;
this.name = "xxx";
}
public Builder(TestClient testClient){
this.index = testClient.index;
this.name = testClient.name;
}
public Builder setIndex(int index) {
this.index = index;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public TestClient build(){
return new TestClient(this);
}
}
}
- 原型模式
- 工厂模式
定义一个创建对象的工厂,根据不同传参 创建不同的对象。
Android 中 BitmapFactory 和 Iterator 根据循环对象不同返回不同的对象 - 策略模式
有一系列的算法,将算法封装起来(每个算法可以封装到不同的类中),各个算法之间可以替换,策略模式让算法独立于使用它的客户而独立变化
Android 中的 时间插值器,可以使用不同的 加速 减速 或者自定义加速器 展示不同的动画效果 - 责任链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接受者直接的耦合关系,将这些对象连成一条链,并沿这条链传递该请求,直到有对象处理它为止。
Android 中有 View 点击事件分发 或者 第三方库 OKHttp 中的拦截器 - 命令模式
命令模式将每个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;将请求进行排队或者记录请求日志,以及支持可撤销操作。
Android 事件机制中,底层逻辑对事件的转发处理。每次的按键事件会被封装成NotifyKeyArgs对象,通过InputDispatcher封装具体的事件操作 / Runable实现中封装我们需要的实现 - 观察者模式
Java的Observable类和Observer接口就是实现了观察者模式。一个Observer对象监视着一个Observable对象的变化,当Observable对象发生变化时,Observer得到通知,就可以进行相应的工作。 - 中介者模式
在Binder机制中,即ServiceManager持有各种系统服务的引用 ,当我们需要获取系统的Service时,首先是向ServiceManager查询指定标示符对应的Binder,再由ServiceManager返回Binder的引用。并且客户端和服务端之间的通信是通过Binder驱动来实现,这里的ServiceManager和Binder驱动就是中介者。 - 代理模式
给某一个对象提供一个代理,并由代理对象控制对原对象的引用 (,静态代理 和 动态代理) - 适配器模式
把一个类的接口变换成客户端所期待的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
参考文章