Android 高频八股文十问

1,566 阅读18分钟

时光荏苒,不知不觉中已经入坑 Android 开发五年多了,在这些年里也经历过大大小小的各种面试,今天闲来无事,回忆一下,在此列举我遇到的,比较高频的八股文十问,答案仅供参考哈~

JVM 模型

image.png

  • 方法区/元空间:存储已被 JVM 加载的类信息(类的结构、字段、方法、接口等)、常量(如字符串常量池)、静态变量、即时编译器编译后的代码等数据。所有线程共享,在 JVM 启动时创建。JDK 7 及之前方法区的实现是 "永久代",物理上属于堆的一部分,JDK 8 及之后永久代被移除,方法区由 "元空间" 实现,元空间使用本地内存。需要回收,主要回收废弃的常量和无用的类,若无法分配内存,会抛出 OutOfMemoryError。
  • :JVM 管理的内存中最大的一块,用于存放对象实例和数组,几乎所有的对象都在这里分配内存。所有线程共享,在 JVM 启动时创建,是垃圾回收器的主要工作区域,若堆无法扩展或分配对象时内存不足,会抛出 OutOfMemoryError。为提高 GC 效率,堆通常被划分为新生代和老年代,新生代存放刚创建的对象或生命周期较短的对象,进一步分为 Eden 区(新对象优先分配)和两个 Survivor 区(用于存活对象的复制),老年代存放生命周期较长的对象。
  • 虚拟机栈:为 Java 方法的执行提供内存支持。每个方法被调用时,JVM 会创建一个栈帧并压入虚拟机栈;方法执行完毕后,栈帧出栈。线程私有,生命周期与线程一致。栈的深度是有限的,若方法调用层级过深(如递归无终止条件),会抛出 StackOverflowError,若栈动态扩展时无法申请到足够内存,会抛出 OutOfMemoryError。
  • 本地方法栈:专门为本地方法(由非 Java 语言编写,如 C/C++)的执行提供内存支持,线程私有,同样可能抛出StackOverflowError(栈深度超限)或OutOfMemoryError(内存不足)。
  • 程序计数器:本质是一个 "行号指示器",记录当前线程正在执行的字节码指令的地址(如果执行的是 Java 方法)。若执行的是本地方法(如 C/C++ 实现),则计数器值为 undefined。每个线程都有独立的程序计数器,互不干扰,生命周期与线程一致。线程切换时,能通过程序计数器恢复到正确的执行位置。是 JVM 中唯一不会抛出 OutOfMemoryError 的区域(内存占用极小)。

GC 机制

判断哪些对象需要回收

可达性分析算法:通过一系列的 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

可作为 GC Roots 的对象包括下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中 JNI(Native方法)引用的对象

GC 管理的区域是 Java 堆,虚拟机栈、方法区和本地方法栈不被 GC 所管理,因此选用这些区域内引用的对象作为 GC Roots,是不会被 GC 所回收的。

分代收集算法

根据对象存活周期的不同将内存划分为几块并采用不同的垃圾收集算法。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,所谓复制算法,就是将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。所谓“标记—整理”算法,就是先标记出所有需要回收的对象,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

Java 引用类型

强引用

强引用是最常见的引用类型,也是默认的引用方式。当一个对象被强引用关联时,只要强引用存在,垃圾回收器就绝不会回收该对象。即使内存不足(OOM),GC 也不会回收被强引用关联的对象,而是直接抛出内存溢出异常。

Object strongRef = new Object(); 
// 当强引用被置为 null 时,对象失去强引用,可能被 GC 回收。
strongRef = null; 

软引用

当系统内存充足时,对象不会被回收,当内存不足时,GC 会主动回收该对象,适用于 “缓存场景”(如图片缓存、数据缓存),内存充足时保留缓存提升性能,内存不足时释放缓存避免 OOM。

// 创建软引用,关联一个字符串对象
SoftReference<String> softRef = new SoftReference<>(new String("缓存数据"));

// 获取对象(内存不足时可能为 null)
String data = softRef.get(); 
if (data != null) {
    // 使用缓存数据
} else {
    // 缓存已被回收,重新加载数据
}

弱引用

只要 GC 运行,无论内存是否充足,都会回收被弱引用关联的对象,可用于临时数据缓存或避免内存泄漏场景等。

// 创建弱引用,关联一个对象
WeakReference<String> weakRef = new WeakReference<>(new String("临时数据"));

String data = weakRef.get(); 

虚引用

虚引用是强度最弱的引用类型,无法通过虚引用获取对象(get() 永远返回 null),唯一作用是跟踪对象被 GC 回收的状态。必须与引用队列配合使用,当虚引用关联的对象被 GC 回收时,虚引用本身会被加入到引用队列中,可通过队列感知对象的回收。

// 创建引用队列
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 创建虚引用,关联对象和队列。
PhantomReference<String> phantomRef = new PhantomReference<>(new String("跟踪对象"), queue);

// 虚引用的 get() 永远返回 null
String data = phantomRef.get(); 

// 当对象被回收后,phantomRef 会被加入 queue。
Reference<? extends String> ref = queue.poll(); 
if (ref != null) {
    // 处理对象已被回收的逻辑
}

HashMap 机制

HashMap 的底层存储结构是数组(称为 “哈希桶”),数组中的每个元素是一个链表,当链表长度超过阈值(默认8),且数组长度 ≥ 64 时,链表会转为红黑树,红黑树的引入将查询时间复杂度从链表的 O(n) 优化为 O(log n)。

hashmap.jpg

当有键值对(key - value)要存储时,首先通过哈希函数计算键(key)的哈希值,然后将哈希值转换为数组的索引,来确定键值对应该存储在数组的哪个位置(桶)中。当不同的键通过哈希函数得到相同的数组索引时,就会发生哈希冲突。发生冲突时,就会通过链表的形式来解决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过 key 的哈希值先定位到桶,再遍历桶上的所有键值对,找出 key 相等的键值对,从而来获取 value。

动态扩容

HashMap的容量(数组长度)是动态增长的,目的是避免哈希冲突过于频繁。

  • 触发条件:当元素数量超过 “扩容阈值”(threshold = 容量 × 负载因子)时触发扩容,默认负载因子为 0.75。
  • 扩容过程:新建一个容量为原数组两倍的新数组,将旧数组中的元素重新计算索引,更新阈值(新阈值 = 新容量 × 负载因子)。

为什么重写 equals 方法时也要重写 hashCode ?

这俩存在强关联性,核心目的是保证 “逻辑相等” 的对象在哈希表(如 HashMap、HashSet)中能被正确识别。哈希表(如 HashMap)存储数据时,会先通过 hashCode 定位对象的存储位置,再通过 equals 判断桶内是否有相同的对象。倘若两个对象 equals 相等但 hashCode 不同:哈希表会把它们分到不同的桶里,导致明明 “相等” 的对象被当成不同的键(比如在 HashMap 中会被同时存储,违反了 “键唯一” 的预期)。

如果两个对象通过 equals 比较为 “相等”,那么它们的 hashCode 必须返回相同的值;反之,hashCode 相同的对象,equals 不一定相等(这是哈希冲突的正常情况)。

HashMap 和 Hashtable 的区别?

  • HashMap 是线程不安全的,多线程并发操作时可能导致数据错乱。Hashtable 是线程安全的。其所有方法都被 synchronized 修饰,多线程环境下可直接使用,不会出现数据不一致问题。
  • HashMap 允许 key 和 value 为 null。其中,key 为 null 时固定存储在数组索引 0 的位置(仅允许一个 null 键),value 可以有多个 null。Hashtable 不允许 key 或 value 为 null。若传入 null,会直接抛出 NullPointerException。
  • Hashtable 继承自 Dictionary 类,同时实现 Map 接口。HashMap 继承自 AbstractMap 类,实现 Map 接口。

实际开发中,优先使用 HashMap;若需线程安全,推荐 ConcurrentHashMap 而非 Hashtable

ConcurrentHashMap 是线程安全的键值对集合,专为高并发场景设计。它不是锁整个表,而是锁数组中的单个桶(链表 / 红黑树的头节点),当插入元素时,先尝试直接插入(若桶为空),失败则说明桶已被占用,此时对桶的头节点加 synchronized 锁,保证同一桶内的操作互斥。不同桶的操作可以完全并发,锁冲突只发生在同一桶内的线程之间,并发效率大幅提升。

浅拷贝和深拷贝的区别

  • 浅拷贝:当拷贝一个对象时,对于基本数据类型字段(如int、float、boolean等),会直接复制其值;但对于引用类型字段(如对象、数组等),仅复制其引用地址(即指向原对象中引用类型的内存地址),因此,原对象和拷贝对象的引用类型字段会指向同一个内存地址,修改其中一个的引用类型字段,会影响另一个。
  • 深拷贝:拷贝对象时,不仅会复制基本数据类型字段的值,对于引用类型字段,会递归复制其指向的实际对象,即创建一个新的引用类型对象,并复制原对象中引用类型的所有内容,因此,原对象和拷贝对象的引用类型字段会指向不同的内存地址,修改其中一个的引用类型字段,不会影响另一个。
public class Address {

    private String city;

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }
}
public class User implements Cloneable {
    private int age; // 基本类型
    private Address address; // 引用类型

    @Override
    public User clone() { //浅拷贝
        try {
            User clone = (User) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

    public User deepCopy() {  //深拷贝
        Gson gson = new Gson();
        String json = gson.toJson(this);
        User clone = gson.fromJson(json, new TypeToken<User>() {
        });
        return clone;
    }
}

Activity,Window,View 三者之间的关系

Activity、Window、View 三者是UI 展示与交互的核心组件,它们的关系可以用 “层次依赖” 来概括。

核心定位

  • Activity:Android 四大组件之一,是用户交互的 “载体”,负责管理界面的生命周期、业务逻辑。
  • Window:抽象类,具体实现为 PhoneWindow,是 “窗口” 的概念,是 Activity 与 View 之间的 “桥梁”,负责提供 UI 绘制的载体。
  • View:所有 UI 元素的基类,是具体的可视化组件,负责实际的绘制和用户交互响应。

依赖关系

  • Activity 持有 Window:每个 Activity 在创建时会初始化一个 Window 对象,通过 getWindow() 可获取该 Window。Activity 的生命周期会间接影响 Window 的状态(如 Activity 销毁时,Window 也会被回收)。
  • Window 承载 View:Window 是 View 的容器,它内部维护了一个顶级 View - DecorView(整个 View 树的根节点)。DecorView 包含标题栏和内容栏,在 Activity 中通过 setContentView(view) 设置的布局,最终会被添加到 DecorView 的内容栏中。
  • View 依附于 Window 才能显示:View 本身无法独立显示,必须通过 Window 关联到屏幕。Window 通过 WindowManager 将 DecorView 添加到系统的窗口管理服务中,最终由系统把 View 绘制到屏幕上。

Activity 的启动模式

Standard(默认模式)

  • 特点:每次启动 Activity 时,系统都会创建一个新的实例,并将其压入当前任务栈的栈顶。
  • 适用场景:大多数普通页面(如列表项详情页、设置页等),需要多次创建的场景。

SingleTop

  • 特点:启动 Activity 时,系统会先检查当前任务栈的栈顶是否已存在该 Activity 的实例,若存在则不创建新实例,而是调用该实例的 onNewIntent() 方法,复用现有实例。若不存在则创建新实例并压入栈顶。
  • 适用场景:需要频繁启动,但栈顶复用即可满足需求的页面,例如推送通知打开的详情页(若已在栈顶,直接刷新内容)。

SingleTask

  • 特点:启动 Activity 时,系统会检查整个任务栈中是否存在该 Activity 的实例,若存在则将该实例上方的所有 Activity 全部出栈,使该实例成为栈顶,并调用其 onNewIntent()。若不存在:则创建新实例并压入当前任务栈。
  • 适用场景:需要唯一实例,且希望 “返回时直接回到该页面” 的场景,例如应用的主界面。

SingleInstance

  • 特点:该 Activity 会独占一个任务栈,整个系统中只有一个实例,且所在任务栈中只有它自己。启动时,若实例不存在,系统会创建一个新的任务栈,将该 Activity 实例放入其中。若实例已存在,无论哪个应用启动它,都会直接切换到该实例所在的任务栈,并调用其 onNewIntent()。
  • 适用场景:系统级,需要全局唯一的页面。例如电话拨号界面(无论从哪个应用启动拨号,都是同一个实例)

所有启动模式的生命周期回调遵循统一规则:
新实例创建:onCreate() → onStart() → onResume()
复用已有实例:onNewIntent() → onResume()(若从后台唤醒,会先有 onRestart() → onStart())

Service 的两种启动方式有什么区别

核心用途

  • startService:主要用于让 Service 在后台独立执行耗时任务,不依赖启动它的组件(如 Activity)。
  • bindService:主要用于让启动它的组件与 Service 建立双向通信,Service 的生命周期与客户端绑定。

生命周期差异

  • startService:生命周期为:onCreate() → onStartCommand()(每次启动都会调用) → onDestroy()。一旦启动,Service 会独立运行,即使启动它的组件(如 Activity)被销毁,Service 仍可继续存在,直到通过 stopService(外部调用)或 stopSelf(Service 自身调用)停止,才会触发 onDestroy()。
  • bindService:onCreate() → onBind()(返回 Binder 对象,仅首次绑定调用) → onUnbind() → onDestroy()。Service 的生命周期与绑定的客户端强关联。当所有绑定的客户端都通过 unbindService() 解绑后,Service 会自动销毁(触发 onUnbind() 和 onDestroy()),若客户端被销毁(如 Activity finish),系统会自动为其解绑,进而可能导致 Service 销毁。

通信能力差异

  • startService:启动组件与 Service 之间无直接通信通道。若需传递数据,只能通过启动时的 Intent 携带,无法实时交互。
  • bindService:通过 ServiceConnection 接口和 Binder 对象实现双向通信。客户端可获取 Service 返回的 Binder 对象,直接调用 Service 中的方法。

requestLayout 和 invalidate 的区别

Android View 的绘制流程分为三个核心阶段:
measure(测量尺寸)→ layout(确定位置)→ draw(绘制内容)。

  • invalidate:仅触发 draw 阶段(重绘),不影响 measure 和 layout。
  • requestLayout:触发 measure 和 layout 阶段(重新测量尺寸、确定位置),可能间接触发 draw(若布局变化导致外观改变)。

如何解决滑动冲突问题

核心原理

解决滑动冲突的核心原理是通过控制事件分发机制,合理分配触摸事件的处理权,让最符合用户滑动意图的控件获得事件处理权,从而避免事件争夺导致的滑动行为异常。

拦截与传递

触摸事件分发遵循固定流程:Activity.dispatchTouchEvent() → 父 ViewGroup.dispatchTouchEvent() → 父 ViewGroup.onInterceptTouchEvent() → 子 View.onTouchEvent()。

三个主要方法

  • onInterceptTouchEvent():父控件是否拦截事件(返回 true 则拦截,由父控件处理;返回 false 则传递给子控件)。
  • onTouchEvent():控件是否消费事件(返回 true 则消费,事件终止;返回 false 则向上传递给父控件)。
  • requestDisallowInterceptTouchEvent(boolean):子控件可通过此方法强制父控件是否允许拦截(true 表示禁止父控件拦截)

解决滑动冲突的本质,就是通过重写这些方法,在合适的时机返回合适的值,让事件流向 "应该处理它的控件"。

滑动冲突类型

  • 方向相反的滑动冲突:父控件与子控件滑动方向垂直(如外层垂直滑动(ScrollView),内层水平滑动(RecyclerView))。
  • 方向相同的滑动冲突:父控件与子控件滑动方向一致(如 ScrollView 嵌套另一个 ScrollView,均为垂直滑动)。
  • 复杂嵌套冲突:多层嵌套或混合方向滑动(如 ViewPager 嵌套 ScrollView,ScrollView 又嵌套 HorizontalScrollView)。

处理方式

外部拦截法

父控件在 onInterceptTouchEvent 中根据条件判断是否拦截事件,符合父控件滑动条件则拦截,否则交给子控件。

示例:ScrollView(垂直)与 RecyclerView(水平)的冲突

这种情况的处理还是比较简单的,可以根据用户滑动方向(水平/垂直)决定谁处理事件,只需重写父容器 ScrollView 即可。

class VerticalScrollView : ScrollView {
    private var mLastX = 0f
    private var mLastY = 0f

    companion object {
        // 滑动方向判断的阈值(避免轻微滑动误判)
        private const val SCROLL_THRESHOLD = 20
    }

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 记录初始触摸位置
                mLastX = ev.x
                mLastY = ev.y
            }

            MotionEvent.ACTION_MOVE -> {
                // 计算滑动距离
                val dx = abs(ev.x - mLastX)
                val dy = abs(ev.y - mLastY)
                // 如果垂直滑动距离 > 水平滑动距离,且超过阈值,说明是垂直滑动,拦截事件
                if (dy > dx && dy > SCROLL_THRESHOLD) {
                    return true // 拦截事件,由当前 ScrollView 处理
                }
            }
        }
        // 不拦截,事件交给子 View 处理
        return super.onInterceptTouchEvent(ev)
    }

}

内部拦截法

父控件默认不拦截事件,所有事件传递给子控件;子控件在 onTouchEvent() 中根据条件判断是否需要处理,若不需要则通过 requestDisallowInterceptTouchEvent(false) 让父控件拦截。

示例1:ViewPager2(水平)与 RecyclerView(水平)的滑动冲突

ViewPager2 水平滑动切换多个 Fragment,而在 Fragment 中又存在着水平滑动的 RecyclerView,就会出现滑动冲突的问题,这种情况,可以通过重写 RecyclerView 的 onInterceptTouchEvent 和 onTouchEvent,实现事件拦截判断。

class HorizontalRecyclerView : RecyclerView {

    // 记录初始触摸位置
    private var downX = 0f
    private var downY = 0f

    constructor(context: Context) : super(context)
    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
    constructor(context: Context, attributeSet: AttributeSet, defStyle: Int) : super(
        context,
        attributeSet,
        defStyle
    )

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        var shouldIntercept = false
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                // 记录初始触摸坐标
                downX = e.x
                downY = e.y
                // 初始时禁止父控件拦截,确保子控件能收到后续事件
                parent.requestDisallowInterceptTouchEvent(true)
                shouldIntercept = false
            }

            MotionEvent.ACTION_MOVE -> {
                // 计算滑动距离
                val dx = e.x - downX  // X方向滑动距离(正值向右,负值向左)
                val dy = e.y - downY  // Y方向滑动距离

                // 判断是否为横向滑动(横向滑动优先级高于纵向)
                if (abs(dx) > abs(dy)) {
                    // 横向滑动时,判断子控件是否能处理当前滑动
                    if (dx > 0) {
                        // 向右滑动:检查是否已滑到最左侧
                        val canScrollLeft = canScrollHorizontally(-1)  // -1表示检查向左滑动能力
                        if (!canScrollLeft) {
                            // 已到最左,向右滑需让 ViewPager2 处理,不拦截事件
                            shouldIntercept = false
                            parent.requestDisallowInterceptTouchEvent(false)  // 允许父控件拦截
                        } else {
                            // 未到最左,子控件自己处理滑动,拦截事件
                            shouldIntercept = true
                            parent.requestDisallowInterceptTouchEvent(true)  // 禁止父控件拦截
                        }
                    } else {
                        // 向左滑动:检查是否已滑到最右侧(不能再向右滑)
                        val canScrollRight = canScrollHorizontally(1)  // 1表示检查向右滑动能力
                        if (!canScrollRight) {
                            // 已到最右,向左滑需让 ViewPager2 处理,不拦截事件
                            shouldIntercept = false
                            parent.requestDisallowInterceptTouchEvent(false)  // 允许父控件拦截
                        } else {
                            // 未到最右,子控件自己处理滑动,拦截事件
                            shouldIntercept = true
                            parent.requestDisallowInterceptTouchEvent(true)  // 禁止父控件拦截
                        }
                    }
                } else {
                    // 纵向滑动,不拦截,让父控件或其他控件处理
                    shouldIntercept = false
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                // 事件结束,恢复父控件拦截能力
                shouldIntercept = false
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        return shouldIntercept
    }

    override fun onTouchEvent(e: MotionEvent): Boolean {
        // 处理触摸事件(交给父类处理滑动逻辑)
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                downX = e.x
                downY = e.y
                parent.requestDisallowInterceptTouchEvent(true)
            }

            MotionEvent.ACTION_MOVE -> {
                val dx = e.x - downX
                val dy = e.y - downY
                if (abs(dx) > abs(dy)) {
                    // 同步更新父控件拦截状态(与 onInterceptTouchEvent 逻辑一致)
                    if (dx > 0) {
                        val canScrollLeft = canScrollHorizontally(-1)
                        parent.requestDisallowInterceptTouchEvent(canScrollLeft)
                    } else {
                        val canScrollRight = canScrollHorizontally(1)
                        parent.requestDisallowInterceptTouchEvent(canScrollRight)
                    }
                } else {
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        return super.onTouchEvent(e)
    }
}
示例2:ScrollView(垂直)与 RecyclerView(垂直)的滑动冲突

这种情况就没有必要自己去处理滑动冲突了,将 ScrollView 换为 NestedScrollView 就是最优解,NestedScrollView 是增强版滚动容器,属于 AndroidX 库,核心改进就是支持嵌套滚动机制。

<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"> 

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:background="@color/teal_200"
            android:text="顶部固定区域"
            android:gravity="center"/>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"       
            android:layout_height="wrap_content"
            android:nestedScrollingEnabled="false"/> 
            <!-- 关闭 RecyclerView 自身的嵌套滑动处理,交给 NestedScrollView 协调 -->
    </LinearLayout>
</androidx.core.widget.NestedScrollView>