面试题总结
1 JAVA中的强软弱虚引用分别有什么使用场景?
如果一个对象具有强引用(Strong Reference),垃圾回收器就不会回收这个对象,即使内存空间不足。
如果一个对象具有软引用(Soft Reference),垃圾回收器在内存空间不足时可能会回收这个对象。通常用来实现类似缓存的功能
如果一个对象只具有弱引用(Weak Reference),那么垃圾回收器在下一次运行时一定会回收这个对象。通常用来实现类似键值对的功能。
如果一个对象只具有虚引用(Phantom Reference),那么就相当于没有任何引用。通常用来跟踪对象被垃圾回收的状态,比如 LeakCanary 的实现。
2 哪些情况下会出现OOM?
长时间的内存泄漏。
对象过大或者过多。
递归调用或循环使用不当。
3 JVM中运行时数据区有哪些组成部分,每个部分的作用是什么?
程序计数器
:可以看作是当前线程所执行的字节码的行号指示器,线程私有,确保线程切换后能恢复到正确的执行位置。虚拟机栈
:线程私有,描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。栈帧随着方法的进入和退出而动态地压栈和出栈。本地方法栈
:线程私有,功能和虚拟机栈类似,不过是为虚拟机使用到的 Native 方法服务的。堆
:线程共享,用于存放对象实例。方法区
:线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
4 Service有哪些生命周期?
onCreate()
:在Service首次创建时被调用,用于进行一些初始化操作,例如绑定数据库、设置监听器等。onStartCommand()
:在Service启动后被调用,用于处理Intent请求并执行相应的后台操作。该函数返回一个整数值,指示Service如何处理后续的请求。onBind()
:用于绑定Service到客户端组件,例如Activity或Fragment。在绑定过程中,客户端可以通过IBinder接口与Service进行通信。onUnbind()
:在客户端组件与Service解除绑定时被调用,用于清理资源或执行其他操作。onDestroy()
:在Service销毁前被调用,用于释放资源或执行一些清理操作,例如关闭数据库连接、取消注册监听器等。
5 Okhttp底层实现是怎样的,有阅读过Okhttp源码吗?
基于Socket的TCP通信
:OkHttp使用底层的Socket套接字进行与服务器的TCP通信。它通过建立和管理Socket连接,实现了可靠的数据传输。连接池管理
:OkHttp通过连接池管理多个TCP连接,以提高性能和效率。连接池会维护空闲连接,并重用它们来发送请求,避免了频繁的建立和关闭连接的开销。请求和响应拦截器
:OkHttp使用拦截器链来处理请求和响应。拦截器可以在发送请求前和接收响应后对数据进行处理,例如添加请求头、处理响应结果等。异步和并发处理
:OkHttp支持异步和并发处理,它使用线程池来执行请求和处理响应,从而实现了高效的并发网络请求。缓存支持
:OkHttp提供了缓存功能,可以将响应结果缓存在本地,避免重复的网络请求。它支持设置缓存的大小、过期时间等参数,并提供了灵活的缓存策略。
6 LiveData使用过吗?它的 setValue() 方法和 postValue() 方法有什么区别?
setValue()
方法用于在主线程中设置 LiveData 的值。它必须在主线程中调用。当调用 setValue()后,LiveData 会立即分发新的值给所有活跃的观察者。postValue()
方法用于在后台线程中设置 LiveData 的值。它可以在任意线程中调用。但需要注意,当调用postValue()时,LiveData 不会立即分发新的值,而是将更新延迟到主线程中。
7 读过LiveData源码吗?实现原理是怎样的?
观察者模式
:LiveData 使用观察者模式(Observer Pattern)来实现数据更新通知。LiveData 包含一个观察者列表,当数据发生变化时,LiveData 会遍历观察者列表,并通知每个观察者数据已更新。生命周期感知
:LiveData 可以感知其关联的生命周期所有者(如 Activity、Fragment)的生命周期状态。当观察者的生命周期处于活动状态(STARTED 或 RESUMED)时,LiveData 会通知观察者数据更新;当生命周期处于非活动状态(DESTROYED)时,LiveData 不会通知观察者,避免造成内存泄漏。线程安全
:LiveData 内部使用了线程安全的机制,确保数据更新操作在主线程执行,从而避免多线程并发访问导致的数据不一致性问题。数据更新
:通过 setValue() 和 postValue() 方法来更新 LiveData 中的数据。setValue() 方法必须在主线程调用,而 postValue() 方法可以在任意线程调用,它会将数据更新操作 post 到主线程执行。数据持有
:LiveData 会持有最新的数据,并在观察者注册后立即发送数据更新通知,确保观察者能够收到最新的数据。
8 为什么要使用 Double-Check Locking 来实现单例模式?
第一次检查
:在获取单例实例时,首先检查实例是否已经被创建。如果实例已经存在,则直接返回该实例,无需再进行后续的加锁和实例化操作,提高了程序的性能。加锁
:如果实例尚未被创建,则使用同步块对代码进行加锁,确保只有一个线程可以进入临界区。第二次检查
:在加锁的临界区内部,再次检查实例是否已经被创建。这是为了防止多个线程同时通过了第一次检查,在竞争锁的过程中,其中一个线程已经创建了实例,而另一个线程不知道,继续创建新的实例。
此外,在早期的 Java 版本中,由于指令重排序的影响,双重检查锁可能存在线程安全性问题。为了解决这个问题,可以将单例实例声明为 volatile 类型,以禁止指令重排序。
9 volatile可以保证原子性吗?
volatile 只能保证单次读/写操作的原子性,如果对该变量进行多次读/写操作,就不能保证整个操作的原子性。
但是volatile可以保证变量的可见性和有序性。
10 安卓组件间通信是用什么实现的,比如如果使用的是ARouter路由,怎么在一个模块中访问另一个模块的类?
在Android应用中,组件间通信可以通过多种方式实现,其中包括使用
广播、Intent、接口回调、EventBus
等方式。另外,像ARouter这样的路由框架也是一种常用的组件间通信方式,可以方便地实现跨模块的通信和导航。
如果你在Android项目中使用了ARouter路由框架,要在一个模块中访问另一个模块的类,可以在需要暴露的类或接口上添加ARouter注解:@Route(path = “/module/xxx”)
在需要访问其他模块的地方,通过ARouter提供的API获取对应的类实例,示例代码如下:
ExampleClass exampleClass = (ExampleClass) ARouter.getInstance().build(“/module/xxx”).navigation();
11 安桌进程间通信(IPC)有哪些方式?
Intent
:使用Intent对象进行通信,通过startActivity、startService、sendBroadcast等方法触发对应的组件进行处理。Binder
:Binder是Android中的进程间通信机制,可以跨进程共享数据、调用服务等,主要用于AIDL接口实现和远程服务等场景。Messenger
:Messenger 是基于 Binder 的轻量级通信方式,可以传递 Message 对象进行通信,通常适用于简单的跨进程通信场景。ContentProvider
:ContentProvider可以允许不同应用程序之间共享数据,并且可以通过Uri地址来识别不同的数据,通常用于数据共享和数据同步等场景。Socket
:Socket方式是一种基于网络通信的进程间通信方式,可以实现跨进程或跨设备的数据交换,通常用于需要高效的数据传输和远程服务调用等场景。
12 Handler的原理是什么?维护的MessageQueue是什么数据结构?
Handler是基于Android中的
消息循环机制
来实现的。Handler内部维护了一个消息队列(MessageQueue),用于存储待处理的消息。
当我们创建一个Handler对象时,它会自动和当前线程的Looper(消息循环)关联起来。Looper负责不断地从消息队列中取出消息,并将消息分发给对应的Handler进行处理。
当我们调用Handler的sendMessage()方法发送消息时,实际上是将一个消息对象(Message)加入到消息队列中。Looper会不断地轮询消息队列,当有消息时,会将消息分发给目标Handler的handleMessage()方法进行处理。
MessageQueue在底层是通过单链表
实现的,每一个消息都封装在一个Message对象中,包括消息的目标Handler、消息类型、消息内容等信息。当消息被处理后,它会被从消息队列中移除,Looper会继续处理下一个消息,直到消息队列为空。
13 Handler的postDelay()方法是如何实现的?
Handler的postDelayed()方法是用于在指定延迟时间后向消息队列发送一个Runnable对象。它的实现依赖于Message和MessageQueue的机制。
调用postDelayed()方法时,会创建一个Message对象,并设置它的target为当前的Handler对象。Message对象中包含了一个Runnable对象,表示需要延迟执行的任务。
计算出指定延迟时间后的绝对时间点,然后将该Message对象插入到MessageQueue中。Android系统会根据消息的触发时间对消息队列进行排序,以确保消息按照预期的顺序执行。
当消息的触发时间到达时,Looper会从消息队列中取出该消息,并将相关的Runnable对象交给目标Handler的handleMessage()方法进行处理。
14 HashMap和HashTable的区别是什么?HashMap底层是什么数据结构?
线程安全性:HashTable是线程安全的,所有的方法都是同步的,即通过synchronized关键字实现的。而HashMap不是线程安全的。
null值的处理
:HashMap允许key和value为null,而HashTable不允许null作为key或value。遍历顺序
:HashMap的遍历顺序是不确定的,而HashTable是按照插入顺序来遍历的。
HashMap的底层数据结构是数组+链表/红黑树
。在JDK 8及以上版本中,当链表长度超过阈值(8)时,会将链表转化为红黑树,以提高查找、插入和删除的效率。
15 CurrentHashMap知道吗?他和HashMap的区别是什么?
线程安全性
:ConcurrentHashMap是线程安全的,其内部实现采用了分段锁(Segment)机制,这样在多线程并发访问时,每个Segment可以独立加锁,降低了锁的粒度,提高了并发性能。而HashMap不是线程安全的,需要外部同步来确保线程安全。效率
:ConcurrentHashMap在大多数情况下比HashMap具有更好的性能,特别是在并发读写操作频繁的场景下。由于ConcurrentHashMap采用了分段锁机制,允许多个线程同时读取数据而不会阻塞,因此在并发性能上有更好的表现。扩容
:ConcurrentHashMap在扩容时不会将整个表锁住,只会锁住需要进行操作的部分,可以提高并发性能。而HashMap在扩容时需要将整个表锁住,可能导致在扩容期间其他线程无法进行操作。
16 你用过哪些常用的线程池?单线程线程池的4个参数作用是什么?
常见的线程池包括:
FixedThreadPool
(固定大小线程池):该线程池中的线程数量是固定的,适用于需要控制并发数量的场景。CachedThreadPool
(可缓存线程池):该线程池中的线程数量不固定,会根据任务的数量动态调整线程数量。适用于短时异步任务较多的情况。SingleThreadExecutor
(单线程线程池):该线程池只有一个工作线程,逐个执行提交的任务。适用于需要保证任务按照顺序执行的场景。ScheduledThreadPool
(定时任务线程池):该线程池可以在指定的时间执行任务,也可以周期性地执行任
单线程线程池的参数及作用:corePoolSize
:核心线程数,即线程池中始终保持的线程数量,即使线程处于空闲状态也不会被回收。maximumPoolSize
:最大线程数,线程池中允许的最大线程数量。当队列满了且活动线程数达到核心线程数时,线程池会尝试创建新的线程来处理任务,但不会超过最大线程数。keepAliveTime
:线程空闲时间,即当线程池中的线程数量超过核心线程数时,多余的空闲线程在空闲时间超过 keepAliveTime 后会被回收。unit
:空闲时间的单位,通常为 TimeUnit 类型,如 TimeUnit.SECONDS。
17 如果一个Task被放入现有SingleThreadExecutor中,会依次发生什么?
如果线程池中的工作线程正在忙碌执行其他任务,则新的任务会被放入等待队列中。
如果线程池中的工作线程没有任务在执行,那么该线程将立即执行新的任务。
如果线程池中的工作线程已经被关闭(shutdown),则新的任务将被拒绝,并抛出 RejectedExecutionException 异常。
如果线程池中的工作线程由于执行任务时抛出异常而终止,那么线程池将创建一个新的工作线程来代替它,并执行新的任务。
如果线程池中的工作线程正常终止,那么线程将被从线程池中移除,并且线程池会检查等待队列中是否有等待执行的任务。如果有,那么线程池会选择一个任务分配给新的工作线程执行,否则该线程将被终止。
18 协程的原理有了解过吗?
协程是一种轻量级的线程,可以在不同的执行单元(如函数、方法)之间切换执行,从而实现非抢占式多任务处理。协程可以实现在单个线程内并发执行多个任务,避免了传统多线程机制中线程切换的开销,提高了程序的性能和效率。
协程的原理主要基于以下几点:用户态调度
:协程的调度是在用户态下完成的,不需要进行内核态的线程切换,因此减少了系统调用和上下文切换的开销。协作式调度
:协程是协作式调度的,即只有当前执行的协程主动让出CPU控制权时,才会切换到其他协程执行。这种调度方式由程序员显式地控制,可以更灵活地管理协程的执行顺序。状态保存
:在协程切换时,需要保存当前协程的执行状态,包括程序计数器、栈指针等上下文信息,以便后续恢复执行。调度器
:协程需要一个调度器来管理多个协程的执行,决定何时切换协程,哪个协程可以执行等。
19 安卓事件分发知道吗?流程是怎样的?
在Android中,事件分发是指将用户触摸事件(如点击、滑动、长按等)从顶层的ViewGroup传递到具体的View进行处理的过程。
详细过程如下:事件产生
:事件首先由底层的硬件设备(如屏幕、触摸板)感知到,并将其转化为原始输入事件。事件分发
:分发过程从Activity开始,依次经过Window、ViewGroup和最终的目标View。事件会先由Activity的dispatchTouchEvent()方法开始,再通过Window传递给顶层的ViewGroup,然后逐级向下传递,直到找到接收该事件的目标View。事件拦截
:在事件分发的过程中,每个View都有机会对事件进行拦截。如果一个父View拦截了事件,那么该事件将不再向子View传递,而是由父View自己处理。事件处理
:当事件到达目标View时,会调用目标View的onTouchEvent()方法进行事件处理。根据具体的事件类型,目标View可以执行相应的操作或者将事件继续传递给父View进行处理。事件返回
:事件处理完毕后,结果会返回到上一级的父View,直到最终返回给Activity。Activity可以根据事件的处理结果执行相应的操作。
简单点说,分发过程从Activity的dispatchTouchEvent()开始,到Window,再到ViewGroup再依次传递到每个View中,每个View都有机会在onTouchEvent()中对事件进行拦截,ViewGroup还有个专门的onInterceptTouchEvent()用于处理事件分发。
20 有写过自定义View吗?流程是怎样的?
继承 View 或其子类
:首先需要创建一个新的类并让它继承自 View 或其子类(如 ViewGroup、ImageView 等),这样可以获得绘制和交互的基本功能。实现 onDraw() 方法
:重写 onDraw() 方法,在该方法中编写绘制 View 所需的代码,包括绘制图形、文本、图片等内容。处理 View 大小和布局
:根据需要重写 onMeasure() 和 onLayout() 方法,处理 View 的测量和布局逻辑,确保 View 在不同尺寸的情况下能够正确显示。处理触摸事件
:如果需要处理触摸事件,可以重写 onTouchEvent() 方法,在其中处理用户的触摸操作,如点击、滑动等。添加自定义属性
:如果需要在 XML 布局文件中设置自定义属性,可以通过在 res/values/attrs.xml 中定义属性,并在构造函数中获取和处理这些属性。
21 onMeasure()中常用的那几个测量模式参数,还记得吗?
MeasureSpec.EXACTLY
:表示父容器对子 View 的大小有精确的要求,子 View 的大小将会被设定为 MeasureSpec.getSize(measureSpec) 返回的值,即确切的大小。这通常对应于布局文件中指定了固定的宽高或者 match_parent 属性。MeasureSpec.AT_MOST
:表示子 View 的大小最多可以达到父容器规定的大小,子 View 的大小将会被设定为 MeasureSpec.getSize(measureSpec) 返回的值,即最大可获得的空间。这通常对应于布局文件中指定了 wrap_content 属性。MeasureSpec.UNSPECIFIED
:表示父容器对子 View 的大小没有任何限制,子 View 可以任意大小。这通常在自定义 View 或特殊情况下使用。