Android 开发核心面试题与答案详解
前言
本文整理了 Android 开发中常见的 Java 基础、Android 组件通信、Framework 原理及 Flutter 架构面试题。涵盖抽象类与接口区别、线程状态管理、Handler 机制、Binder 跨进程通信、Activity 生命周期及 Flutter 渲染树等核心知识点,并提供详细解答与技术解析,帮助开发者系统复习面试重点。
Java 篇
一、抽象类与接口的区别?
在 Java 面向对象编程中,抽象类(Abstract Class)和接口(Interface)都是实现代码复用和约束的手段,但设计意图不同。
- 方法实现:抽象类可以提供成员方法的实现细节,也可以包含抽象方法;而接口在 Java 8 之前只能存在 public 抽象方法,Java 8 之后支持 default 方法和 static 方法,但仍以定义行为契约为主。
- 成员变量:抽象类中的成员变量可以是各种类型(public, private, protected 等),可以有实例变量;接口中的成员变量只能是 public static final 类型的常量。
- 构造器与静态块:接口中不能含有构造器、静态代码块以及普通静态方法(Java 8+ 除外),而抽象类可以有构造器、静态代码块和静态方法,用于初始化状态。
- 继承关系:一个类只能继承一个抽象类(单继承),而一个类却可以实现多个接口(多实现)。这体现了 Java 的多态性限制。
- 性能差异:抽象类访问速度通常比接口快,因为接口需要时间去寻找在类中具体实现的方法(涉及动态查找),而抽象类是静态绑定。但在现代 JVM 优化下,这种差异已不明显。
- 扩展性:如果你往抽象类中添加新的非抽象方法,你可以给它提供默认的实现,因此不需要改变现有的子类代码。如果你往接口中添加方法,那么所有实现该接口的类都必须实现该方法,否则编译报错。
- 设计目的:接口更多的为了约束类的行为,可用于解耦,强调'能做什么';抽象类更加侧重于代码复用,强调'是什么',适合有共同状态的类。
二、谈谈 List, Set, Map 的区别?
这三个是 Java 集合框架的核心接口,存储逻辑和数据结构不同。
- List:有序集合,允许重复元素。底层实现包括 ArrayList(基于数组,查询快插入慢)、LinkedList(基于双向链表,插入删除快查询慢)。索引访问效率高。
- Set:无序集合(HashSet),不允许重复元素。元素在集合中的位置是由元素的 hashcode 决定。Set 集合是根据 hashcode 来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的。TreeSet 则根据自然顺序或比较器排序。
- Map:键值对映射,键(Key)不允许重复,值(Value)允许重复。存储的数据是无序的(HashMap),或者按 Key 排序(TreeMap)。常用实现有 HashMap(哈希表)、LinkedHashMap(维护插入顺序)、ConcurrentHashMap(线程安全)。
三、说一下线程的几种状态?
Java 线程生命周期由 java.lang.Thread.State 枚举定义,主要包括以下五种状态:
- 新建状态(NEW):在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。此时线程还未开始运行。
- 就绪状态(RUNNABLE):当调用了线程对象的 start 方法之后,该线程就进入了就绪状态。此时线程调度程序还没有把该线程设置为当前线程,它正在等待 CPU 时间片。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态(RUNNING):线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。注意 RUNNABLE 状态在 Java 中既包含就绪也包含运行。
- 阻塞状态(BLOCKED):线程正在运行的时候,被暂停,通常是为了等待某个时间的发生 (比如说某项资源就绪) 之后再继续运行。sleep(), suspend(), wait() 等方法都可以导致线程阻塞。其中 synchronized 锁竞争失败会进入 BLOCKED 状态。
- 死亡状态(TERMINATED):如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。
四、如何实现多线程中的同步?
多线程同步和异步不是一回事。同步是指多个线程协调工作,保证数据一致性。常见方式如下:
- synchronized 关键字:它可以保证原子性,保证多个线程在操作同一方法或代码块时只有一个线程可以持有锁,并且操作该方法。分为修饰实例方法(锁对象)、修饰静态方法(锁类对象)和修饰代码块(锁指定对象)。
- Lock 接口:手动调用读写锁(ReentrantReadWriteLock)或可中断锁(ReentrantLock)。相比 synchronized,Lock 更灵活,支持尝试获取锁、超时获取锁等功能。
- wait/notify 机制:手动操作线程的 wait 和 notify。需在 synchronized 代码块中使用,用于线程间的协作通知。
- volatile 关键字:volatile 记得是没有原子性的,他可以保证内存可见性,在多线程的情况下保证每个线程的数据都是最新的。它禁止指令重排序,适用于状态标记位。
- 并发工具类:如 CountDownLatch, CyclicBarrier, Semaphore 等,位于 java.util.concurrent 包下,提供更高级的同步控制。
Android 篇
一、Activity 与 Fragment 之间常见的几种通信方式?
- ViewModel 共享:ViewModel 做数据管理,activity 和 fragment 公用同一个 ViewModel 实现数据传递。通过 ViewModelProvider 获取共享实例,利用 LiveData 或 StateFlow 观察数据变化。
- 接口回调:Fragment 定义接口,Activity 实现该接口,Fragment 持有 Activity 引用并调用接口方法。
- Bundle 传参:在启动 Fragment 时通过 Intent 或 Bundle 传递简单数据。
- EventBus / LiveData:使用第三方库 EventBus 或 Android Architecture Components 的 LiveData 进行解耦通信。
二、BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别?
- BroadcastReceiver:是跨应用广播,利用 Binder 机制实现,支持动态和静态两种方式注册方式。接收范围广,安全性较低,可能影响系统性能。
- LocalBroadcastReceiver:是应用内广播,利用 Handler 实现,利用了 IntentFilter 的 match 功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。由于不涉及 Binder 通信,开销更小。
三、子线程能否更新 UI?为什么?
子线程是不能直接更新 UI 的。
注意这句话,是不能直接更新,不是不能更新(极端情况下可更新)。绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制 ui。极端情况就是,在 Activity 的 onResume(含)之前的生命周期中子线程都可以进行更新 ui,也就是 onCreate, onStart 和 onResume,此时主线程的绘制还没开始。但最佳实践是始终在主线程更新 UI,使用 Handler, runOnUiThread, 或协程 MainScope。
四、谈谈 Handler 机制和原理?
Handler 机制是 Android 处理异步消息的核心,主要包含四个部分:Handler, MessageQueue, Looper, Thread。
- 创建 Handler:首先在 UI 线程我们创建了一个 Handler 实例对象,无论是匿名内部类还是自定义类生成的 Handler 实例对象,我们都需要对 handleMessage 方法进行重写。在 handleMessage 方法中我们可以通过参数 msg 来写接受消息过后 UI 线程的逻辑处理。
- 发送消息:接着我们创建子线程,在子线程中需要更新 UI 的时候,新建一个 Message 对象,并且将消息的数据记录在这个消息对象 Message 的内部,比如 arg1, arg2, obj 等,然后通过前面的 Handler 实例对象调用 sendMessage 方法把这个 Message 实例对象发送出去。
- 消息队列:之后这个消息会被存放于 MessageQueue 中等待被处理。MessageQueue 是一个单向链表结构,存储待处理的消息。
- Looper 循环:此时 MessageQueue 的管家 Looper 正在不停的把 MessageQueue 存在的消息取出来,通过回调 dispatchMessage 方法将消息传递给 Handler 的 handleMessage 方法。
- 最终处理:最终前面提到的消息会被 Looper 从 MessageQueue 中取出来传递给 handleMessage 方法,完成 UI 更新。
核心原理:每个线程最多有一个 Looper,通过 ThreadLocal 存储。Handler 与当前线程的 Looper 绑定。sendMessage 将消息加入队列,Looper.loop() 无限循环取出消息分发给 Handler。
五、谈谈 Android 的事件分发机制?
Android 事件分发涉及三个关键方法:dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent。
- 顶层 ViewGroup:当点击的时候,会先调用顶级 viewgroup 的 dispatchTouchEvent。如果顶级的 viewgroup 拦截了此事件(onInterceptTouchEvent 返回 true),则此事件序列由顶级 viewgroup 处理,不再向下传递。
- OnTouchListener:如果顶级 viewgroup 设置 setOnTouchListener,则会回调接口中的 onTouch,此时顶级的 viewgroup 中的 onTouchEvent 不再回调。如果不设置 setOnTouchListener 则 onTouchEvent 会回调。
- OnClickListener:如果顶级 viewgroup 设置 onClickListener,则会回调接口中的 onClick。这通常在触摸动作结束后触发。
- 子 View 传递:如果顶级 viewgroup 不拦截事件,事件就会向下传递给他的子 view,然后子 view 就会调用它的 dispatchTouchEvent 方法。子 View 也可以拦截或消费事件。
- 消费判断:如果任何一层返回 true,表示事件被消费,不再继续传递;如果返回 false,则继续向上传递或向下传递。
Android Framework 篇
一、你了解 Android 系统启动流程吗?
Android 系统启动是一个复杂的过程,主要步骤如下:
- BootLoader:当按电源键触发开机,首先会从 ROM 中预定义的地方加载引导程序 BootLoader 到 RAM 中,并执行 BootLoader 程序启动 Linux Kernel。
- Kernel 启动:Linux Kernel 初始化硬件驱动,挂载根文件系统。
- Init 进程:启动用户级别的第一个进程:init 进程(PID 1)。init 进程会解析 init.rc 脚本做一些初始化工作,包括挂载文件系统创建工作目录以及启动系统服务进程等。
- Zygote 进程:其中系统服务进程包括 Zygote、service manager、media 等。Zygote 是 Android 特有的进程,负责孵化其他应用进程。
- System Server:在 Zygote 中会进一步去启动 system_server 进程。system_server 是 Android 系统的核心服务进程。
- AMS/WMS/PMS:在 system_server 进程中会启动 AMS(Activity Manager Service)、WMS(Window Manager Service)、PMS(Package Manager Service)等服务。
- Launcher:等这些服务启动之后,AMS 中就会打开 Launcher 应用的 home Activity,最终就看到了手机的'桌面'。
二、能具体说说是怎么导致的死锁的吗?
这里特指 fork() 导致的死锁问题,常见于 JNI 或 Native 层。
在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据 (通常使用 copy-on-write 的策略,所以可以实现的速度快)以及所有系统对象,然后仅复制当前线程到子进程。
死锁场景:
- 所有父进程中别的线程,到了子进程中都是突然蒸发掉的。
- 对于锁来说,从 OS 看,每个锁有一个所有者,即最后一次 lock 它的线程。
- 假设这么一个环境,在 fork 之前,有一个子线程 lock 了某个锁,获得了对锁的所有权。
- fork 以后,在子进程中,所有的额外线程都人间蒸发了。而锁却被正常复制了,在子进程看来,这个锁没有主人,所以没有任何人可以对它解锁。
- 当子进程想 lock 这个锁时,不再有任何手段可以解开了,从而导致死锁。
解决方案:在 fork 之前释放所有锁,或在子进程中重新初始化锁。
三、Zygote 为什么不采用 Binder 机制进行 IPC 通信?
- 多线程问题:Binder 机制中存在 Binder 线程池,是多线程的。如果 Zygote 采用 Binder 的话就存在上面说的 fork() 与多线程的问题了。fork 会复制所有线程,但子进程只保留当前线程,导致其他线程状态不一致。
- 实际实现:其实严格来说,Binder 机制不一定要多线程,所谓的 Binder 线程只不过是在循环读取 Binder 驱动的消息而已,只注册一个 Binder 线程也是可以工作的,比如 service manager 就是这样的。
- Zygote 策略:实际上 Zygote 尽管没有采取 Binder 机制,它也不是单线程的,但它在 fork() 前主动停止了其他线程,fork() 后重新启动了必要的线程,从而避免了死锁风险。
四、Binder 是如何做到一次拷贝的
主要是因为 Linux 是使用的虚拟内存寻址方式,它有如下特性:
- 虚拟内存映射:用户空间的虚拟内存地址是映射到物理内存中的。
- 读写即物理:对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射。
- mmap 实现:这个内存映射过程是通过系统调用 mmap() 来实现的。
- 零拷贝机制:Binder 借助了内存映射的方法,在内核空间的接收方用户控件的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。传统 IPC 需要 User Space -> Kernel Space -> User Space,Binder 实现了 User Space -> Kernel Space -> User Space 的共享内存映射,减少了内存复制次数。
五、Binder 机制是如何跨进程的
- Binder 驱动:在内核空间创建一块接收缓存区,实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区。
- 发送流程:发送进程通过系统调用 (copy_from_user) 将数据发送到内核缓存区。由于内核缓存区和接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信。
- 权限校验:Binder 驱动还会进行权限校验,确保只有授权的应用才能访问特定的服务。
Flutter 篇
一、Flutter 中的生命周期
Flutter 的 Widget 生命周期与 Android 类似,但更偏向声明式。
- initState():表示当前 State 将和一个 BuildContext 产生关联,但是此时 BuildContext 没有完全装载完成。如果你需要在该方法中获取 BuildContext,可以 new Future.delayed(const Duration(seconds: 0), (){//context}); 一下。
- didChangeDependencies():在 initState() 之后调用,当 State 对象的依赖关系发生变化时,该方法被调用,初始化时也会调用。常用于 InheritedWidget 变化时刷新。
- deactivate():当 State 被暂时从视图树中移除时,会调用这个方法,同时页面切换时,也会调用。例如路由跳转离开当前页。
- dispose():Widget 销毁了,在调用这个方法之前,总会先调用 deactivate()。用于释放资源,如取消订阅 StreamController。
- didUpdateWidget:当 widget 状态发生变化时,会调用。用于对比新旧 Widget 的差异。
异步构建:通过 StreamBuilder 和 FutureBuilder 我们可以快速使用 Stream 和 Future 快速构建我们的异步控件。
Binding 机制:Flutter 中 runApp 启动入口其实是一个 WidgetsFlutterBinding,它主要是通过 BindingBase 的子类 GestureBinding、ServicesBinding、SchedulerBinding、PaintingBinding、SemanticsBinding、RendererBinding、WidgetsBinding 等,通过 mixins 的组合而成的。
线程模型:Flutter 中的 Dart 的线程是以事件循环和消息队列的形式存在,包含两个任务队列,一个是 microtask 内部队列,一个是 event 外部队列,而 microtask 的优先级又高于 event。因为 microtask 的优先级又高于 event,同时会阻塞 event 队列,所以如果 microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿哦。
四大线程:Flutter 中存在四大线程,分别为 UI Runner、GPU Runner、IO Runner,Platform Runner(原生主线程)。同时在 Flutter 中可以通过 isolate 或者 compute 执行真正的跨线程异步操作,避免阻塞 UI 线程。
二、Widget 和 element 和 RenderObject 之间的关系?
这三者构成了 Flutter 的三层架构:
- Widget:用户界面的一部分,并且是不可变的配置对象。它描述了 UI 的样子,但不包含状态或渲染逻辑。
- Element:是在树中特定位置 Widget 的实例。它是 Widget 和 RenderObject 之间的桥梁,负责管理状态和生命周期。Widget 会被 inflate(填充)到 Element,并由 Element 管理底层渲染树。
- RenderObject:是渲染树中的一个对象,它的层次结构是渲染库的核心。负责布局、绘制等操作。
工作流程:
- Widget 并不会直接管理状态及渲染,而是通过 State 这个对象来管理状态。
- Flutter 创建 Element 的可见树,相对于 Widget 来说,是可变的,通常界面开发中,我们不用直接操作 Element,而是由框架层实现内部逻辑。
- 就如一个 UI 视图树中,可能包含有多个 TextWidget (Widget 被使用多次),但是放在内部视图树的视角,这些 TextWidget 都是填充到一个个独立的 Element 中。
- Element 会持有 renderObject 和 widget 的实例。
- 记住,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。
- 在第一次创建 Widget 的时候,会对应创建一个 Element,然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。这就是 Flutter 高性能的原因——Diff 算法。
三、mixin extends implement 之间的关系?
在 Dart 语言中,这三种机制可以同时存在,前后顺序是 extends -> mixins -> implements。
- 继承(extends):Flutter 中的继承是单继承,子类重写超类的方法要用 @override,子类调用超类的方法要用 super。
- 混入(mixins):Mixins 是一种在多个类层次结构中复用类代码的方法。mixins 的对象是类,mixins 绝不是继承,也不是接口,而是一种全新的特性,可以 mixins 多个类,mixins 的使用需要满足一定条件(如不能有构造函数冲突)。
- 接口实现(implements):Dart 中类默认就是接口,显式 implements 用于强制实现某些方法签名。
总结
以上涵盖了 Android 开发面试中高频出现的 Java 基础、Android 核心组件、Framework 底层原理以及 Flutter 跨平台技术。面试官不仅考察知识点的记忆,更注重对原理的理解和应用场景的判断。建议在实际项目中多动手实践,结合源码阅读加深理解。持续学习新技术,保持对底层原理的探索,才能在技术道路上走得更远。