猿辅导—春招-Android开发—一面(7分凉)
📍面试公司:猿辅导
🕐面试时间:4.20
💻面试岗位:Android开发工程师
🙌面试感想:看邮件里面只约了30min,以为没有什么难度,然后就掉以轻心了,因为确实是不太熟悉Android相关的,想说问到就摆烂了,所以还是挺吃力的,最后还是面了1h左右。
❓面试问题:
0.面试官介绍业务(挺好的,很大程度上帮助了我对这个部门的业务了解)——3min
1.自我介绍(经典背书环节)——5min
2.问项目。
因为本人的实习经历和项目比较杂乱,所以他确定了一下我的技术栈。可能后续便于针对这个技术栈提问八股(大概10min)
——————————————————————————————————
八股环节:
3.C++中的多态
多态就是同一个接口,不同实现,调用时会自动匹配对应对象的行为。
C++ 多态主要分为两类:
- 静态多态(编译期多态)发生在编译阶段典型:函数重载、运算符重载、模板优点:效率高,无运行时开销
- 动态多态(运行期多态)发生在运行阶段核心:基类指针 / 引用 指向 派生类对象实现条件:类之间有继承关系基类中有虚函数派生类重写(override)虚函数作用:提高代码扩展性、可维护性,符合开闭原则
4.C++中是如何实现使用基类指针能够调动派生类的虚函数的?
核心依靠 虚函数表 + 虚指针 实现。
实现机制:
- 包含虚函数的类,编译器会生成一张虚函数表(vtable),存放类的虚函数地址
- 每个对象会生成一个虚指针(vptr),指向所属类的虚函数表
- 派生类重写虚函数时,会用自己的虚函数地址覆盖虚函数表中对应位置
- 基类指针 / 引用调用虚函数时:不看指针类型,看指向对象的实际类型通过对象的虚指针找到虚表再从虚表中找到对应函数地址执行
一句话总结:虚表存地址,虚指针找表,运行时查表调用。
5.安卓的启动过程
安卓启动分为 5 个核心阶段:
- BootLoader 引导:硬件初始化,加载 Linux 内核
- Linux 内核启动:启动内核进程,挂载文件系统,启动 init 进程
- init 进程:安卓第一个用户进程,启动各种守护进程,启动 Zygote
- Zygote 进程:安卓所有应用进程的父进程初始化虚拟机、注册系统服务孵化 SystemServer 进程
- SystemServer 进程:系统核心进程启动 AMS、WMS、PMS 等系统服务启动桌面 Launcher,用户看到桌面,启动完成
6.安卓的四大组件,以及他们所能完成的任务
Activity
- 负责界面展示与用户交互
- 一个页面通常对应一个 Activity
- 生命周期:onCreate -> onStart -> onResume -> onPause -> onStop -> onDestroy
2. Service
- 后台无界面任务
- 用于执行耗时操作:播放音乐、后台下载、数据同步
- 分启动型、绑定型两种
3. BroadcastReceiver
- 全局消息监听 / 广播接收
- 接收系统 / 应用广播:开机、网络变化、电量低、自定义消息
- 轻量级,不适合做耗时操作
4. ContentProvider
- 跨进程数据共享
- 为不同应用提供数据访问接口
- 底层基于 Binder 实现
7.安卓的binder机制
Binder 是安卓跨进程通信(IPC) 的核心方案。
优点:
- 相比传统管道、Socket:效率高、开销小、安全
- 一次拷贝,传统 IPC 需两次拷贝
核心结构:
- Client:请求方
- Server:服务提供方
- ServiceManager:服务注册、查询(类似 DNS)
- Binder 驱动:内核层,负责进程间数据传递
工作流程:
- Server 向 ServiceManager 注册服务
- Client 向 ServiceManager 获取服务代理
- Client 通过代理向 Binder 驱动发请求
- 驱动转发给 Server,执行后返回结果
一句话:Binder 让安卓跨进程通信高效、安全、稳定。
8.C++的内存管理机制
C++ 内存分为 5 个区域:
- 栈区(stack)存放局部变量、函数参数系统自动分配 / 释放,效率高
- 堆区(heap)new/malloc 分配,delete/free 释放手动管理,容易内存泄漏、野指针
- 全局 / 静态区全局变量、static 变量程序运行期间一直存在
- 常量区字符串常量、const 常量
- 代码区存放程序执行代码
内存管理核心:
- 栈:自动管理
- 堆:手动管理,现代 C++ 推荐用智能指针自动释放
9.多线程需要注意的问题
10.如何避免死锁
死锁四个必要条件:互斥、持有并等待、不可抢占、循环等待
避免方案:
- 按固定顺序加锁(破坏循环等待)
- 一次性申请所有锁(破坏持有并等待)
- 设置锁超时(破坏不可抢占)
- 避免锁嵌套
- 减少锁粒度,用原子操作代替锁
- 使用上层工具:智能锁、线程池、协程
11.嵌套锁如何解决
嵌套锁 = 同一个线程多次加同一把锁,容易导致死锁或逻辑混乱。
解决方案:
- 使用递归锁(std::recursive_mutex)允许同一线程多次加锁解锁次数 = 加锁次数才会真正释放
- 代码重构抽离公共逻辑,避免锁内调用加锁函数
- 减小锁范围不要在加锁代码里调用其他可能加锁的方法
12.智能指针的底层结构
智能指针本质是封装了裸指针的类,利用 RAII 机制自动管理内存。
通用结构:
- 封装原生指针 T*
- 析构函数中自动释放内存
- 重载
*、->运算符,使用像普通指针
不同智能指针:
auto_ptr:废弃,不安全unique_ptr:独占指针,不能拷贝,效率最高shared_ptr:共享指针,引用计数 + 原生指针weak_ptr:辅助 shared_ptr,解决循环引用,不增加计数
13.共享智能指针如何销毁
shared_ptr 靠引用计数(reference count) 管理生命周期。
销毁规则:
- 每拷贝一次 shared_ptr,引用计数 +1
- 每析构一次、或重置(reset),引用计数 -1
- 当引用计数变为 0 时:自动调用 delete 释放托管对象再释放引用计数对象本身
注意:
weak_ptr 解决 14.内存碎片是什么?如何避免
内存碎片
已释放的、不连续的小内存块,总空间足够,但无法分配给大块内存。
分为:
- 外部碎片:空闲内存分散,无法使用
- 内部碎片:分配内存比实际使用大,浪费空间
如何避免
- 内存池:预先分配一大块内存,重复使用
- 减少频繁 new/delete
- 使用智能指针规范管理
- 尽量使用栈内存,少用堆内存
- 按需分配,避免一次性申请过大内存
15.epoll的特性,对比poll、select
共同作用
I/O 多路复用:一个线程监听多个文件描述符
性能对比
- select最大监听 1024 个 fd每次需遍历全部 fd内核态 / 用户态频繁拷贝,效率低
- poll无 1024 限制仍需遍历全部 fd,效率一般
- epoll(Linux 最优)无最大连接限制事件驱动,只返回活跃 fd无需遍历,效率 O (1)支持 ET(边缘触发)+ LT(水平触发)
epoll 核心特性
- 高效、高并发
- 零拷贝、事件回调
- 适合百万连接高并发服务器
16.coding部分(快排)
#include <iostream>
#include <vector>
using namespace std;
// 分区函数
int partition(vector<int>& nums, int left, int right) {
// 选基准值
int pivot = nums[left];
while (left < right) {
// 从右往左找小于 pivot 的数
while (left < right && nums[right] >= pivot) {
right--;
}
nums[left] = nums[right];
// 从左往右找大于 pivot 的数
while (left < right && nums[left] <= pivot) {
left++;
}
nums[right] = nums[left];
}
// 基准值归位
nums[left] = pivot;
return left;
}
// 快排递归
void quickSort(vector<int>& nums, int left, int right) {
if (left >= right) return;
int mid = partition(nums, left, right);
quickSort(nums, left, mid - 1);
quickSort(nums, mid + 1, right);
}
// 测试
int main() {
vector<int> nums = {3,1,4,2,5};
quickSort(nums, 0, nums.size()-1);
for(int x : nums) cout << x << " ";
return 0;
}
总结
因为最近可能有点懈怠,然后导致代码其实不太会写,写得磕磕碰碰的,不过好在写出来了,但是调试了可能有10min,手生了。。。
查看8道真题和解析