一转眼,就到了8月中旬,金九银十还有半个月就要拉开序幕,那么我们的Android程序员关于Android开发岗位的面试,大家知不知道要对哪些知识点做准备呢?
在Android开发面试中,针对Framework的问题是常被考察的。根据近两年,总结网上的以及开发朋友的一些信息,以下是一些常见的Framework金典面试题目及其详解:
一、你了解 Android 系统启动流程吗?
当按电源键触发开机,首先会从ROM中预定义的地方加载引导程序BootLoader 到 RAM中,并执行BootLoader程序启动Linux Kernel,然后启动用户级别的第一个进程: init 进程。init 进程会解析init.rc脚本做一些初始化工作,包括挂载文件系统创建工作目录以及启动系统服务进程等,其中系统服务进程包括 Zygote、service manager、media 等。在 Zygote中会进一步去启动 system_ server进程,然后在 system_server 进程中会启动AMS、WMS、PMS等服务,等这些服务启动之后,AMS中就会打开Launcher应用的home Activity,最终就看到了手机的“桌面”。
二、system_server 为什么要在 Zygote 中启动,而不是由 init 直接启动呢?
Zygote 作为一个孵化器,可以提前加载一些资源,这样 fork()时基于 Copy-On-Write 机制创建的其他进程就能直接使用这些资源,而不用重新加载。 比如 system_server 就可以直接使用 Zygote 中的 JNI 函数、共享库、常用的类、以及主题资源。
三、为什么要专门使用 Zygote 进程去孵化应用进程,而不是让system_server去孵化呢?
首先 system_server 相比 Zygote 多运行了 AMS、 WMS 等服务,这些对一个应用程序来说是不需要的。另外进程的fork()对多线程不友好,仅会将发起调用的线程拷贝到子进程,这可能会导致死锁,而system_server 中肯定是有很多线程的。
四、能具体说说是怎么导致的死锁的吗?
在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有什么优势
性能方面
- 共享内存 0次数据拷贝
- Binder 1次数据拷贝
- Socket/管道/消息队列 2次数据拷贝
稳定性方面
- Binder:基于C/S架构,客户端 (Client) 有什么需求就丢给服务端 (Server) 去完成,架构清晰、职责明确又相互独立,自然稳定性更好
- 共享内存:虽然无需拷贝,但是控制复杂,难以使用
- 从稳定性的角度讲,Binder机制是优于内存共享的。
安全性方面
- 传统的IPC没有任何安全措施,安全依赖上层协议来确保。
- 传统的IPC方法无法获得对方可靠的进程用户ID/进程UI (UID/PID) ,从而无法鉴别对方身份。
- 传统的IPC只能由用户在数据包中填入UID/PID,容易被恶意程序利用。
- 传统的IPC访问接入点是开放的,无法阻止恶意程序通过猜测接收方地址获得连接。
- Binder既支持实名Binder,又支持匿名Binder,安全性高。
七、Binder是如何做到一次拷贝的
主要是因为Linux是使用的虚拟内存寻址方式,它有如下特性:
- 用户空间的虚拟内存地址是映射到物理内存中的。
- 对虚拟内存的读写实际上是对物理内存的读写,这个过程就是内存映射。
- 这个内存映射过程是通过系统调用mmap()来实现的。
- Binder借助了内存映射的方法,在内核空间的接收方用户控件的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。
八、MMAP的内存映射原理了解吗?
MMAP内存映射的实现过程,总的来说可以分为三个阶段:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
- 进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
- 为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
- 将新建的虚拟区结构 (vm_area_struct) 插入进程的虚拟地址区域链表或树中
(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
- 为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描 述 符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体 (struct file) , 每个
文件结构体维护着和这个已打开文件相关各项信息。 - 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为: int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库 函数。
- 内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
- 通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这 片 虚拟地址并没有任何数据关联到主存中。 注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。
真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物
理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
- 缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
- 调页过程先在交换缓存空间 (swap cache) 中寻找需要访问的内存页,如果没有则调用nopage 函 数把所缺的页从磁盘装入到主存中。
- 之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注 :修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步,这样所写的内容就能立即保存到文件里了
九、Binder机制是如何跨进程的
- Binder驱动
- 在内核空间创建一块接收缓存区,
- 实现地址映射:将内核缓存区、接收进程用户空间映射到同一接收缓存区
- 发送进程通过系统调用 (copy_from_user) 将数据发送到内核缓存区。由于内核缓存区和
接收进程用户空间存在映射关系,故相当于也发送了接收进程的用户空间,实现了跨进程通信
十、简述下Handler机制的总体原理
- Looper 准备和开启轮循: Looper#prepare()初始化线程独有的 Looper 以及 MessageQueue Looper#loop()开启死循环读取 MessageQueue 中下一个满足执行时间的 Message 尚无 Message 的话,调用
Native 侧的 pollOnce()进入无限等待存在 Message,但执行时间 when 尚未满足的话,调用
pollOnce()时传入剩余时长参数进入有限等待 - Message 发送、入队和出队: Native 侧如果处于无限等待的话:任意线程向 Handler 发送 Message 或 Runnable 后,Message 将按照 when 条件的先后,被插 入 Handler 持有的 Looper 实例所对应的
MessageQueue 中适当的位置。 MessageQueue 发现有合适的 Message 插入后将调用 Native 侧的
wake() 唤醒无限等待的线程。这将促使 MessageQueue 的读取继续进入下一次循环,此刻 Queue 中已有满足条件的
Message 则 出队返回给 Looper Native 侧如果处于有限等待的话:在等待指定时长后 epoll_wait
将返回。线程继续读取 MessageQueue, 此 刻因为时长条件将满足将其出队 Looper 处理Message 的实现: - Looper 得到 Message 后回调 Message 的 callback 属性即 Runnable,或依据 target 属性即 Handler,去执行 Handler 的回调。存在 mCallback属性的话回调 Handler$Callback 反之,回调
handleMessage()
十一、 Looper 存在哪?如何可以保证线程独有?
Looper实例被管理在静态属性sThreadLocal中ThreadLocal内部通过ThreadLocalMap持有Looper,key为ThreadLocal实例本身,value即为Looper实例每个Thread都有一个自己的ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper实例,进而保证 myLooper()可以获得线程独有的 Looper 彩蛋:一个 App 拥有几个 Looper 实例?几个 ThreadLocal 实例?几个 MessageQueue 实例?几个Message 实例?几个Handler实例一个线程只有一个 Looper实例一个 Looper 实例只对应着一个 MessageQueue实例一个MessageQueue 实例可对应多个 Message 实例,其从 Message 静态池里获取,存在50的上限一个线程可以拥有多个 Handler 实例,其Handler 只是发送和执行任务逻辑的入口和出口 ThreadLocal 实例是静态的,整个进程共用一个实例。每个 Looper 存放的 ThreadLocalMap 均弱引用它作为 key
十二、如何理解 ThreadLocal 的作用?(六)
首先要明确并非不是用来切换线程的,只是为了让每个线程方便程获取自己的 Looper 实例,见 Looper#myLooper()后续可供 Handler 初始化时指定其所属的 Looper 线程也可用来线程判断自己是否是主线程
十三、ActivityManagerService是什么?什么时候初始化的?有什么作用?九
ActivityManagerService 主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,其职责与操作系统中的进程管理和调度模块类似。ActivityManagerService进行初始化的时机很明确,就是在 SystemServer进程开启的时候,就会初始化ActivityManagerService。(系统启动流程)如果打开一个App的话,需要AMS去通知zygote进程,所有的Activity的生命周期AMS来控制
十四、ActivityThread是什么?ApplicationThread是什么?他们的区别
1、ActivityThread
在 Android中它就代表了 Android的主线程,它是创建完新进程之后,main函数被加载,然后执行一个loop的循环使当前线程进入消息循环,并且作为主线程。
2、ApplicationThread
ApplicationThread是 ActivityThread的内部类,是一个Binder对象。在此处它是作为IApplicationThread对象的server端等待client端的请求然后进行处理,最大的client就是AMS。
十五、 Instrumentation是什么?和ActivityThread是什么关系?
AMS与 ActivityThread之间诸如Activity的创建、暂停等的交互工作实际上是由Instrumentation具体操作的。每个 Activity都持有一个Instrumentation对象的一个引用,整个进程中是只有一个Instrumentation。mInstrumentation的初始化在ActivityThread::handleBindApplication函数。可以用来独立地控制某个组件的生命周期。Activity的
startActivity方法。
startActivity会调用
mInstrumentation .execStartActivity(); mInstrumentation 掉用 AMS, AMS 通过 socket 通信告知 Zygote 进程 fork 子进程。
其实除了以上Framework相关的面试题外,在Android开发岗的面试中还会出现算法、MVP架构、HashMap原理、性能优化、源码分析,甚至还有Flutter、Kotlin等问题。虽然说不同部门对应聘者的要求会有所不同,但是对于基础知识储备的考察还是一样的。
针对这种情况,我整理了一套Android面试题合集,除了以上面试题,还包含【Java 基础、集合、多线程、虚拟机、反射、泛型、并发编程、Android四大组件、异步任务和消息机制、UI绘制、性能调优、SDN、第三方框架、设计模式、Kotlin、计算机网络、系统启动流程、Dart、Flutter、算法和数据结构、NDK、H.264、H.265.音频编解码、FFmpeg、OpenMax、OpenCV、OpenGL ES】
最后
面试是一个挑战和机遇并存的过程,不论结果如何,都当做一次宝贵的经验。
整理不易,想要进阶或者是更多面试题,查看我的个人简介,说不定就有你需要的