跳到主要内容Android 中大厂秋招常见面试题及答案解析 | 极客日志Java大前端java算法
Android 中大厂秋招常见面试题及答案解析
综述由AI生成Android 开发中大厂秋招的常见面试题,涵盖 Java 基础、Android 核心机制、Framework 原理及 Flutter 框架知识。内容涉及抽象类与接口区别、线程状态管理、Handler 消息机制、Binder 跨进程通信、Zygote 启动流程等关键技术点,并提供了详细的解析与代码示例,旨在帮助求职者系统复习核心技术栈,提升面试通过率。
全栈工匠22 浏览 Android 中大厂秋招常见面试题及答案解析
Java 篇
一、抽象类与接口的区别?
在 Java 面向对象编程中,抽象类和接口都是实现代码复用和约束的重要手段,但设计目的和使用场景有所不同。
主要区别如下:
- 方法实现细节:抽象类可以提供成员方法的实现细节(非抽象方法),而接口(Java 8 之前)只能存在 public 抽象方法。Java 8 之后接口支持 default 方法和 static 方法,但仍以抽象为主。
- 成员变量类型:抽象类中的成员变量可以是各种类型的(public, private, protected 等),而接口中的成员变量只能是 public static final 类型的常量。
- 构造器与静态块:接口中不能含有构造器、静态代码块以及静态方法(Java 8 前),而抽象类可以有构造器、静态代码块和静态方法。
- 继承限制:一个类只能继承一个抽象类(单继承),而一个类却可以实现多个接口(多实现)。
- 性能差异:抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法(通过查找表),而抽象类直接调用。
- 扩展性:如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类(除非使用 default 方法)。
- 设计理念:接口更多的为了约束类的行为,可用于解耦,体现'是什么';抽象类更加侧重于代码复用,体现'是什么东西'。
abstract class Animal {
String name;
public abstract void eat();
public void sleep() { System.out.println("Sleeping..."); }
}
interface Flyable {
void fly();
}
二、谈谈 List, Set, Map 的区别?
这是 Java 集合框架的核心部分,理解它们的特性对面试至关重要。
- List:存储的数据是有顺序的,并且值允许重复。常见的实现有 ArrayList(基于动态数组,查询快插入慢)和 LinkedList(基于双向链表,插入删除快)。
- Map:存储的是键值对(Key-Value)。数据是无序的(HashMap),它的键是不允许重复的,但是值是允许重复的。常见的实现有 HashMap、TreeMap、Hashtable。
- Set:存储的数据是无顺序的(HashSet),并且不允许重复。元素在集合中的位置是由元素的 hashcode 决定,即位置是固定的(Set 集合是根据 hashcode 来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的)。常见的实现有 HashSet、LinkedHashSet、TreeSet。
三、说一下线程的几种状态?
Java 线程生命周期包含以下五种状态,对应 Thread.State 枚举:
- 新建状态 (NEW):在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。
- 就绪状态 (RUNNABLE):当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 运行状态 (RUNNING):线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
- 阻塞状态 (BLOCKED/WAITING/TIMED_WAITING):线程正在运行的时候,被暂停,通常是为了等待某个时间的发生 (比如说某项资源就绪) 之后再继续运行。sleep, suspend, wait, join 等方法都可以导致线程阻塞。
- 死亡状态 (TERMINATED):如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。
四、如何实现多线程中的同步?
- synchronized 关键字:它可以保证原子性,保证多个线程在操作同一方法时只有一个线程可以持有锁,并且操作该方法。分为修饰实例方法、修饰静态方法和修饰代码块。
- 读写锁 (ReentrantReadWriteLock):手动调用读写锁,读共享写独占,适合读多写少的场景。
- wait/notify 机制:手动操作线程的 wait 和 notify,配合 synchronized 使用,用于线程间的协作。
- volatile 关键字:volatile 我记得是没有原子性的,他可以保证内存可见性,禁止指令重排序,在多线程的情况下保证每个线程的数据都是最新的。
- Lock 接口:如 ReentrantLock,提供更灵活的锁控制,支持公平锁、可中断锁等。
Android 篇
一、Activity 与 Fragment 之间常见的几种通信方式?
在现代 Android 开发中,推荐使用 ViewModel 进行数据管理。
- ViewModel + LiveData:Activity 和 Fragment 公用同一个 ViewModel 实例(Scope 为 Activity 或 SharedViewModel),实现数据传递。这样即使 Fragment 销毁重建,数据依然存在。
- 接口回调:Fragment 定义接口,Activity 实现接口,Fragment 通过 getActivity().getApplicationContext() 获取 Activity 实例并调用。
- EventBus / RxBus:使用第三方库进行解耦通信,适合多层级组件通信。
class SharedViewModel : ViewModel() {
val data = MutableLiveData<String>()
}
二、BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别?
- BroadcastReceiver:是跨应用广播,利用 Binder 机制实现,支持动态和静态两种方式注册方式。系统广播属于此类,安全性较低,容易被恶意应用拦截。
- LocalBroadcastReceiver:是应用内广播,利用 Handler 实现,利用了 IntentFilter 的 match 功能,提供消息的发布与接收功能,实现应用内通信,效率和安全性比较高,仅支持动态注册。由于不涉及 Binder 交互,性能更优。
三、子线程能否更新 UI?为什么?
注意这句话,是不能直接更新,不是不能更新(极端情况下可更新)。绘制过程要保持同步(否则页面不流畅),而我们的主线程负责绘制 ui。极端情况就是,在 Activity 的 onResume(含)之前的生命周期中子线程都可以进行更新 ui,也就是 onCreate, onStart 和 onResume,此时主线程的绘制还没开始。
但在常规开发中,必须通过 Handler、post、runOnUiThread 等方式在主线程更新 UI,否则会导致 CalledFromWrongThreadException。
四、谈谈 Handler 机制和原理?
Handler 机制是 Android 处理异步消息的核心,涉及四个核心组件:Handler、MessageQueue、Looper、Message。
- 初始化:首先在 UI 线程我们创建了一个 Handler 实例对象,无论是匿名内部类还是自定义类生成的 Handler 实例对象,我们都需要对 handleMessage 方法进行重写。在 handleMessage 方法中我们可以通过参数 msg 来写接受消息过后 UI 线程的逻辑处理。
- 发送消息:接着我们创建子线程,在子线程中需要更新 UI 的时候,新建一个 Message 对象,并且将消息的数据记录在这个消息对象 Message 的内部,比如 arg1, arg2, obj 等,然后通过前面的 Handler 实例对象调用 sendMessage 方法把这个 Message 实例对象发送出去。
- 消息队列:之后这个消息会被存放于 MessageQueue 中等待被处理。此时 MessageQueue 的管家 Looper 正在不停的把 MessageQueue 存在的消息取出来。
- 分发处理:通过回调 dispatchMessage 方法将消息传递给 Handler 的 handleMessage 方法,最终前面提到的消息会被 Looper 从 MessageQueue 中取出来传递给 handleMessage 方法。
核心流程:Handler.sendMessage -> MessageQueue.enqueueMessage -> Looper.loop() -> MessageQueue.next() -> Handler.dispatchMessage -> Handler.handleMessage。
五、谈谈 Android 的事件分发机制?
Android 事件分发机制遵循从父到子,再由子到父的原则。
- dispatchTouchEvent:当点击的时候,会先调用顶级 viewgroup 的 dispatchTouchEvent。
- onInterceptTouchEvent:如果顶级的 viewgroup 拦截了此事件(onInterceptTouchEvent 返回 true),则此事件序列由顶级 viewgroup 处理,不再向下传递。
- onTouch / onTouchEvent:如果顶级 viewgroup 设置 setOnTouchListener,则会回调接口中的 onTouch,此时顶级的 viewgroup 中的 onTouchEvent 不再回调。如果不设置 setOnTouchListener 则 onTouchEvent 会回调。
- onClick:如果顶级 viewgroup 设置 onClickListener,则会回调接口中的 onClick。
- 向下传递:如果顶级 viewgroup 不拦截事件,事件就会向下传递给他的子 view,然后子 view 就会调用它的 dispatchTouchEvent 方法,递归直到最底层的 View。
Android Framework 篇
一、你了解 Android 系统启动流程吗?
Android 系统启动是一个复杂的过程,主要步骤如下:
- BootLoader:当按电源键触发开机,首先会从 ROM 中预定义的地方加载引导程序 BootLoader 到 RAM 中,并执行 BootLoader 程序启动 Linux Kernel。
- Kernel 与 Init:Linux Kernel 启动后,启动用户级别的第一个进程:init 进程。init 进程会解析 init.rc 脚本做一些初始化工作,包括挂载文件系统创建工作目录以及启动系统服务进程等。
- System Services:其中系统服务进程包括 Zygote、service manager、media 等。在 Zygote 中会进一步去启动 system_server 进程。
- AMS/WMS/PMS:然后在 system_server 进程中会启动 AMS(活动管理器)、WMS(窗口管理器)、PMS(包管理器)等服务。
- Launcher:等这些服务启动之后,AMS 中就会打开 Launcher 应用的 home Activity,最终就看到了手机的'桌面'。
二、能具体说说是怎么导致的死锁的吗?
在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据 (通常使用 copy-on-write 的策略,所以可以实现的速度快)以及所有系统对象,然后仅复制当前线程到子进程。
这里有一个关键点:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。对于锁来说,从 OS 看,每个锁有一个所有者,即最后一次 lock 它的线程。
假设这么一个环境,在 fork 之前,有一个子线程 lock 了某个锁,获得了对锁的所有权。fork 以后,在子进程中,所有的额外线程都人间蒸发了。而锁却被正常复制了,在子进程看来,这个锁没有主人,所以没有任何人可以对它解锁。当子进程想 lock 这个锁时,不再有任何手段可以解开了,从而导致死锁。
三、Zygote 为什么不采用 Binder 机制进行 IPC 通信?
Binder 机制中存在 Binder 线程池,是多线程的。如果 Zygote 采用 Binder 的话就存在上面说的 fork() 与多线程的问题了。
其实严格来说,Binder 机制不一定要多线程,所谓的 Binder 线程只不过是在循环读取 Binder 驱动的消息而已,只注册一个 Binder 线程也是可以工作的,比如 service manager 就是这样的。实际上 Zygote 尽管没有采取 Binder 机制,它也不是单线程的,但它在 fork() 前主动停止了其他线程,fork() 后重新启动了。
四、Binder 是如何做到一次拷贝的?
主要是因为 Linux 是使用的虚拟内存寻址方式,它有如下特性:
- 用户空间的虚拟内存地址是映射到物理内存中的。
- 对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射。
- 这个内存映射过程是通过系统调用 mmap() 来实现的。
- Binder 借助了内存映射的方法,在内核空间的接收方用户控件的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。
五、Binder 机制是如何跨进程的?
- Binder 驱动:在内核空间创建一块接收缓存区,实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区。
- 发送进程:发送进程通过系统调用 (copy_from_user) 将数据发送到内核缓存区。由于内核缓存区和 接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信。
Flutter 篇
一、Flutter 中的生命周期
Flutter 的 Widget 生命周期与 StatefulWidget 紧密相关:
- initState():表示当前 State 将和一个 BuildContext 产生关联,但是此时 BuildContext 没有完全装载完成,如果你需要在该方法中获取 BuildContext,可以 new Future.delayed(const Duration(seconds: 0), (){//context}); 一下。
- didChangeDependencies():在 initState() 之后调用,当 State 对象的依赖关系发生变化时,该方法被调用,初始化时也会调用。
- deactivate():当 State 被暂时从视图树中移除时,会调用这个方法,同时页面切换时,也会调用。
- dispose():Widget 销毁了,在调用这个方法之前,总会先调用 deactivate()。
- didUpdateWidget:当 widget 状态发生变化时,会调用。
通过 StreamBuilder 和 FutureBuilder 我们可以快速使用 Stream 和 Future 快速构建我们的异步控件。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 执行真正的跨线程异步操作。
二、Widget 和 element 和 RenderObject 之间的关系?
- Widget:是用户界面的一部分,并且是不可变的配置描述符。
- Element:是在树中特定位置 Widget 的实例,负责维护状态和生命周期。
- RenderObject:是渲染树中的一个对象,它的层次结构是渲染库的核心,负责布局、绘制等操作。
Widget 会被 inflate(填充)到 Element,并由 Element 管理底层渲染树。Widget 并不会直接管理状态及渲染,而是通过 State 这个对象来管理状态。Flutter 创建 Element 的可见树,相对于 Widget 来说,是可变的,通常界面开发中,我们不用直接操作 Element,而是由框架层实现内部逻辑。就如一个 UI 视图树中,可能包含有多个 TextWidget(Widget 被使用多次),但是放在内部视图树的视角,这些 TextWidget 都是填充到一个个独立的 Element 中。Element 会持有 renderObject 和 widget 的实例。记住,Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。
在第一次创建 Widget 的时候,会对应创建一个 Element,然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。
三、mixin extends implement 之间的关系?
继承(关键字 extends)、混入 mixins(关键字 with)、接口实现(关键字 implements)。这三者可以同时存在,前后顺序是 extends -> mixins -> implements。
Flutter 中的继承是单继承,子类重写超类的方法要用 @Override,子类调用超类的方法要用 super。在 Flutter 中,Mixins 是一种在多个类层次结构中复用类代码的方法。mixins 的对象是类,mixins 绝不是继承,也不是接口,而是一种全新的特性,可以 mixins 多个类,mixins 的使用需要满足一定条件。
总结
本文整理了 Android 开发中大厂秋招的常见面试题,涵盖了 Java 基础、Android 核心机制、Framework 原理及 Flutter 框架知识。内容涉及抽象类与接口区别、线程状态管理、Handler 消息机制、Binder 跨进程通信、Zygote 启动流程等关键技术点。建议求职者结合官方文档与源码深入理解,并通过实际项目巩固知识点,提升面试通过率。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online