本文内容由ai整理,答案谨慎辨别与相信。
从牛客网上搜刮了最近1、2年面试题,大部分是校招题,可能偏基础,因为我基础不好,所以这一篇整理的基础,偏八股文比较多。感兴趣的也可以看另外一篇文章,另外一篇是去年社招面试被问到的凉经。
-
安卓基础:(几乎必问)启动流程、handler、事件方法与滑动冲突、自定义view、binder机制&aidl、activity、fragment、四大组件。(可能会问到)window、数据库、retrofit、协程、四大组件、hook、recyclerview、glide、多点触控、拖拽
-
数据结构:常用数据结构,java类。hashmap(几乎必问)、链表
-
面向对象、设计原则、设计模式与架构设计:
-
jvm:内存模型、gc算法、弱引用、虚引用、类加载机制与实际应用场景
-
性能优化、检测、埋点:内存抖动/泄露原理检测、线上检测、启动优化、埋点链路、保活
-
基础:进程、线程、多线程、线程池、git、java异常机制
-
其他:跨端组件、音视频、多媒体、平行视界、图片压缩算法、音频焦点(因为我是有点这个基础和相关业务的,所以会问一些,大部分会根据之前项目经历来问)
-
项目:star方式讲清楚
建议复习书籍:
安卓第一行代码、安卓艺术开发探索、安卓源码与设计模式实战
算法:
2线公司基本不考,非互联网公司也基本不考,二线普通公司一般逮着项目和安卓使劲扒。一线大公司基本都要考,不过社招大部分只出简单的题。
好,开背!!!
Android 面试题1.0 - 四大组件·UI·事件·列表·Handler·Fragment·布局
去重整理版,含答案
覆盖:四大组件 · View与UI · 事件分发 · RecyclerView · Handler · Fragment · 布局适配
一、Android 四大组件
1.1 四大组件有哪些?
Activity (界面)、Service (后台服务)、BroadcastReceiver (广播)、ContentProvider(数据共享)。
1.2 Activity 启动模式
|--------------------|----------------------|--------|
| 模式 | 说明 | 使用场景 |
| standard | 每次创建新实例 | 默认模式 |
| singleTop | 栈顶复用,不在栈顶则新建 | 消息详情页 |
| singleTask | 栈内复用,清除其上方所有Activity | 主页/登录页 |
| singleInstance | 独立栈,全局单例 | 电话拨打页 |
singleTop vs singleTask:singleTop 只检查栈顶,singleTask 检查整个栈并清除上方 Activity。
动态设置 :可以通过 Intent.addFlags() 动态设置,如 FLAG_ACTIVITY_SINGLE_TOP、FLAG_ACTIVITY_NEW_TASK。
1.3 Activity 生命周期
正常生命周期 :onCreate → onStart → onResume → onPause → onStop → onDestroy
A跳转B :A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop
A跳转透明B :A.onPause → B.onCreate → B.onStart → B.onResume(A不执行onStop,因为仍部分可见)
按返回键B返回A :B.onPause → A.onRestart → A.onStart → A.onResume → B.onStop → B.onDestroy
横竖屏切换 :onPause → onSaveInstanceState → onStop → onDestroy → onCreate → onStart → onRestoreInstanceState → onResume(配置 configChanges 可避免重建)
后台被回收 :onSaveInstanceState 保存状态,重建时 onRestoreInstanceState 恢复
后台到前台 :onRestart → onStart → onResume
A启动B后A调用finish() :A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop → A.onDestroy
1.4 关键生命周期方法
- onPause:可见但不可交互
- onNewIntent:singleTop 栈顶复用或 singleTask 栈内复用时调用
- onRestoreInstanceState :Activity 被系统销毁后重建时调用,在
onStart之后
- onRestart:Activity 从 stopped 状态回到前台时调用
- onStart vs onResume:onStart 表示可见,onResume 表示可交互(获得焦点)
1.5 Activity 实例化
通过 反射 实现:ClassLoader 加载 Activity 类 → 通过 newInstance() 创建实例。
1.6 Activity 和 Fragment
- 生命周期区别 :Fragment 额外有
onAttach、onCreateView、onActivityCreated、onDestroyView、onDetach
- 关系:一对多,一个 Activity 可以包含多个 Fragment
- getActivity() 返回 null :在
onDetach之后
1.7 判断 App 前台/后台
- 方案1:Activity 生命周期计数(
onStart+1,onStop-1,为0则在后台)
- 方案2:
ProcessLifecycleOwner(Jetpack)
- 方案3:
ActivityManager.getRunningAppProcesses()
1.8 ActivityThread 和 AMS
- ActivityThread :App 主线程入口,
main()方法中创建 Looper 并 loop
- ApplicationThread:ActivityThread 的内部类,是 AMS 通知 App 的 Binder 接口
- Binder 通信流程:App → IActivityManager → AMS(system_server) → ApplicationThread → ActivityThread.H(Handler)→ 主线程处理
1.9 Service
|------------------|------------------------------------------|--------------------|
| 类型 | 特点 | 场景 |
| startService | onCreate→onStartCommand,需 stopService 停止 | 后台音乐播放 |
| bindService | onCreate→onBind,解绑即销毁 | Activity与Service交互 |
Service vs 线程:Service 运行在主线程,用于后台长期任务;Thread 用于耗时操作。Service 不等于后台线程。
保活方案:前台 Service(通知栏)、双进程守护、JobScheduler、白名单。
IntentService:内部使用 HandlerThread 处理耗时任务,执行完自动 stopSelf。现已废弃,推荐 WorkManager。
1.10 BroadcastReceiver
|----------|------------------------------------------|
| 类型 | 特点 |
| 标准广播 | 异步发送,所有接收者几乎同时收到 |
| 有序广播 | 同步发送,按优先级依次传播,可截断 |
| 本地广播 | LocalBroadcastManager,只在 App 内传播,更安全高效 |
注册方式:静态注册(AndroidManifest,常驻)vs 动态注册(代码,跟随生命周期)。
指定App发送 :使用 Intent.setPackage() 指定包名。
1.11 ContentProvider
核心方法:onCreate、query、insert、update、delete、getType。用于跨进程数据共享,底层基于 Binder。
1.12 多进程
- 每个进程有独立的虚拟机实例,Application 会启动多次
- Context 提供资源访问、启动组件、获取系统服务等能力
1.13 AIDL
- 本质 :Android Interface Definition Language,底层基于 Binder
- 流程:定义 .aidl 文件 → 编译生成 Stub/Proxy → 服务端实现 Stub → 客户端通过 Proxy 调用
- 底层原理:Proxy 将数据写入 Parcel → Binder 驱动 → Stub 读取并执行 → 结果写回
1.14 设计题
记事本功能:Activity(界面)+ Service(自动保存)+ ContentProvider(数据共享)+ BroadcastReceiver(定时提醒)
日志收集上传:Service(后台收集)+ ContentProvider(日志存储)+ BroadcastReceiver(网络变化触发上传)+ WorkManager(定时任务)
二、View 与 UI
2.1 View 绘制流程
onMeasure(测量) → onLayout(布局) → onDraw(绘制)
2.2 MeasureSpec
|-----------------|-------------|-----------------|-----------------------------|
| 父约束 \ 子模式 | EXACTLY | AT_MOST | UNSPECIFIED |
| EXACTLY | EXACTLY | EXACTLY/AT_MOST | EXACTLY/AT_MOST/UNSPECIFIED |
| AT_MOST | AT_MOST | AT_MOST | AT_MOST/UNSPECIFIED |
| UNSPECIFIED | UNSPECIFIED | UNSPECIFIED | UNSPECIFIED |
UNSPECIFIED 场景:ScrollView 内部子 View 测量、RecyclerView item 测量。
2.3 父View对子View的三种约束
EXACTLY (精确值)、AT_MOST (最大值)、UNSPECIFIED(无限制)。
2.4 获取控件宽高
onWindowFocusChanged(有焦点时)
view.post(() -> {})(布局完成后)
ViewTreeObserver.addOnGlobalLayoutListener
2.5 自定义View
步骤:继承 View/ViewGroup → 三个构造函数 → 自定义属性 → onMeasure → onLayout(ViewGroup)→ onDraw → 交互逻辑
三个方法 :onMeasure(测量)、onLayout(布局)、onDraw(绘制)
实现动画 :属性动画 + invalidate() 重绘
百分比变色背景:在 onDraw 中根据百分比计算绘制区域,先用 clipRect 裁剪绘制已选色,再绘制未选色
不规则遮罩层 :Canvas.clipPath() 裁剪遮罩区域,未遮罩部分通过 onTouchEvent 判断点击位置是否在遮罩外
圆形View :重写 onMeasure(设为正方形)+ onDraw(canvas.drawCircle),需重写 onMeasure 和 onDraw
自定义时间轴:涉及缩放(ScaleGestureDetector)、滑动冲突(外部拦截法)、事件分发(dispatchTouchEvent)
2.6 关键API区别
- getMeasuredWidth vs getWidth:前者是测量宽高(onMeasure 后),后者是最终宽高(layout 后),大多数情况相同
- requestLayout vs invalidate:requestLayout 触发重新 measure+layout+draw;invalidate 只触发 draw
- SurfaceView vs View:SurfaceView 在独立线程绘制,适用于频繁更新(视频/相机);View 在主线程
- onSaveInstanceState :在
onStop之前调用,适合保存轻量级状态数据(不保存大对象/Bitmap)
2.7 Canvas 常用 API
drawCircle、drawRect、drawLine、drawPath、drawBitmap、drawText、drawArc、clipRect、clipPath、save/restore
2.8 动画
|----------|--------------------|----------|
| 类型 | 原理 | 特点 |
| 帧动画 | 逐帧播放图片 | 简单但易 OOM |
| 补间动画 | 渐变变换(平移/旋转/缩放/透明度) | 不改变真实属性 |
| 属性动画 | 通过反射修改对象属性 | 真正改变属性值 |
插值器 :定义动画变化速率(加速/减速/弹跳等),TimeInterpolator 接口
过度绘制 :同一像素被多次绘制。防止方法:减少层级、clipRect、去除不必要的背景
2.9 屏幕刷新原理
VSync 信号 → Choreographer → 遍历 View 树 → measure/layout/draw → SurfaceFlinger 合成 → 显示
2.10 其他
- 悬浮窗 :
WindowManager.addView()+TYPE_APPLICATION_OVERLAY权限
- WindowManager 两个窗口同层级:后添加的在上层,可能覆盖
- Compose vs View:声明式 vs 命令式,Compose 通过 State 变化触发重组更新 UI
- Compose 三棵树:State → Composable(组合)→ LayoutNode(布局)→ RenderNode(渲染);数据刷新时 State 变化 → 重组 → 重新布局/渲染
- Compose 优势:声明式、少代码、状态驱动、重组智能跳过
- Compose 更新UI:State 变化 → 触发 recomposition → 重新执行 Composable 函数
- Compose 与 C++ 交互:通过 JNI
- 鸿蒙 UI vs 安卓 UI:ArkUI 声明式 vs XML 命令式;ArkTS vs Java/Kotlin
- 鸿蒙 UIAbility :
onCreate → onWindowStageCreate → onForeground → onBackground → onWindowStageDestroy → onDestroy;启动模式:singleton(单实例)、specified(指定实例)、multiton(多实例)
- arkts:TypeScript 扩展,闭包即函数内部引用外部变量的函数
- 图片变圆形 :
BitmapShader+Canvas.drawCircle,或ClipPath
- 品字形布局:ConstraintLayout 约束或 LinearLayout 嵌套
- 减少布局嵌套 :ConstraintLayout、
merge、ViewStub、扁平化布局
- Android 系统架构:应用层 → 应用框架层 → 系统运行库层 → Linux 内核层
- AMS:Activity Manager Service,管理四大组件生命周期
- WMS:Window Manager Service,管理窗口和输入事件
三、事件分发与滑动
3.1 事件分发机制
Activity.dispatchTouchEvent
→ ViewGroup.dispatchTouchEvent
→ ViewGroup.onInterceptTouchEvent(是否拦截)
→ 子 View.dispatchTouchEvent
→ 子 View.onTouchEvent
→ 自身 onTouchEvent
→ Activity.onTouchEvent
|---------------------------|-------------------|---------|
| 方法 | 作用 | 返回值 |
| dispatchTouchEvent | 分发事件 | true=消费 |
| onInterceptTouchEvent | 拦截事件(ViewGroup独有) | true=拦截 |
| onTouchEvent | 处理事件 | true=消费 |
- onTouch 返回 true:事件被消费,不再继续分发
- onInterceptTouchEvent 返回 false:不拦截,传递给子 View
- MotionEvent.CANCEL:父 View 拦截了事件,通知子 View 取消(如滑动时按下按钮再滑动)
3.2 MotionEvent
事件类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL
常用属性:getX/getY(相对 View)、getRawX/getRawY(相对屏幕)、getPointerId、getPressure、getSize、getEventTime
3.3 滑动冲突
|-----------|--------------------------------------------------------------------|-------------|
| 方案 | 做法 | 适用场景 |
| 外部拦截法 | 父 View 的 onInterceptTouchEvent 中判断 | 父View 需要控制时 |
| 内部拦截法 | 子 View dispatchTouchEvent 中 requestDisallowInterceptTouchEvent | 子View 需要优先时 |
两个嵌套 RecyclerView:根据滑动方向和边界判断,外部拦截法处理
ScrollView 嵌套按钮滑动:DOWN 事件给按钮 → MOVE 事件父View 拦截 → 按钮 收到 CANCEL
四、RecyclerView 与 ListView
4.1 RecyclerView 四级缓存
|----|------------------------|-----------------------------|
| 级别 | 缓存 | 说明 |
| 1 | Scrap | 屏幕内临时分离的 ViewHolder |
| 2 | Cache | 屏幕外缓存(默认2个) |
| 3 | ViewCacheExtension | 自定义缓存 |
| 4 | RecyclerPool | 全局缓存池(按 viewType 分类,默认每种5个) |
4.2 RecyclerView vs ListView
|---------------|----------------------------|--------------------------------|
| 对比 | RecyclerView | ListView |
| 缓存 | 四级缓存 | 两级缓存(ScrapViews + ActiveViews) |
| 布局 | LayoutManager 灵活 | 仅纵向 |
| 动画 | ItemAnimator 内置 | 无 |
| 分割线 | ItemDecoration | divider 属性 |
| 点击 | 自己实现 | setOnItemClickListener |
| Header/Footer | 多 ViewType 或 ConcatAdapter | addHeaderView/FooterView |
4.3 RecyclerView 优化
setHasFixedSize(true)(item 大小不变时)
setRecycledViewPool共享缓存池
- DiffUtil 替代
notifyDataSetChanged
- 预加载(
recyclerView.setItemViewCacheSize)
- 减少
onBindViewHolder耗时操作
- 共用
RecyclerView的RecycledViewPool
4.4 设计模式
- 观察者模式:AdapterDataObservable
- 适配器模式:Adapter
- 责任链模式:ItemDecoration
- ** builders 模式**:LayoutManager
4.5 DiffUtil
最重要的两个接口:areItemsTheSame(是否同一 item)和 areContentsTheSame(内容是否相同)
4.6 ViewHolder 作用
复用 View 避免重复 findViewById,减少创建View的开销。
4.7 ViewPager2
底层基于 RecyclerView 实现,支持竖向滑动、notifyDataSetChanged 更好。
4.8 视频列表
推荐 ViewPager2 + Fragment,预加载前后各1个,其余回收。
4.9 列表问题
- 闪动 :DiffUtil 或
setHasStableIds(true)
- 错位:复用时未重置状态
- 点击错乱 :position 使用
holder.adapterPosition而非holder.layoutPosition
- 曝光不准:滑动回调中判断可见范围
五、Handler 与线程通信
5.1 Handler 原理
四大核心:Message (消息)、MessageQueue (队列)、Looper (循环器)、Handler(处理器)
流程:Handler.sendMessage → MessageQueue.enqueueMessage → Looper.loop() 取出 → Handler.dispatchMessage → handleMessage
5.2 Looper 为什么不阻塞主线程
Looper.loop() 内部调用 nativePollOnce(),当没有消息时线程通过 Linux epoll 机制 挂起(休眠),不消耗 CPU。有新消息时通过 nativeWake() 唤醒。
- 空消息时 :线程挂起,等待
nativeWake()唤醒
- 空闲机制 :
IdleHandler,当消息队列空闲时执行
- 一个线程只有1个 Looper、1个 MessageQueue,但可以有多个 Handler
- 区分 Message 归属 :Message 的
target字段指向发送它的 Handler
5.3 同步屏障
同步屏障是一种特殊 Message(target=null),插入后只处理异步消息,同步消息被阻塞。用于 UI 渲染优先级高于普通消息。
5.4 Handler 内存泄漏
原因:非静态内部类持有外部 Activity 引用 → Message 持有 Handler → MessageQueue 持有 Message → Activity 无法回收
解决 :静态内部类 + WeakReference,或者 onDestroy 中 removeCallbacksAndMessages(null)
5.5 关键区别
- Handler.post vs Message:post 内部也是发送 Message,只是 callback 不同
- view.post vs handler.post:view.post 在 View attach 到 Window 前会排队等待
- 同步消息 vs 异步消息:异步消息可被同步屏障优先处理
- 子线程创建 Handler :需先
Looper.prepare()+Looper.loop()
5.6 其他
- Message 对象复用 :通过
sPool链表复用,避免频繁创建对象,obtain()从池中取
- MessageQueue 排序 :按
when(延迟时间)排序,post和postDelay都是按时间顺序插入
- 停止子线程 Looper :
quit()(立即移除所有消息)、quitSafely()(处理完已有消息再退出)
- 子线程 Handler 崩溃:会导致线程退出,不会影响其他线程
- 子线程 Handler 加锁:MessageQueue 内部已加锁,不需要额外加锁
- IdleHandler:在 Looper 空闲时执行,运行在 Looper 所在线程;新消息到来时 IdleHandler 会被执行完再处理
- ThreadLocal:线程本地存储,保证每个线程有独立的 Looper
- 子线程到主线程 :
new Handler(Looper.getMainLooper()).post(() -> {})
- 网络请求到主线程 :通过 Handler 或
runOnUiThread
- Handler 跨进程:基于 Binder 实现 Messager
六、Fragment
6.1 Fragment 生命周期
onAttach → onCreate → onCreateView → onActivityCreated → onStart → onResume → onPause → onStop → onDestroyView → onDestroy → onDetach
与 Activity 关系:一对多,一个 Activity 可包含多个 Fragment。
真正加入 Activity :onActivityCreated 之后
6.2 Fragment 常见问题
- 生命周期错乱:异步回调时 Fragment 可能已 detach,需判空
- 重复请求 :
onCreateView可能多次调用,VM + 缓存策略
- 重复订阅 :使用
viewLifecycleOwner而非this作为 LiveData observer
6.3 Fragment 交互
- Fragment → Activity :接口回调或
getActivity()
- Activity → Fragment :
fragment.setXxx()
- Fragment ↔ Fragment:通过共享 ViewModel 或 Fragment Result API
- setArguments vs 接口传参:setArguments 适合初始化参数(可恢复),接口适合运行时交互
6.4 其他
- add vs replace:add 叠加(需配合 hide/show),replace 替换(销毁旧的)
- BackStack 返回 :
onDestroyView→ 返回时onCreateView重建
- 动画 :
FragmentTransaction.setCustomAnimations
- 单Activity+多Fragment:减少 Activity 开销,Navigation 组件推荐
- LiveData 新注册获取旧数据:LiveData 的 version 机制,新 observer 注册时会比较 version 并分发
七、布局与适配
7.1 布局优化
|--------------|------|----------------------------------------------------------------|
| 标签 | 作用 | 原理 |
| include | 复用布局 | 直接拷贝布局内容 |
| merge | 减少层级 | 作为根标签时,子 View 直接添加到父容器 |
| ViewStub | 懒加载 | 不参与 measure/layout,setVisibility(VISIBLE) 或 inflate() 时才加载 |
ViewStub 原理:ViewStub 的 width/height 为 0 且不绘制,inflate 时用真实布局替换自身,替换后 ViewStub 从父容器移除。
7.2 屏幕适配
- dp/sp/px:dp 密度无关(1dp = density px),sp 缩放无关(跟随系统字体),px 物理像素
- dp 转 px :
px = dp * density,density = dpi / 160
- 适配方案:smallestWidth 限定符、今日头条方案(修改 density)、ConstraintLayout 百分比
- 半屏适配 :ConstraintLayout 的
percentWidth或Guideline
- drawable 资源选择:根据屏幕密度(mdpi/hdpi/xhdpi/xxhdpi/xxxhdpi)选择对应文件夹
Android 面试题2.0 - Java 基础·集合·并发
去重整理版,含答案
覆盖:网络编程 · Java基础 · 集合框架 · 并发与锁 · JVM与内存 · Kotlin · 网络框架
八、网络编程
8.1 TCP vs UDP
|-----|------------|----------|
| 对比 | TCP | UDP |
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠传输 | 不可靠 |
| 传输 | 字节流 | 数据报 |
| 速度 | 较慢 | 较快 |
| 场景 | 文件传输、HTTP | 视频通话、DNS |
8.2 TCP 三次握手
- 客户端 → SYN → 服务端
- 服务端 → SYN+ACK → 客户端
- 客户端 → ACK → 服务端
为什么三次:防止已失效的连接请求到达服务端导致资源浪费。两次无法确认客户端接收能力。
第一次握手失败:客户端超时重传 SYN(指数退避,默认5次)
SYN 重传序列号:不同,每次 SYN 的序列号随机生成
第一个带数据的包:第三次握手的 ACK 可以携带数据
8.3 TCP 四次挥手
- 主动方 → FIN → 被动方
- 被动方 → ACK → 主动方
- 被动方 → FIN → 主动方
- 主动方 → ACK → 被动方
为什么四次:FIN 只表示不再发送数据,但还可以接收;对方可能还有数据要发,所以 ACK 和 FIN 分开发。
SYN的ACK丢失:客户端认为连接未建立,重传SYN;服务端已进入ESTABLISHED,超时后关闭连接
8.4 TCP 滑动窗口与拥塞控制
- 滑动窗口:接收方通告窗口大小控制发送速率,两端窗口大小不一定相同
- 流量控制:接收方通过窗口大小控制发送方速率
- 拥塞控制:慢启动→拥塞避免→快重传→快恢复
- 超时重传:RTO 动态计算,基于 RTT 的加权平均值
8.5 TCP 首部与可靠传输
TCP 首部 20 字节:源端口、目的端口、序列号、确认号、数据偏移、标志位、窗口大小、校验和、紧急指针
可靠传输:序列号+确认号+超时重传+滑动窗口+拥塞控制+校验和
区分连接:四元组(源IP+源端口+目的IP+目的端口)
8.6 HTTP
HTTP 1.0 → 1.1 → 2.0:
- 1.0:短连接,每次请求新建连接
- 1.1:持久连接(keep-alive),管道化
- 2.0:多路复用、头部压缩(HPACK)、服务端推送、二进制帧
HTTP 3.0:基于 QUIC(UDP),解决队头阻塞,更快握手
GET vs POST:GET 幂等、参数在 URL、有长度限制;POST 非幂等、参数在 body、无限制
POST body 不可见原因:body 在请求体中,URL 中不可见,但抓包可以看到
8.7 HTTPS
HTTPS = HTTP + TLS/SSL
流程:TCP连接 → TLS握手(证书验证+密钥协商) → 对称加密传输
- 对称加密:加解密用同一密钥,速度快(AES、DES)
- 非对称加密:公钥加密私钥解密,安全性高(RSA、ECC)
- HTTPS中:TLS握手用非对称加密协商密钥,数据传输用对称加密
- CA证书:CA私钥签名 → 浏览器用CA公钥验证 → 确认服务器身份
- 无法完全防御中间人:客户端不验证证书时可被中间人攻击
8.8 其他
- 长连接:keep-alive,TCP 连接复用
- WebSocket:全双工持久连接,基于 HTTP 升级
- DNS:域名解析为IP,优化:DNS缓存、预解析、HTTPDNS
- QUIC:基于UDP,0-RTT握手,解决TCP队头阻塞
- 从URL到页面:DNS解析 → TCP连接 → TLS握手 → HTTP请求 → 服务器处理 → 响应 → 浏览器渲染
- 五层模型:应用层→传输层→网络层→数据链路层→物理层
- TCP属于传输层,HTTP属于应用层,Socket是应用层和传输层之间的接口
九、Java 基础
9.1 面向对象
- 封装:隐藏内部细节,暴露接口
- 继承:子类复用父类属性和方法
- 多态:编译时多态(重载)+ 运行时多态(重写)
重写 vs 重载:重写是子类覆盖父类方法(签名相同),重载是同类中方法名相同参数不同
9.2 String 相关
- String :不可变,常量池,
+拼接会创建 StringBuilder
- StringBuilder:可变,线程不安全,性能好
- StringBuffer:可变,线程安全(synchronized),性能略差
- String.intern():将字符串放入常量池,常量池有则返回引用
- String 不可被继承:final 类
- String str1="abc" vs new String("abc"):前者在常量池,后者在堆
9.3 关键字
- final:修饰类不可继承、方法不可重写、变量不可修改
- static:属于类而非实例,静态内部类不持有外部类引用
- this:当前对象引用
- super:父类对象引用
9.4 抽象类 vs 接口
|-----|--------|-------------------|
| 对比 | 抽象类 | 接口 |
| 方法 | 可有实现 | Java8+ 可有 default |
| 变量 | 可有成员变量 | 只能有常量 |
| 继承 | 单继承 | 多实现 |
| 构造器 | 有 | 无 |
接口可以继承接口。
9.5 equals 与 hashCode
- equals 相同,hashCode 必须相同
- hashCode 相同,equals 不一定相同(哈希冲突)
- HashMap 先调用 hashCode 定位桶,再调用 equals 比较键
- 重写 equals 必须重写 hashCode
9.6 其他
- 内部类访问局部变量需 final:局部变量在栈上,内部类对象可能在堆上,需保证一致性(Java8 隐式 final)
- 非静态内部类持有外部类引用,静态内部类不持有
- protected :同包+子类可访问;default:仅同包
- 注解:源码级(@Override)、编译时(@Deprecated)、运行时(@Inject),运行时注解通过反射读取
- try-catch-finally return:finally 总是执行,finally 中的 return 会覆盖 try 中的
- 深拷贝 vs 浅拷贝:深拷贝复制所有层级对象,浅拷贝只复制引用。深拷贝实现:Cloneable + 手动复制引用类型,或序列化
- int 范围:-2³¹ ~ 2³¹-1(4字节),最高位是符号位
- 泛型:编译时类型检查,运行时擦除(Type Erasure)
- 反射:运行时获取类信息并操作,通过 Class 对象访问字段/方法/构造器
- 动态代理:JDK 动态代理(接口代理,Proxy.newProxyInstance)、CGLIB(子类代理,MethodInterceptor)
- 序列化:对象→字节流(Serializable/Parcelable),JSON序列化(Gson/Moshi)
十、集合框架
10.1 ArrayList vs LinkedList
|------|-----------|-------------|
| 对比 | ArrayList | LinkedList |
| 底层 | 数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 插入删除 | O(n)(需移动) | O(1)(找到节点后) |
| 内存 | 连续内存,局部性好 | 每个节点额外存储指针 |
- ArrayList 扩容:默认容量10,扩容为1.5倍
- ArrayList 线程不安全:多线程修改可能数组越界或数据覆盖
- 线程安全替代 :
CopyOnWriteArrayList(写时复制)
- foreach 修改检测 :
checkForComodification,modCount 与 expectedModCount 不一致抛 ConcurrentModificationException
10.2 HashMap
- 底层:数组+链表+红黑树(JDK 1.8)
- 1.7 vs 1.8:1.7 头插法(并发死链),1.8 尾插法+红黑树
- 默认大小:16,扩容因子0.75
- 哈希冲突:链表法,链表长度≥8且数组≥64转红黑树
- 查找复杂度:O(1)(无冲突),O(n)(链表),O(log n)(红黑树)
- 可以存 null key:放在桶0
- 线程不安全:1.7 并发扩容死链,1.8 数据覆盖
- 线程安全方案:ConcurrentHashMap、Collections.synchronizedMap
10.3 ConcurrentHashMap
- 1.7:分段锁(Segment),每个段独立加锁
- 1.8:CAS + synchronized(锁桶的头节点),粒度更细
- size():1.7 遍历所有Segment,1.8 用 baseCount + CounterCell 统计
10.4 其他集合
- LinkedHashMap:HashMap + 双向链表,维护插入/访问顺序
- TreeMap:红黑树,按 key 排序
- SparseArray:Android 特有,int key 替代 HashMap<Integer,V>,更省内存
- HashSet:底层是 HashMap,value 为固定对象
- CopyOnWriteArrayList:写时复制,适合读多写少
十一、并发与锁
11.1 线程
状态:NEW → RUNNABLE → RUNNING → BLOCKED → WAITING → TIMED_WAITING → TERMINATED
创建方式:继承 Thread、实现 Runnable、实现 Callable、线程池
安全停止 :中断标志位(interrupt()),不推荐 stop()
11.2 线程池
七大参数:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler
执行流程:提交任务 → 核心线程未满则创建 → 核心线程满则入队列 → 队列满则创建非核心线程 → 最大线程满则拒绝
拒绝策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最老)
线程复用:Worker 线程循环从 workQueue 取任务执行
非核心线程销毁:keepAliveTime 超时后,非核心线程从 workQueue.poll 返回 null,退出循环
CPU 密集型 :核心线程数 = CPU核心数 + 1;IO 密集型:核心线程数 = CPU核心数 × 2
11.3 并发三大特性
- 原子性:synchronized、Lock、Atomic 类、CAS
- 可见性:volatile、synchronized、Lock
- 有序性:volatile(禁止重排)、synchronized、happens-before
11.4 synchronized
锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
可重入:是的,同一线程可重复获取同一把锁
锁 .class vs 锁对象:锁 .class 是类锁(所有实例共享),锁对象是实例锁(各实例独立)
vs ReentrantLock:ReentrantLock 可中断、可超时、可公平、Condition 精确唤醒;synchronized 自动释放
11.5 volatile
- 保证可见性和有序性,不保证原子性
- 双重检查锁加 volatile:防止指令重排导致其他线程看到未初始化的对象
- volatile 变量不是线程安全的:i++ 仍然不安全
11.6 CAS 与 AQS
- CAS:Compare And Swap,乐观锁实现,无锁操作。ABA 问题用 AtomicStampedReference 解决
- AQS:AbstractQueuedSynchronizer,ReentrantLock/CountDownLatch 底层,FIFO 双向链表 + state 变量
11.7 死锁
四个必要条件:互斥、占有且等待、不可抢占、循环等待
解决:破坏任一条件,如按顺序加锁、超时放弃
11.8 常见并发题
三个线程顺序打印ABC:
- 方案1:
join()保证顺序
- 方案2:
wait()/notify()+ 标志位
- 方案3:
CountDownLatch
生产者消费者 :BlockingQueue 或 wait()/notify()
读写场景 :读多写少用 CopyOnWriteArrayList 或 ReentrantReadWriteLock;写多用 synchronized
十二、JVM 与内存
12.1 JVM 内存结构
|-------------|------|--------------------|
| 区域 | 线程共享 | 内容 |
| 堆 | ✅ | 对象实例、数组 |
| 方法区/元空间 | ✅ | 类信息、常量、静态变量 |
| 栈 | ❌ | 栈帧(局部变量、操作数栈、返回地址) |
| 程序计数器 | ❌ | 当前指令地址 |
| 本地方法栈 | ❌ | native 方法 |
JDK 1.7 → 1.8:永久代移除,元空间(Metaspace)使用本地内存
12.2 GC
判断垃圾:可达性分析(从 GC Root 出发,不可达则为垃圾)
GC Root:栈帧局部变量、静态变量、JNI 引用、活跃线程
两个变量互相引用:如果从 GC Root 不可达,仍会被回收
引用类型:
- 强引用 :不会被回收(
Object o = new Object())
- 软引用 :内存不足时回收(
SoftReference,图片缓存)
- 弱引用 :下次 GC 回收(
WeakReference,Handler、ThreadLocal)
- 虚引用 :仅跟踪回收通知(
PhantomReference)
垃圾回收算法:
- 标记清除(碎片多)
- 标记复制(新生代,空间换时间)
- 标记整理(老年代,无碎片但慢)
- 分代收集(新生代复制,老年代整理)
GC 触发时机:堆内存不足、System.gc()(建议)、CMS/G1 周期性
12.3 内存泄漏
常见场景:静态持有 Context、内部类持有外部引用、未取消注册/动画、Handler 延迟消息、资源未关闭
排查:Android Profiler → Dump Java Heap → 搜索 Activity → 查看引用链
LeakCanary:通过 ActivityLifecycleCallbacks 监控 Activity 销毁 → 弱引用 + ReferenceQueue 检测 → 分析引用链(HPROF)→ 通知
12.4 类加载
双亲委派:加载类时先委托父加载器,父加载器无法加载才自己加载
好处:避免重复加载、保护核心类不被篡改
打破双亲委派 :自定义 ClassLoader 重写 loadClass;热修复就是打破双亲委派
PathClassLoader vs DexClassLoader:前者加载已安装 APK,后者加载 dex/jar/apk
十三、Kotlin
13.1 Kotlin vs Java
- 空安全 :编译时检查(
?可空、!!非空断言、?.安全调用)
- 空异常检查:编译时
- ===:引用相等(比较对象地址)
- by lazy :懒加载,首次访问时初始化;多线程模式默认
LazyThreadSafetyMode.SYNCHRONIZED(线程安全)
- lateinit:延迟初始化 var,只能用于非基本类型
13.2 Kotlin 特性
- 内联函数 :
inline关键字,编译时展开函数体,减少 lambda 对象创建
- 密封类 :
sealed class,限制子类数量,when 表达式穷举
- 高阶函数:以函数作为参数或返回值
- 扩展函数:编译时转为静态函数,第一个参数为接收者
- let/also/apply/run:
-
let:it 引用,返回 lambda 结果
-
also:it 引用,返回对象本身
-
apply:this 引用,返回对象本身
-
run:this 引用,返回 lambda 结果
13.3 协程
- 协程 vs 线程:协程是用户态轻量级线程,由协程调度器管理,切换不涉及内核态
- 轻量原因:协程挂起时不占线程,恢复时继续执行;线程阻塞时占着资源
- 挂起本质 :状态机 + 回调,
suspend函数编译后生成 Continuation
- 启动方式 :
launch(不返回结果)、async(返回 Deferred)
- 取消 :
job.cancel(),需配合ensureActive()或isActive检查
- Job:协程的生命周期管理、取消、等待
13.4 其他
- KAPT vs KSP:KAPT 生成 Java Stub 再处理,慢;KSP 直接处理 Kotlin AST,快
- KMP:Kotlin Multiplatform,逻辑层共享,UI 各端原生
- Java 调用 Kotlin 问题:空安全不保证(Java 可传 null)、默认参数需 @JvmOverloads
十四、网络框架与第三方库
14.1 OkHttp
拦截器链(责任链模式):RetryAndFollowUp → Bridge → Cache → Connect → CallServer
连接复用:ConnectionPool,保持连接减少握手
缓存:DiskLruCache,根据 HTTP 缓存头判断
同步 vs 异步:同步阻塞当前线程,异步通过 Dispatcher 调度到线程池
设计模式:责任链(拦截器)、工厂(OkHttpClient)、建造者(Request)
14.2 Retrofit
核心原理:动态代理(Proxy.newProxyInstance)生成接口实现类,注解解析生成 Request
为什么不实现接口:动态代理在运行时生成实现类
设计模式:动态代理、工厂、适配器(CallAdapter)、建造者
14.3 Glide
工作流程:检查缓存 → 活动资源 → 内存缓存 → 磁盘缓存 → 网络请求
三级缓存:活动资源(弱引用)→ 内存缓存(LruCache)→ 磁盘缓存(DiskLruCache)
绑定生命周期:添加无 UI Fragment(RequestManagerFragment),监听 Activity 生命周期
缓存 key:url + 签名 + 宽高 + 变换 等
加载大图 :downsample 降采样 + BitmapPool 复用
14.4 其他库
- EventBus:观察者模式,注册 → 注解扫描 → 事件发送 → 反射调用
- Room vs SQLite:Room 是 SQLite 的 ORM 封装,编译时检查 SQL,支持 LiveData/Flow
- LiveData:生命周期感知,只在活跃状态通知,版本号防止重复通知
- setValue vs postValue:setValue 主线程,postValue 子线程(最终合并到主线程)
- ViewModel 持久化:通过 ViewModelStore 在配置变更时保存,非配置变更销毁时清除
- ViewModel 比 Activity 生命周期长:ViewModelStore 在 NonConfigurationInstances 中保存
Android 面试题3.0 - JVM·Kotlin·网络·框架
去重整理版,含答案。保留原始问题格式。
本篇所有问题
- JVM 内存结构(堆栈方法区/元空间程序计数器本地方法栈)?
- JDK1.7 和 1.8 的内存模型变化?
- Java 内存区域?
- Java 并发模型?
- 哪些区域线程共享哪些是线程私有的?
- 堆存放什么?
- 堆和栈分别用来存什么?
- 栈帧结构?
- 方法调用时栈空间变化?
- 方法返回值保存在哪里(操作数栈)?
- 程序地址空间?
- 堆区和栈区作用?
- 物理内存和虚拟内存区别?
- 32 位 CPU 架构对进程的虚拟内存是多大?
- 内存管理?
- 垃圾回收算法(标记清除标记复制标记整理分代收集)?
- 垃圾回收器分类并行收集器原理?
- CMS/G1?
- 分代回收(新生代/老年代)思想为什么要这么划分?
- GC 什么时候发生?
- GC Root 有哪些?
- 安卓中哪些可以作为 GC Root?
- 如何判断一个对象是垃圾对象?
- 可达性分析两个变量互相引用会不会被 GC?
- 如何确保所有对象被穷举(安全点安全区域)?
- 引用计数和可达性分析区别 Java 使用哪种?
- 强/软/弱/虚引用区别与使用场景?
- Java 引用类型和 C++ 引用有什么区别?
- Java 引用类型在实际开发中有哪些应用场景?
- 图片缓存用什么引用?
- 弱引用如何恢复?
- 内存泄漏定义常见场景?
- 安卓常见的内存泄漏场景?
- 从内存泄漏角度静态内部类和匿名内部类有什么区别?
- 匿名内部类声明为静态的可以持有外部类吗为什么?
- 内存泄漏怎么排查?
- native 内存泄漏第三方 SDK 泄漏没代码怎么办?
- 内存泄漏工具怎么使用?
- 如何定位 Android 中的内存泄漏具体操作流程?
- Android Studio 内置的 Android Profiler 如何操作?
- 定位内存泄漏打印快照时机如何选择?
- 内存占用的分析中的火焰图怎么看?
- LeakCanary 原理检测机制?
- LeakCanary 如何实现应用启动后自动初始化?
- LeakCanary 检测出来什么类型的内存泄漏?
- 真正检测的对象是哪个?
- 怎么判断是哪一种情况导致的内存泄漏?
- 从检测到内存泄漏到弹出提示引导修复链路如何实现?
- 什么时候触发内存泄漏检测?
- LeakCanary 监控什么安卓组件?
- 监控 ViewModel 怎么注册的?
- 场景 Activity 创建 Handler 并 post 消息 Activity 销毁但消息仍在队列中 LeakCanary 能检测到吗?
- 已发送的 message 无法被 remove 怎么处理?
- LeakCanary 告诉内存泄漏开发者如何验证排查是否真实泄漏?
- 如何避免内存泄漏开发过程中?
- 内存峰值降低 18% 有没有分析是哪类对象的优化?
- 类加载机制(加载→验证→准备→解析→初始化)?
- ClassLoader 的整体架构和理解?
- ClassLoader 在安卓里的应用场景?
- 双亲委派机制(是组合不是继承)好处?
- 如何打破双亲委派机制?
- 利用 ClassLoader 怎么实现热修复原理是什么?
- 热修复方案?
- 如何自定义类加载器生产环境用途?
- 如果自定义一个 String 类会怎么加载能编译成功吗?
- PathClassLoader 与 DexClassLoader 区别?
- 假如有个 jar 包不能被 maven 下载到本地 library 应该怎么办?
- new String() 创建几个对象?
- JVM 怎么保证一个类只加载一次?
- 类加载时对静态成员变量和非静态成员变量处理有什么不同?
- 一个 static 变量更新值另一个进程能获取最新值吗另一个线程呢?
- 静态变量存放在哪里?局部变量存放在哪里?
- 启动一个 Activity 设置 TextView 文字标题什么存在堆中什么存在栈中?
- JMM 的意义?
- 为什么 Java 会有线程不安全(并发)底层原因(OS)?
- Kotlin 与 Java 区别?
- Kotlin 相比 Java 有哪些独有的好用优势?
- Kotlin 语法特性?
- Kotlin 怎么保证空安全?
- Kotlin 空异常检查是编译时还是运行时?
- 看过 Kotlin 的书吗?
- Kotlin 中===是什么作用?
- java 调用 kotlin 可能出现什么问题?
- by lazy 原理会不会有多线程问题?
- by lazy 和 lateinit 的区别?
- 内联函数原理和优点?
- 密封类(sealed class)是什么优势是什么?
- 高阶函数是什么?
- 扩展函数怎么实现的(编译时转化为静态函数)?
- 协程与线程区别?协程为什么轻量?节省的内存在哪里?
- 协程是什么有什么用?
- 协程挂起是怎么实现的本质是什么(异步回调)?
- 协程调度实现为什么不需独立线程?
- 平时是怎么用协程的?
- 启动协程的几种方式?
- 协程怎么取消?
- 协程里 Job 存在的意义是什么?
- 在项目里用协程做什么了?
- 协程内联函数?
- DSL 理解(高阶函数 + 带接收者的 Lambda)?
- let also apply run 区别?
- Kotlin 非空参数在 Java 调用时是否安全(不安全抛 IllegalArgumentException)?
- KAPT 与 KSP 区别?
- Kotlin 使用过哪些单例?
- lazy?
- TCP 与 UDP 区别?安卓开发中的使用场景?
- TCP 三次握手(为什么是三次?两次不行吗)?
- TCP 第一次握手失败后客户端的重试策略?
- TCP 第一个带数据的包是第几个包(第三个)?
- TCP 四次挥手(为什么是四次)?
- 三次握手服务端返回的 ACK 报文丢失客户端和服务端分别会发生什么?
- 重传的 SYN 报文的序列号和之前一样吗(不一样)?
- TCP 滑动窗口机制窗口大小动态变化(接收方缓冲区)两端窗口大小一样吗?
- TCP 流量控制机制?
- TCP 拥塞控制(慢启动拥塞避免快重传快恢复)?
- TCP 超时重传算法超时时间设置?
- TCP 首部 20 个字节包含哪些内容?
- TCP 如何保证可靠传输?
- 服务端用 80 端口接受多个连接如何区分不同连接(四元组)?
- 什么时候用 UDP?游戏为什么适合 UDP?网络差时 UDP 优势还能发挥吗?
- 长连接短连接区别?怎么保持长连接?
- HTTP 报文结构?请求头包含内容?请求首部?
- HTTP1.0/1.1/2.0 区别?报文头部有什么区别?
- HTTP2.0 做了什么优化?
- HTTP2.0 最大改进?
- HTTP2.0 基于什么协议?HTTP3.0 基于 UDP 的好处?
- HTTP 中的同步和异步?
- HTTP 是什么协议?
- 应用里用了什么协议(GET POST PUT)?
- GET 和 POST 区别?实际使用中参数有何不同?
- GET 查询数据怎么携带?POST 怎么携带?
- POST 的 body 不可见解释为什么?
- 四种 HTTP 请求区别和使用场景?
- HTTPS 与 HTTP 区别?
- HTTPS 为什么更安全?
- HTTPS 如何建立连接?
- HTTPS 原理加密算法和整体流程?
- 对称加密和非对称加密 HTTPS 什么时候用对称什么时候用非对称?
- 对称加密和非对称加密加密解密流程?
- 常见对称加密和非对称加密算法?
- CA 证书加解密过程?
- 证书在 HTTPS 中起的作用?
- 如何校验证书有效性(如访问百度时)?
- HTTPS 能否完全防御中间人攻击?
- 抓包了解吗?
- WebSocket 协议的理解?
- WebSocket 原理与 Socket 区别?好处?
- 表示层和会话层功能?
- IP 层负责什么?TCP 负责什么?
- 浏览器输入 URL 到页面渲染的完整过程?
- 网络请求用哪个第三方库?看过源码吗?
- 网络错误情况怎么处理?
- 打开浏览器白屏可能是什么情况(不是 403 404)?
- 网络请求如何完成(结合项目)?
- 通信协议了解哪些(HTTP HTTPS MQTT)?
- HTTP 属于 TCP/IP 协议哪一层?底层用哪种传输协议?
- TCP 属于哪一层?
- 五层网络模型?
- 网络七层结构?
- 传输层和网络层能合并吗?
- Socket 位于哪一层?
- 网络如何确保安全?如何确保秘钥正确性?
- IPv4 和 IPv6 区别?
- HTTP 实体压缩?有针对头部的压缩吗?
- 差量更新/下载断点续传?
- OkHttp 源码拦截器链(责任链模式)?
- 责任链好处是什么?
- OkHttp DNS 注入处理?
- OkHttp 设计模式有哪些?
- OkHttp 底层原理(连接复用?缓存)?缓存用哪种数据结构存储?
- OkHttp 请求有同步和异步两种有什么区别?
- Retrofit 底层动态代理原理?为什么接口不用实现?
- Retrofit 的核心原理?
- Retrofit 与 OkHttp 区别?
- Retrofit 用到了哪些设计模式(动态代理工厂适配器建造者)?
- Retrofit 如何通过注解实现网络请求?
- 两个接口会生成不同的实例对象吗?
- 动态代理优势?
- Retrofit 发送请求如何记录日志(拦截器)?
- Glide 源码整体运作流程?
- Glide 工作原理?
- Glide 比其他的优势?
- Glide 三级缓存 LRU 算法?
- Glide 缓存原理?
- Glide 图片缓存 key 生成规则(url 图片签名等)?
- 在客户端如何实现 Glide 图片缓存?
- 如何判断下次请求 Glide 是不是需要更新?
- 排行榜头像一直在更新如何实现缓存?
- Glide 一次请求多少个图片?
- Glide 加载网络资源实际请求方法(HEAD 非 GET)?
- Glide 如何绑定生命周期?Activity 和 Fragment 生命周期在 Glide 里有什么区别?
- Glide 与其他图片框架优劣比较?
- Glide 处理大图加载的逻辑?
- Glide 在项目中用到了什么地方?
- 如何解决图片加载的错位问题?
- 图片加载用过哪些库?Glide 还用过什么其他功能?
- EventBus 原理优劣使用场景?
- EventBus 手写实现(单例 + 观察者模式)?
- LeakCanary 原理检测机制?
- LeakCanary 如何实现应用启动后自动初始化?
- Room 与 SQLite 区别?Room 原理?
- Flow 使用场景?
- LiveData 与普通的观察者模式有什么区别?
- 处于后台时多次更新 LiveData 的 value 会触发 onChange 吗?
- 如果想在处于后台时每次更新 value 都触发更新怎么做?
- LiveData 如何与多个 ViewModel 共享数据?
- LiveData 数据更新通知 UI 的核心链路?
- LiveData 生命周期感知原理?
- LiveData 的版本号是干啥的?为什么需要版本号?
- LiveData 发相同数据会通知观察者吗?
- LiveData 的 setValue 和 postValue 区别?
- 子线程更新 LiveData 数据涉及过吗?
- ViewModel 如何应对屏幕旋转?
- ViewModel 的持久化是怎么实现的?
- ViewModel 为什么比 Activity 生命周期长?Activity 销毁后 ViewModel 怎么保证不销毁?
- 正常的退出和销毁重建是怎么区分的?
- ViewModel 中的 safe state handle 是干嘛的?
- 协程+ViewModel 实现?
- Litepal 与其他 ORM 对比?
- RxJava(网络请求后展示到 UI 十个请求全部完成打印输出线程切换)?
- 总线类的框架用过吗?
十二、JVM 与内存
问:JVM 内存结构(堆栈方法区/元空间程序计数器本地方法栈)?
JVM 内存分为五大区域:堆(Heap)存储对象实例;栈(Stack)每个线程私有,存储栈帧(局部变量表、操作数栈、动态链接、方法出口);方法区/元空间存储类信息、常量、静态变量;程序计数器记录当前执行字节码行号;本地方法栈为 Native 方法服务。
问:JDK1.7 和 1.8 的内存模型变化?
JDK1.8 移除了永久代(PermGen),用元空间(Metaspace)替代,元空间使用本地内存而非 JVM 堆内存,默认无上限(可受系统限制),减少了 OOM 风险,类卸载更灵活。
问:Java 内存区域?
JVM 规范将内存分为:线程私有(程序计数器、JVM 栈、Native 方法栈);线程共享(堆、方法区/元空间)。堆是 GC 主战场,栈帧存储方法调用上下文。
问:Java 并发模型?
Java 内存模型(JMM)定义了线程如何与主内存和工作内存交互,保证原子性、可见性、有序性。volatile 保证可见性不保证原子性,synchronized 保证两者。
问:哪些区域线程共享哪些是线程私有的?
线程共享:堆、元空间(方法区)。线程私有:程序计数器、JVM 栈、Native 方法栈。
问:堆存放什么?
堆存放所有对象实例和数组,是 GC 主要区域,几乎所有类实例都在堆上分配(逃逸分析优化后可能在栈上)。
问:堆和栈分别用来存什么?
堆:对象实例、数组。栈:栈帧(局部变量表、操作数栈、动态链接、返回地址),局部变量表存储基本类型和对象引用。
问:栈帧结构?
栈帧包含:局部变量表(参数 + 局部变量)、操作数栈(方法执行计算)、动态链接(运行时常量池引用)、方法返回地址(正常/异常)。
问:方法调用时栈空间变化?
方法调用时创建新栈帧压入当前线程栈顶,方法返回时弹出栈帧并恢复上层方法栈帧,局部变量随栈帧销毁。
问:方法返回值保存在哪里(操作数栈)?
方法返回值通过操作数栈传递,调用方从操作数栈获取,然后存入自己栈帧的局部变量槽。
问:程序地址空间?
进程地址空间包括:代码段、数据段(全局/静态变量)、堆(动态分配)、栈(函数调用)、内核区。32 位系统理论 4GB(用户 3GB+ 内核 1GB)。
问:堆区和栈区作用?
堆区:对象动态分配,GC 管理,生命周期灵活。栈区:方法调用链管理,线程私有,自动随方法结束清理。
问:物理内存和虚拟内存区别?
物理内存:实际 RAM 硬件。虚拟内存:操作系统抽象,进程看到连续地址空间,可映射到物理内存或磁盘 Swap,支持内存保护与隔离。
问:32 位 CPU 架构对进程的虚拟内存是多大?
32 位地址空间理论 4GB,Linux 通常用户空间 3GB+ 内核 1GB,Windows 类似。单个进程可用虚拟内存约 2-3GB。
问:内存管理?
JVM 内存管理主要是堆内存自动 GC,栈内存随线程生命周期自动分配释放。监控工具:jstat、jmap、GC 日志、VisualVM。
问:垃圾回收算法(标记清除标记复制标记整理分代收集)?
- 标记清除:标记存活对象后清除垃圾,产生碎片
- 标记复制:复制存活对象到另一区,无碎片但空间利用率低,适合新生代
- 标记整理:存活对象向一边整理,消除碎片,适合老年代
- 分代收集:新生代 Eden+Survivor,老年代,不同区域用不同算法
问:垃圾回收器分类并行收集器原理?
收集器分类:串行、并行(多线程 GC)、CMS(标记清除低延迟)、G1(分区收集器,按价值排序 Region,可预测停顿时间)。
问:CMS/G1?
CMS(Concurrent Mark Sweep):低延迟并发收集器,标记清除算法,有碎片。G1(Garbage First):分区收集器,按价值排序 Region,可预测停顿时间,兼顾吞吐和延迟。
问:分代回收(新生代/老年代)思想为什么要这么划分?
基于弱分代假说:绝大多数对象朝生夕死。新生代:老年代约 1:3,Eden:Survivor=8:1:1,短命对象快速回收减少 GC 成本,长期存活对象晋升老年代减少复制次数。
问:GC 什么时候发生?
- Minor GC(新生代):Eden 空间不足时
- Major GC(老年代):大对象直接进入、长期存活对象晋升、老年代空间不足
- Full GC:堆/方法区不足、System.gc() 建议、CMS/Ascolded 失败
问:GC Root 有哪些?
- 栈帧局部变量表引用
- 静态变量引用
- JNI 引用
- 活跃线程
- JVM 内部引用(Class 对象、异常对象等)
问:安卓中哪些可以作为 GC Root?
- 栈帧中的活跃线程
- 静态变量
- JNI 全局引用
- Binder 对象
- Handler/Message 中的引用
- 单例对象
- 静态内部类持有外部类引用
问:如何判断一个对象是垃圾对象?
通过可达性分析:从 GCRoot 出发,无法到达的对象即为垃圾。引用计数法无法解决循环引用问题,Java 采用可达性分析。
问:可达性分析两个变量互相引用会不会被 GC?
会。循环引用不影响 GC,只要这组对象无法从 GCRoot 到达,整组都被回收。
问:如何确保所有对象被穷举(安全点安全区域)?
- 安全点:线程执行到特定位置(方法调用、循环回跳)才响应 GC,避免枚举时对象引用变化。
- 安全区域:一段代码执行期间引用关系不变(如睡眠线程),允许 GC 在此期间进行。
问:引用计数和可达性分析区别 Java 使用哪种?
- 引用计数:对象引用数为 0 可回收,无法解决循环引用。
- 可达性分析:从 GCRoot 遍历,可解决循环引用。Java 使用可达性分析。
问:强/软/弱/虚引用区别与使用场景?
- 强引用:默认引用,niemals 被 GC
- 软引用:内存不足时回收,适合缓存
- 弱引用:下次 GC 必回收,适合监听器避免内存泄漏
- 虚引用:无法获取对象,追踪对象被回收时间,配合 ReferenceQueue
问:Java 引用类型和 C++ 引用有什么区别?
Java 引用是对象指针但支持四种类型(强软弱虚),C++ 引用是别名(实际相当于 const 指针),不能重新绑定,无空引用。
问:Java 引用类型在实际开发中有哪些应用场景?
- 软引用:图片缓存,内存不足时回收
- 弱引用:监听器/回调,View 持有 Context 时用 WeakReference 避免泄漏
- 虚引用:配合 ReferenceQueue 监听对象回收
问:图片缓存用什么引用?
LruCache 用强引用但限制大小,或软引用缓存大图,内存紧张时自动回收。
问:弱引用如何恢复?
弱引用对象被回收后无法恢复。可通过弱引用 + 缓存策略,回收后重新加载。即 WeakReference.get() 返回 null 后需重新创建。
问:内存泄漏定义常见场景?
内存泄漏:无用的对象因被引用而无法回收,导致内存持续增长。常见场景:单例持 Activity 引用、静态集合存储对象、未注销监听器、Handler/线程未回收、Cursor 未关闭。
问:安卓常见的内存泄漏场景?
- 单例持 Activity/Context
- 静态内部类/匿名内部类持有外部类
- Handler 消息队列延迟消息
- 线程/AsyncTask 后台任务持外部引用
- 未注销 BroadcastReceiver/EventBus
- WebView 持 Activity référence
- Cursor/Bitmap 未释放
问:从内存泄漏角度静态内部类和匿名内部类有什么区别?
- 非静态内部类/匿名内部类:隐式持有外部类引用,外部类无法被回收
- 静态内部类:不持有外部类引用,避免泄漏
问:匿名内部类声明为静态的可以持有外部类吗为什么?
匿名内部类不能声明为 static。要持外部类需显式用 WeakReference,否则默认强引用外部类导致泄漏。
问:内存泄漏怎么排查?
- Android Profiler 内存快照
- LeakCanary 自动检测
- MAT(Memory Analyzer Tool)分析 dump 文件
- 观察内存曲线持续增长
问:native 内存泄漏第三方 SDK 泄漏没代码怎么办?
- 使用 Native Memory Debug 工具(如 AddressSanitizer)
- 查看 fd 和 native heap 增长
- 第三方 SDK 泄漏:联系厂商、换替代方案、限制资源使用频率
问:内存泄漏工具怎么使用?
LeakCanary:引入依赖自动初始化,检测后弹出通知显示泄漏链。MAT:导入 hprof 文件,Dominator Tree 找大对象,Path To GC Roots 定位引用链。
问:如何定位 Android 中的内存泄漏具体操作流程?
- 复现场景,使用 Profiler 监控内存
- 触发 GC,观察内存是否下降
- Dump heap 文件用 MAT 分析
- 查找直方图 Top 类实例异常增长
- 查看 GC Roots 追踪引用链
问:Android Studio 内置的 Android Profiler 如何操作?
- 运行 App 连接 Profiler
- Memory 面板观察内存曲线
- 点击 Dump Java Heap 生成快照
- 分析 Allocation Tracker 查看对象分配栈
问:定位内存泄漏打印快照时机如何选择?
- 操作前快照(基线)
- 执行可疑操作
- 触发 GC 后再次快照
- 对比两 snapshot 找出新增未回收对象
问:内存占用的分析中的火焰图怎么看?
火焰图横向表示 CPU 时间/内存占用,纵向表示调用栈,矩形宽度表示耗时/占用比例,从上往下追踪调用链找到热点方法。
问:LeakCanary 原理检测机制?
- 监听 Activity/Fragment 生命周期
- 销毁后用弱引用引用,GC 后检查弱引用队列
- 未回收则分析引用链找到泄漏路径
- 生成报告
问:LeakCanary 如何实现应用启动后自动初始化?
通过 ContentProvider 生命周期(onCreate 在 Application 之前)或 AppInit 框架,注册 ActivityLifecycleCallbacks 监听组件生命周期。
问:LeakCanary 检测出来什么类型的内存泄漏?
Activity Leak、Fragment Leak、ViewModel Leak、静态变量泄漏、线程/Handler 泄漏、监听器未注销、单例持 Context。
问:真正检测的对象是哪个?
检测被销毁的 Activity/Fragment 是否被回收,若未被回收则存在泄漏。
问:怎么判断是哪一种情况导致的内存泄漏?
分析 LeakCanary 的引用链,查看是哪个静态变量/单例/线程持有对象,对应代码位置定位原因。
问:从检测到内存泄漏到弹出提示引导修复链路如何实现?
检测到泄漏 → 生成泄漏摘要 → 后台线程分析引用链 → 写入文件 → 通知栏弹窗 → 点击查看详情和修复建议。
问:什么时候触发内存泄漏检测?
组件(Activity/Fragment)onDestroy 后,触发 GC,检查弱引用是否被回收。
问:LeakCanary 监控什么安卓组件?
默认监控 Activity、Fragment、ViewModel,可自定义扩展 Service/Dialog。
问:监控 ViewModel 怎么注册的?
通过 ViewModelStore 的 clear() 回调,结合弱引用检测。
问:场景 Activity 创建 Handler 并 post 消息 Activity 销毁但消息仍在队列中 LeakCanary 能检测到吗?
能。Handler 消息队列持有 Activity 引用,Activity 销毁后消息未处理则泄漏,LeakCanary 可检测到。
问:已发送的 message 无法被 remove 怎么处理?
使用静态 Handler+WeakReference 持 Activity,或 onDestroy 时 removeCallbacksAndMessages(null),或用 Handler 的 WeakReference。
问:LeakCanary 告诉内存泄漏开发者如何验证排查是否真实泄漏?
根据引用链找到泄漏点,检查是否有静态变量持有、监听器未注销、线程未停止等。
问:如何避免内存泄漏开发过程中?
- 避免非静态内部类持外部类
- 使用 WeakReference 持 Context
- Handler 消息及时 remove
- 生命周期绑定取消任务
- 使用 LifecycleAwareComponent
问:内存峰值降低 18% 有没有分析是哪类对象的优化?
分析前后 heap dump 对比,查看减少的对象类型:可能是 Bitmap 复用、缓存策略优化、大对象延迟加载等。
问:类加载机制(加载→验证→准备→解析→初始化)?
- 加载:通过全限定名获取二进制流,转方法区结构,生成 Class 对象
- 验证:确保字节码符合 JVM 规范
- 准备:分配内存设默认值
- 解析:常量池符号引用转直接引用
- 初始化:执行 static 块和静态变量赋值
问:ClassLoader 的整体架构和理解?
JVM 内置 Bootstrap(核心类库)→ Extension(扩展类库)→ Application(应用类路径)。自定义 ClassLoader 加载非标准路径类。Android 使用 PathClassLoader/DexClassLoader。
问:ClassLoader 在安卓里的应用场景?
- PathClassLoader:加载 APK 内 dex
- DexClassLoader:加载外部 jar/apk(插件化/热修复)
- 多 dex 支持
- 双亲委派打破实现热修复
问:双亲委派机制(是组合不是继承)好处?
ClassLoader 组合父加载器不是继承。好处:避免重复加载、保证核心类安全(如 String 不会被篡改)、类隔离性。
问:如何打破双亲委派机制?
自定义 ClassLoader 重写 loadClass(),不调用父加载器,优先自己加载。
问:利用 ClassLoader 怎么实现热修复原理是什么?
替换类加载顺序,让自定义 ClassLoad 优先加载补丁类。Android 热修复:DEX 补丁插入 ClassLoader 的 dexElements 数组最前面。
问:热修复方案?
- Tinker(DEX 替换)
- Andfix(Native ART 方法替换)
- Instant Run(增量编译)
- 插件化架构(VirtualApp)
问:如何自定义类加载器生产环境用途?
- 热修复
- 插件化
- 多版本类隔离
- 加密类加载
问:如果自定义一个 String 类会怎么加载能编译成功吗?
不能成功。JVM 启动时 Bootstrap ClassLoader 已加载 java.lang.String,自定义类会被父加载器拦截,抛 ClassCastException。
问:PathClassLoader 与 DexClassLoader 区别?
- PathClassLoader:只能加载已安装的 APK dex
- DexClassLoader:可加载外部 dex/jar/apk,支持热修复插件化
问:假如有个 jar 包不能被 maven 下载到本地 library 应该怎么办?
- 手动下载 jar 放入 libs 目录,build.gradle 添加 implementation files('libs/xxx.jar')
- 搭建私有仓库 Nexus/Artifactory
- 本地 mvn install 安装到本地仓库
问:new String() 创建几个对象?
- 常量池中有"xxx"则:1 个(堆中新对象)
- 常量池中无"xxx"则:2 个(常量池 1 个 + 堆中 1 个)
问:JVM 怎么保证一个类只加载一次?
类加载器 + Class 对象缓存,ClassLoader 的 findLoadedClass() 先检查是否已加载,相同 ClassLoader 下保证单例。
问:类加载时对静态成员变量和非静态成员变量处理有什么不同?
静态变量在準備阶段分配内存设默认值,初始化阶段赋值;非静态变量在对象实例化时分配。
问:一个 static 变量更新值另一个进程能获取最新值吗另一个线程呢?
- 另一进程:不能,进程隔离,各自内存空间
- 另一线程:可以(volatile 保证可见性)
问:静态变量存放在哪里?局部变量存放在哪里?
- 静态变量:方法区/元空间
- 局部变量:栈帧的局部变量表
问:启动一个 Activity 设置 TextView 文字标题什么存在堆中什么存在栈中?
- 堆:Activity 对象实例、TextView 对象、String 对象
- 栈:Activity 引用、方法参数、局部变量
问:JMM 的意义?
Java 内存模型解决多线程环境下共享变量可见性、原子性、有序性问题,规范了主内存与工作内存交互协议。
问:为什么 Java 会有线程不安全(并发)底层原因(OS)?
- CPU 缓存不一致:多核 CPU 各自缓存,写操作未及时刷新主内存
- 指令重排序:编译器/CPU 优化导致执行顺序改变
- 交叉执行:多线程交替执行临界区代码
十三、Kotlin
问:Kotlin 与 Java 区别?
Kotlin 空安全、数据类、扩展函数、协程、函数式编程支持;Java 冗长但生态成熟。Kotlin 编译为字节码可互操作。
问:Kotlin 相比 Java 有哪些独有的好用优势?
- 空安全(可空/非空类型)
- 数据类自动生成 equals/hashCode/toString
- 扩展函数无需继承或工具类
- 协程简化异步
- 类型推断减少冗余
- 密封类 exhaustive when
问:Kotlin 语法特性?
- val/var
- 字符串模板
- 智能类型转换
- 默认参数和命名参数
- 扩展函数/属性
- 内联函数
- 协程
- 委托属性
问:Kotlin 怎么保证空安全?
- 类型系统区分可空(String?)和非空(String)
- 安全调用运算符 ?.
- Elvis 运算符 ?:
- !! 断言(可能抛 NPE)
- 编译期检查减少运行时异常
问:Kotlin 空异常检查是编译时还是运行时?
编译时检查大部分可空引用,但 !! 和 Java 互操作时仍有运行时 NPE 风险。
问:看过 Kotlin 的书吗?
《Kotlin 实战》《Kotlin 编程实战》《Kotlin 开发者指南》。
问:Kotlin 中===是什么作用?
===是引用相等判断(同 Java 的),比较是否是同一对象实例。是相等性比较(调用 equals())。
问:java 调用 kotlin 可能出现什么问题?
- 空安全失效:Java 可传 null 给 Kotlin 非空参数,运行时抛 IllegalArgumentException
- @JvmDefault 默认参数不生效
- Kotlin 内联函数 Java 不可用
- 顶层函数转为静态方法
问:by lazy 原理会不会有多线程问题?
- lazy 默认 SYNCHRONIZED 模式:加锁保证线程安全
- PUBLICATION:多线程同时初始化返回同一实例,但 initializer 会多次执行
- NONE:无锁,适合单线程
问:by lazy 和 lateinit 的区别?
- by lazy:用于 val,线程安全,延迟初始化,有开销
- lateinit:用于 var,不线程安全,手动保证初始化,无额外开销
- val 必须用 lazy,var 优先 lateinit
问:内联函数原理和优点?
编译时将函数体复制到调用处,消除 lambda 对象创建和虚方法调用开销。适合高阶函数,但不适合大函数避免代码膨胀。
问:密封类(sealed class)是什么优势是什么?
- 限制继承层次,所有子类在同一文件定义
- when 表达式可 exhaustive 无需 else
- 编译器检查完整性
- 适合状态机/代数数据类型
问:高阶函数是什么?
函数作为参数或返回值,接受 lambda 的函数。例如 filter、map.forEach.
问:扩展函数怎么实现的(编译时转化为静态函数)?
编译为静态方法,第一个参数是接收者对象。调用时语法糖隐式传递,无运行时开销。
问:协程与线程区别?协程为什么轻量?节省的内存在哪里?
- 线程:OS 调度,1-2MB 栈空间,上下文切换成本高
- 协程:用户态调度,~KB 级栈空间,挂起不阻塞线程
- 节省内存:协程可数万并发,线程几千就吃紧
问:协程是什么有什么用?
协程是轻量级线程,用于异步编程。用同步方式写异步代码,避免回调地狱。
问:协程挂起是怎么实现的本质是什么(异步回调)?
挂起函数编译为状态机,挂起时保存状态返回,回调续时时恢复状态继续执行。
问:协程调度实现为什么不需独立线程?
协程在 Dispatchers 线程池执行,挂起时释放线程给其他协程,回调时再获取线程继续,复用线程资源。
问:平时是怎么用协程的?
- launch 启动不返回结果
- async 获取结果 await()
- 用 ViewModelScope/LifecycleScope 生命周期绑定
- withContext 切换线程
- SupervisorJob 子协程失败不影响兄弟
问:启动协程的几种方式?
- launch:不阻塞,返回 Job
- async:阻塞式获取结果,返回 Deferred
- runBlocking:阻塞当前线程等待完成(测试用)
- coroutineScope:结构化并发
问:协程怎么取消?
- cancel() 取消 Job
- withTimeOut() 超时自动取消
- 检查 isActive 或 ensureActive()
- 避免阻塞调用用 suspendCancellableCoroutine
问:协程里 Job 存在的意义是什么?
Job 代表协程任务,可取消、可等待完成、可结构化组织父子关系。
问:在项目里用协程做什么了?
- 网络请求(Retrofit+ 协程)
- 数据库操作(Room 支持挂起)
- 边界生命周期感知(LifecycleScope)
- 并行请求 awaitAll()
问:协程内联函数?
suspend 函数本身不能内联,但 runBlocking 等可内联减少开销。
问:DSL 理解(高阶函数 + 带接收者的 Lambda)?
DSL:领域特定语言。Kotlin 用 lambda with receiver 实现。例如:apply{this.}、build 模式、HTML DSL。
问:let also apply run 区别?
- let:it 引用对象,返回 lambda 结果
- also:it 引用对象,返回原对象
- apply:this 引用对象,返回原对象
- run:this 引用对象,返回 lambda 结果
- takeIf/takeUnless:条件过滤
问:Kotlin 非空参数在 Java 调用时是否安全(不安全抛 IllegalArgumentException)?
不安全。Java 可传 null 给 Kotlin 非空参数,运行时抛 IllegalArgumentException(@NotNull 校验)。
问:KAPT 与 KSP 区别?
- KAPT:注解处理,生成 Java stub 再处理,速度慢
- KSP:Kotlin Symbol Processing,原生支持 Kotlin 语法,速度快 2 倍+
问:Kotlin 使用过哪些单例?
- object 关键字:编译期静态
- enum 单例
- lazy+volatile 双重检查
问:lazy?
lazy 是委托属性,延迟初始化 val。默认线程安全 SYNCHRONIZED 模式。
八、网络编程
问:TCP 与 UDP 区别?安卓开发中的使用场景?
- TCP:面向连接、可靠、有重传、有序、慢;HTTP/HTTPS、即时通讯
- UDP:无连接、不可靠、快速、低延迟;直播、实时游戏、语音、DNS
问:TCP 三次握手(为什么是三次?两次不行吗)?
- Client→Server:SYN
- Server→Client:SYN+ACK
- Client→Server:ACK
两次不行:防止已失效连接请求突然传到服务端产生错误连接。
问:TCP 第一次握手失败后客户端的重试策略?
指数退避重传 SYN,默认 1s、2s、4s、8s...重传多次后放弃。
问:TCP 第一个带数据的包是第几个包(第三个)?
第三次握手开始可携带数据。
问:TCP 四次挥手(为什么是四次)?
- Client→Server:FIN
- Server→Client:ACK
- Server→Client:FIN
- Client→Server:ACK
四次原因:TCP 全双工,每方向需单独关闭。
问:三次握手服务端返回的 ACK 报文丢失客户端和服务端分别会发生什么?
客户端未收到 ACK 重传 SYN,服务端收到重复 SYN 会重发 SYN+ACK。
问:重传的 SYN 报文的序列号和之前一样吗(不一样)?
一样。
问:TCP 滑动窗口机制窗口大小动态变化(接收方缓冲区)两端窗口大小一样吗?
不一样。接收方通告窗口大小,发送方根据自身拥塞窗口和接收方窗口取最小值发送。
问:TCP 流量控制机制?
接收方通告窗口大小,发送方不超窗口发送,防止接收缓冲区溢出。
问:TCP 拥塞控制(慢启动拥塞避免快重传快恢复)?
- 慢启动:cwnd 指数增长
- 拥塞避免:cwnd 线性增长
- 快重传:收到 3 个重复 ACK 立即重传
- 快恢复:cwnd 减半后线性增长
问:TCP 超时重传算法超时时间设置?
RTO(Retransmission TimeOut)基于 RTT 加权平均:RTO = 平滑 RTT + 4×平滑偏差。初始 1s。
问:TCP 首部 20 个字节包含哪些内容?
源/目的端口(4B)、序号/确认号(8B)、数据偏移 + 标志位(2B)、窗口大小(2B)、校验和 + 紧急指针(4B)。
问:TCP 如何保证可靠传输?
确认应答 + 超时重传、序号保证顺序、流量控制 + 拥塞控制、校验和保证数据完整性。
问:服务端用 80 端口接受多个连接如何区分不同连接(四元组)?
四元组:源 IP、源端口、目的 IP、目的端口。服务器不同客户端形成不同连接。
问:什么时候用 UDP?游戏为什么适合 UDP?网络差时 UDP 优势还能发挥吗?
- 实时性要求高场景:语音、直播、游戏
- 游戏适合 UDP:低延迟,丢包比卡顿好
- 网络差时 UDP 仍低延迟,但丢包率更高,需应用层重传
问:长连接短连接区别?怎么保持长连接?
- 短连接:每次请求新建连接
- 长连接:复用连接
- 保持:心跳包(PING/PONG)、Connection:keep-alive
问:HTTP 报文结构?请求头包含内容?请求首部?
- 请求行:方法+URL+ 版本
- 请求头:Host、Content-Type、Content-Length、Accept、User-Agent、Cookie 等
- 空行
- 请求体
问:HTTP1.0/1.1/2.0 区别?报文头部有什么区别?
- HTTP1.0:短连接
- HTTP1.1:默认长连接、Host 头、分块传输
- HTTP2.0:多路复用、头部压缩、服务器推送
问:HTTP2.0 做了什么优化?
- 多路复用(单连接多请求)
- 头部 HPACK 压缩
- 服务器推送
- 二进制分帧层
问:HTTP2.0 最大改进?
多路复用解决队头阻塞,并行请求无需多连接。
问:HTTP2.0 基于什么协议?HTTP3.0 基于 UDP 的好处?
HTTP2.0 基于 TCP。HTTP3.0 基于 QUIC(UDP):消除 TCP 队头阻塞,0-RTT 握手。
问:HTTP 中的同步和异步?
- 同步:请求阻塞等待响应
- 异步:回调/事件驱动,不阻塞 UI
问:HTTP 是什么协议?
超文本传输协议,应用层协议,基于 TCP,请求 - 响应模式。
问:应用里用了什么协议(GET POST PUT)?
GET:查询;POST:创建;PUT:更新;DELETE:删除。
问:GET 和 POST 区别?实际使用中参数有何不同?
- GET:参数在 URL,可见,长度受限,幂等,缓存友好
- POST:参数在 body,隐藏,不限长度,不幂等
- 实际:GET 查询,POST 提交敏感数据
问:GET 查询数据怎么携带?POST 怎么携带?
GET:URL 问号后 key=value&key2=value2。POST:body 中(application/x-www-form-urlencoded、JSON、multipart).
问:POST 的 body 不可见解释为什么?
body 不在 URL 显示,但仍是明文传输。HTTPS 加密后不可见。
问:四种 HTTP 请求区别和使用场景?
- GET:获取资源(幂等、可缓存)
- POST:创建资源(不幂等)
- PUT:更新资源(幂等)
- DELETE:删除资源(幂等)
问:HTTPS 与 HTTP 区别?
HTTPS=HTTP+SSL/TLS,加密传输,端口 443,需要证书。
问:HTTPS 为什么更安全?
加密(对称 + 非对称)、身份认证(证书校验)、完整性保护(HMAC)。
问:HTTPS 如何建立连接?
- ClientHello(加密套件、随机数)
- ServerHello+ 证书
- 客户端验证证书
- 生成 session key(非对称加密传输)
- 切换到对称加密通信
问:HTTPS 原理加密算法和整体流程?
- 非对称加密(RSA/ECC)交换密钥
- 对称加密(AES)传输数据
- 证书验证服务器身份
问:对称加密和非对称加密 HTTPS 什么时候用对称什么时候用非对称?
- 握手阶段:非对称加密交换密钥
- 数据传输:对称加密(性能高)
问:对称加密和非对称加密加密解密流程?
- 对称:同一密钥加解密,快
- 非对称:公钥加密私钥解密,慢
问:常见对称加密和非对称加密算法?
- 对称:AES、DES、3DES、RC4
- 非对称:RSA、ECC、DSA
问:CA 证书加解密过程?
CA 用私钥对服务器公钥签名。客户端用 CA 公钥验证签名获取服务器公钥。
问:证书在 HTTPS 中起的作用?
证明服务器身份,防止中间人攻击。由 CA 签名验证可信。
问:如何校验证书有效性(如访问百度时)?
- 证书链验证:根 CA→中间 CA→服务器证书
- 检查吊销列表 CRL 或 OCSP
- 域名匹配 CommonName/SAN
- 有效期检查
问:HTTPS 能否完全防御中间人攻击?
证书正确配置时能防御。但用户忽略证书警告或 CA 被攻破时仍可能。
问:抓包了解吗?
Wireshark/Charles/Fiddler 抓包分析 HTTP/HTTPS 流量,HTTPS 需安装代理证书。
问:WebSocket 协议的理解?
全双工通信协议,握手用 HTTP,之后独立长连接,适合实时推送(聊天、股票)。
问:WebSocket 原理与 Socket 区别?好处?
- Socket:TCP/IP 封装,需自己处理粘包
- WebSocket:应用层协议,握手后长连接,支持文本/二进制帧
- 好处:浏览器原生支持、自动心跳、低延迟
问:表示层和会话层功能?
- 表示层(L6):数据格式转换、加密/解密、压缩
- 会话层(L5):建立/管理/终止会话
问:IP 层负责什么?TCP 负责什么?
- IP 层:路由、寻址、分片
- TCP:可靠传输、流量控制、拥塞控制
问:浏览器输入 URL 到页面渲染的完整过程?
- DNS 解析
- TCP 三次握手
- HTTP 请求
- 服务器响应
- 浏览器解析 HTML/CSS/JS
- 构建 DOM/CSSOM、渲染树
- Layout、Paint、Composite
- TCP 四次挥手
问:网络请求用哪个第三方库?看过源码吗?
OkHttp+Retrofit。看过 OkHttp 拦截器链、连接池、Retrofit 动态代理。
问:网络错误情况怎么处理?
- 超时重试(指数退避)
- 错误提示用户
- 降级策略(缓存/默认值)
- 日志上报监控
问:打开浏览器白屏可能是什么情况(不是 403 404)?
- DNS 解析失败
- TCP 连接超时
- JS 加载失败阻塞渲染
- SSL 证书问题
- 跨域资源阻塞
- 前端资源加载超时
问:网络请求如何完成(结合项目)?
Retrofit 定义接口→OkHttp 执行→协程挂起等待→结果处理 UI 更新→异常捕获提示。
问:通信协议了解哪些(HTTP HTTPS MQTT)?
- HTTP/HTTPS:Web 请求
- MQTT:物联网轻量发布订阅
- TCP/UDP:传输层
- WebSocket:实时双向
问:HTTP 属于 TCP/IP 协议哪一层?底层用哪种传输协议?
应用层。底层用 TCP。
问:TCP 属于哪一层?
传输层(L4)。
问:五层网络模型?
应用层、传输层、网络层、数据链路层、物理层。
问:网络七层结构?
应用层、表示层、会话层、传输层、网络层、数据链路层、物理层(OSI 模型)。
问:传输层和网络层能合并吗?
不能。网络层负责寻址路由,传输层负责端到端可靠传输,职责不同。
问:Socket 位于哪一层?
应用层(收发数据接口),使用传输层的 TCP/UDP。
问:网络如何确保安全?如何确保秘钥正确性?
- HTTPS/TLS 加密
- 证书验证身份
- 数字签名防篡改
- 密钥交换(Diffie-Hellman)
问:IPv4 和 IPv6 区别?
- IPv4:32 位,40 亿地址
- IPv6:128 位,地址充足,内置 IPSec,简化首部
问:HTTP 实体压缩?有针对头部的压缩吗?
- 实体:gzip、deflate、br(Brotli)
- 头部:HTTP2.0 HPACK 压缩
问:差量更新/下载断点续传?
- 差量更新:bsdiff 算法,只下载差异部分
- 断点续传:Range 头指定字节范围,服务端支持 206 Partial Content
十四、网络框架与第三方库
问:OkHttp 源码拦截器链(责任链模式)?
拦截器:RetryAndFollowUpInterceptor→BridgeInterceptor→CacheInterceptor→ConnectInterceptor→CallServerInterceptor。每个拦截器处理特定职责,责任链依次执行。
问:责任链好处是什么?
解耦、职责单一、灵活组合、便于扩展测试。
问:OkHttp DNS 注入处理?
实现 Dns 接口,自定义 DNS 解析(如 HTTPDNS),通过 builder.dns() 注入。
问:OkHttp 设计模式有哪些?
- 责任链(拦截器)
- 建造者(Request/Client)
- 适配器(Converter)
- 单例(ConnectionPool)
- 工厂(RouteSelector)
问:OkHttp 底层原理(连接复用?缓存)?缓存用哪种数据结构存储?
- 连接池:RealConnection,空闲连接复用,5 分钟保活
- 缓存:DiskLruCache(LRU 算法),key 为请求 URL+Method
问:OkHttp 请求有同步和异步两种有什么区别?
- execute():同步阻塞
- enqueue():异步回调,内部线程池执行
问:Retrofit 底层动态代理原理?为什么接口不用实现?
动态代理生成接口实现类,调用时拦截处理注解解析,构建 Request 通过 OkHttp 执行。
问:Retrofit 的核心原理?
注解解析 + 动态代理 + 适配器/转换器工厂,将接口方法转为 HTTP 请求。
问:Retrofit 与 OkHttp 区别?
- OkHttp:HTTP 客户端,执行请求
- Retrofit:HTTP 客户端封装,简化 API 定义(基于 OkHttp)
问:Retrofit 用到了哪些设计模式(动态代理工厂适配器建造者)?
- 动态代理:接口转发
- 工厂模式:CallAdapter.Factory、ConverterFactory
- 适配器:适配 RxJava/Coroutines
- 建造者:Retrofit.Builder
问:Retrofit 如何通过注解实现网络请求?
@GET/@POST 解析为 RequestBuilder,@Path/@Query 填充 URL/ 参数,动态代理执行。
问:两个接口会生成不同的实例对象吗?
会。retrofit.create() 每次返回新代理实例,但共享 OkHttpclient.
问:动态代理优势?
无需手动实现接口,运行时生成,统一处理逻辑(日志/重试)。
问:Retrofit 发送请求如何记录日志(拦截器)?
添加 HttpLoggingInterceptor,设置 Level(NONE/BASIC/HEADERS/BODY)。
问:Glide 源码整体运作流程?
- with(context) 获取 DrawableTypeRequest
- load(url) 创建 RequestBuilder
- into(imageView) 构建 Target
- Engine 缓存查询
- 加载资源(网络/磁盘/内存)
- 解码 Bitmap 设置 ImageView
问:Glide 工作原理?
- Lifecycle 绑定:Activity/Fragment 绑定请求生命周期
- 三级缓存:激活资源→弱引用→内存缓存→磁盘缓存→网络
- 线程池:异步加载
问:Glide 比其他的优势?
- 自动 Downsampling 减少内存
- 生命周期感知自动取消
- 高效缓存策略
- 支持 GIF 加载
问:Glide 三级缓存 LRU 算法?
内存缓存(LruCache 强引用)→弱引用池→磁盘缓存(DiskLruCache)→网络。
问:Glide 缓存原理?
- 内存缓存:LruResourceCache
- 磁盘缓存:DiskLruCache(LRU 替换)
- key:URL+ 签名 + 变换
问:Glide 图片缓存 key 生成规则(url 图片签名等)?
key = 安全哈希(URL+ 签名 + 选项 + 变换),保证不同配置缓存分离。
问:在客户端如何实现 Glide 图片缓存?
使用 Glide 默认缓存策略(DiskCacheStrategy.ALL),或自定义 DiskCache、MemoryCache。
问:如何判断下次请求 Glide 是不是需要更新?
- 图片 URL 或签名变化
- ETag/Last-Modified 服务端通知变化
- 强制跳过缓存 skipMemoryCache(true).diskCacheStrategy(NONE)
问:排行榜头像一直在更新如何实现缓存?
- URL 带时间戳/版本号强制更新
- 签名 Signature 变化使缓存 key 变化
- 或跳过缓存
问:Glide 一次请求多少个图片?
根据线程池配置(默认 4 个),可调整。
问:Glide 加载网络资源实际请求方法(HEAD 非 GET)?
GET 请求。HEAD 只获取头部无 body.
问:Glide 如何绑定生命周期?Activity 和 Fragment 生命周期在 Glide 里有什么区别?
- 通过 Fragment 注入(无 UI Fragment)
- Activity/Fragment 均支持,Fragment 生命周期更清晰
- onDestroy 自动清除请求
问:Glide 与其他图片框架优劣比较?
- Glide:Android 友好、GIF 支持、生命周期感知
- Picasso:简单 API、但无 GIF、缓存弱
- Fresco:内存优化(Ashmem)、但重量级
问:Glide 处理大图加载的逻辑?
- Downsampling:按 Target 尺寸解码
- 分块加载(BitmapRegionDecoder)
- 内存缓存前降采样
问:Glide 在项目中用到了什么地方?
头像加载、列表图片、Banner、GIF 动图、圆形/圆角变换。
问:如何解决图片加载的错位问题?
- setTag() 标记当前 URL
- 加载前检查 tag 是否匹配
- 取消旧请求
问:图片加载用过哪些库?Glide 还用过什么其他功能?
- Glide、Picasso、Fresco
- Glide 还用于:缩略图、过渡动画、占位图、错误图
问:EventBus 原理优劣使用场景?
- 原理:观察者模式,@Subscribe 注解,索引加速
- 优势:解耦、简单
- 劣势:隐式耦合、难追踪
- 场景:组件间通信、事件广播
问:EventBus 手写实现(单例 + 观察者模式)?
单例 EventBus,维护 Map<Topic,List>,post 时遍历通知,支持线程模型。
问:LeakCanary 原理检测机制?
弱引用监测销毁对象,GC 后未回收则 dump heap 分析引用链,生成报告。
问:LeakCanary 如何实现应用启动后自动初始化?
ContentProvider 或 AppInit 框架,注册 ActivityLifecycleCallbacks。
问:Room 与 SQLite 区别?Room 原理?
- SQLite:原始 SQL API
- Room:编译期验证 SQL,注解生成 DAO,LiveData/Flow 响应式
- 原理:注解处理器生成实现类
问:Flow 使用场景?
响应式数据流,支持背压,适合数据变化通知(数据库、网络状态)。
问:LiveData 与普通的观察者模式有什么区别?
- 生命周期感知
- 粘性事件生命周期重启自动重发最新值
- 避免内存泄漏
问:处于后台时多次更新 LiveData 的 value 会触发 onChange 吗?
不会。后台观察者不 active 时,只更新 value 不回调。下次前台才收到最新值。
问:如果想在处于后台时每次更新 value 都触发更新怎么做?
用 Event/Channel,自定义 LiveData 不检查 active 状态,或 Flow 代替。
问:LiveData 如何与多个 ViewModel 共享数据?
在 Activity 或父 ViewModel 创建,通过 SharedViewModel 或应用级存储。
问:LiveData 数据更新通知 UI 的核心链路?
setValue/postValue → DataHolder → Active 观察者→onChanged。
问:LiveData 生命周期感知原理?
绑定 LifecycleOwner,通过 LifecycleObserver 监听 STARTED/RESUMED 状态。
问:LiveData 的版本号是干啥的?为什么需要版本号?
区分数据更新,粘性事件确保观察者激活时收到最新版本。
问:LiveData 发相同数据会通知观察者吗?
不会(equals 相同),避免无效刷新。
问:LiveData 的 setValue 和 postValue 区别?
- setValue:主线程直接通知
- postValue:后台线程,异步通知(最终调用 setValue)
问:子线程更新 LiveData 数据涉及过吗?
用 postValue() 或协程 Dispatchers.Main。
问:ViewModel 如何应对屏幕旋转?
ViewModel 生命周期长于 Activity,旋转不销毁,数据保留。
问:ViewModel 的持久化是怎么实现的?
ViewModel 本身不持久化,配合 SavedStateHandle 或 Room 持久化。
问:ViewModel 为什么比 Activity 生命周期长?Activity 销毁后 ViewModel 怎么保证不销毁?
ViewModelStore 持有,配置变化时 Activity 重建但 ViewModelStore 保留(转交给新 Activity)。
问:正常的退出和销毁重建是怎么区分的?
- isChangingConfigurations():true 为配置变化
- false 为真的销毁
问:ViewModel 中的 safe state handle 是干嘛的?
保存恢复 UI 状态,配置变化/进程杀死后恢复数据。
问:协程+ViewModel 实现?
viewModelScope.launch{} 启动协程,生命周期结束时自动取消,避免泄漏。
问:Litepal 与其他 ORM 对比?
LitePal:Android 轻量 ORM,链式 API。对比:Room(Jetpack 官方)、GreenDAO(性能优)。
问:RxJava(网络请求后展示到 UI 十个请求全部完成打印输出线程切换)?
Observable.merge() 或 zip(),subscribeOn(IO) + observeOn(Main),countDownLatch 或 toList() 聚合。
问:总线类的框架用过吗?
用过 EventBus 实现组件解耦通信,或 LiveData 做数据总线。
Android 面试题大全4.0 - 性能优化
本篇所有问题
- APK 瘦身(资源压缩、混淆、WebP、移除无用资源)
- 包大小优化措施,提升了哪些性能
- 无用资源/代码线上判断(类加载机制)
- 代码层面瘦身优化
- 内存优化
- 内存泄漏排查(MAT、LeakCanary)
- OOM 线上收集方案
- 卡顿优化(Systrace、Perfetto、BlockCanary)
- 如何监听 UI 卡顿
- 线程绘制的时长
- 卡顿排查工具(Profiler、Perfetto)
- UI 卡顿原因(掉帧)
- 怎么排查掉帧问题,原因有哪些
- 页面跳转慢、页面崩溃有遇到过吗
- 假如程序同一时间段内触发了很多次重绘方法,安卓系统底层如何防止卡顿问题
- 布局优化(include、merge、ViewStub)
- 性能优化理念(靠数据说话)
- 大图加载(10MB 处理方案),Bitmap 池是什么
- Bitmap 如何优化
- 首页冷启动首屏渲染慢问题排查
- 过渡绘制防止
- App 瘦身有哪些方式
- 项目中的性能优化
- 内存峰值降低是如何实现的,优化了什么
- 热点封面的缓存是如何实现的
- 动态换肤的使用场景和遇到的问题
- MVVM 除了数据视图绑定,还有其他使用场景吗
- LeakCanary 和 AndroidProfile 的具体使用场景和方法
- 软件绘制了解吗
- Vsync、SurfaceFlinger、OpenGL 了解吗
- 启动优化
- 项目上线后有监听内存泄漏的机制吗
APK 瘦身(资源压缩、混淆、WebP、移除无用资源)
答: APK 瘦身主要通过以下方式实现:
- 资源压缩:使用 aapt2 自动压缩 PNG、JPG 等图片资源
- 代码混淆:启用 R8/ProGuard 混淆移除无用代码,缩短类名字段名
- WebP 格式:将 PNG/JPG 转换为 WebP 格式,体积减少 25%-35%
- 移除无用资源:使用 lint 工具检测并移除未引用的资源文件
- so 库优化:只保留需要的 ABI 架构(如 arm64-v8a、armeabi-v7a)
包大小优化措施,提升了哪些性能
答: 包大小优化措施及性能提升:
- 减少 APK 体积:降低用户下载时间和流量消耗,提升下载转化率
- 减少 dex 数量:降低类加载时间,加快冷启动速度
- 减少资源文件:降低内存占用,减少 OOM 风险
- 压缩 so 库:使用 ReLinker 按需加载,减少内存压力
- 性能收益:APK 每减少 1MB,下载转化率提升约 1%-2%
无用资源/代码线上判断(类加载机制)
答: 通过类加载机制判断无用代码:
- Class.forName 引用检测:统计运行时实际加载的类
- 线上埋点:上报每个 Activity/Fragment 的访问频次
- 代码覆盖率工具:使用 Jacoco 统计未被执行的代码分支
- 动态下发检测:将疑似无用代码放到独立 dex,按需加载验证
- 灰度验证:小流量移除某模块,观察崩溃率和业务指标
代码层面瘦身优化
答: 代码层瘦身优化策略:
- 移除 dead code:删除未使用的类、方法、字段
- 内联常量:将频繁使用的常量内联,减少字段引用
- 精简依赖:移除未使用的第三方库,使用更轻量的替代方案
- 动态特性模块:使用 Play Feature Delivery 按需下发功能模块
- Kotlin 优化:启用 kotlin-parcelize 替代 ButterKnife,减少注解处理代码
内存优化
答: 内存优化核心要点:
- 减少对象分配:避免在循环/高频方法中创建临时对象
- 使用基本类型:优先使用 int 而非 Integer,避免自动装箱
- 对象池复用:对频繁创建销毁的对象使用对象池(如 Handler、Message)
- Bitmap 优化:使用 inSampleSize 压缩,及时 recycle 不再使用的 Bitmap
- 集合清理:注意静态集合、监听器、单例持有导致的内存泄漏
内存泄漏排查(MAT、LeakCanary)
答: 内存泄漏排查工具使用:
- LeakCanary:
-
- 集成:debugImplementation 'com.squareup.leakcanary:leakcanary-android'
- 自动检测 Activity/Fragment/View 泄漏
- 分析时打开 leakcanary://leaks 查看泄漏栈
- MAT(Memory Analyzer Tool):
-
- 导出 hprof 文件:adb pull /data/local/tmp/xxx.hprof
- Histogram 分析支配树,查找 GC Root 强引用链
- OQL 查询特定类型的实例分布
- Android Studio Profiler:实时查看内存曲线和对象分配
OOM 线上收集方案
答: OOM 线上收集方案:
-
捕获 UncaughtExceptionHandler:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if (e instanceof OutOfMemoryError) {
上报 OOM 堆栈和设备信息
}
}); -
hprof 文件生成:使用 Debug.dumpHprofData() 在 OOM 前导出内存快照
-
内存监控:通过 ActivityManager.getMemoryInfo() 监控内存水位
-
关键信息上报:上报 OOM 时的内存占用、Bitmap 数量、大图尺寸
-
聚合分析:按机型/系统版本/应用版本维度统计 OOM 热点
卡顿优化(Systrace、Perfetto、BlockCanary)
答: 卡顿优化工具组合:
- Systrace/Perfetto:
-
- 命令行启动:python systrace.py --time=10 -o trace.html
- 分析 CPU 调度、锁竞争、渲染流水线
- 查看 Choreographer 的 doFrame 耗时
- BlockCanary:
-
- 设置主线程阈值:setBlockThreshold(500ms)
- 自动捕获 Block 堆栈和耗时
- 输出日志到本地或上报服务器
- 线上监控:通过 ChoresFrameMetricsListener 统计帧率
如何监听 UI 卡顿
答: UI 卡顿监听方案:
-
Choreographer 监听:
Choreographer.getInstance().postFrameCallback(new Callback() {
@Override
public void doFrame(long time) {
long cost = time - lastFrameTime;
if (cost > 16.6ms) 记录卡顿;
lastFrameTime = time;
Choreographer.getInstance().postFrameCallback(this);
}
}); -
BlockCanary:监控主线程 Looper 消息处理耗时
-
WatchDog 方案:子线程定时检查主线程是否阻塞
-
FrameMetrics:使用 FrameMetricsAggregator 统计帧率分布
线程绘制的时长
答: 线程绘制时长分析:
- GPU 渲染分析:开发者选项开启"GPU Profile",查看每帧渲染柱状图
- draw 耗时组成:
-
- Measure:测量布局尺寸
- Layout:确定子 View 位置
- Draw:生成显示列表(DisplayList)
- Sync:CPU 同步到 GPU
- Issue:GPU 执行渲染指令
- 目标帧率:单帧耗时 ≤16.6ms(60fps),绘制应控制在 8ms 以内
- 硬件加速:开启硬件加速后 Draw 阶段由 GPU 完成,效率更高
卡顿排查工具(Profiler、Perfetto)
答: 卡顿排查工具链:
- Android Studio Profiler:
-
- CPU Profiler:查看方法耗时和调用栈
- Memory Profiler:查看内存分配和 GC 频率
- Energy Profiler:监控能耗异常
- Perfetto:
-
- 系统级追踪,支持更长时间录制
- 可查看 Binder 调用、锁竞争、IO 等待
- 命令:adb shell perfetto -c config.txt -o trace.pftrace
- Simpleperf:
-
- 性能采样工具,定位热点函数
- 命令:simpleperf record -p --duration 10
UI 卡顿原因(掉帧)
答: UI 掉帧主要原因:
- 主线程阻塞:网络请求、数据库读写、复杂计算在主线程执行
- 过度绘制:同一像素点多次绘制,GPU 负载过高
- 布局嵌套过深:Layer 叠加过多,Measure/Layout 耗时增长
- 频繁 GC:短时间内大量对象分配触发 GC STW
- Vsync 错过:渲染耗时超过 16.6ms,错过下一个 Vsync 信号
- 锁竞争:synchronized/ReentrantLock 导致线程阻塞
怎么排查掉帧问题,原因有哪些
答: 掉帧排查步骤:
- 使用 Systrace 抓取:定位耗时超过 16ms 的帧
- 分析 CPU 时间轴:查看主线程、渲染线程的耗时分布
- 检查 RenderThread:是否有长时间 GPU 命令提交
- 查看 GC 事件:GC Pause 是否导致帧丢失
- 分析 Bitmap 加载:大图解码是否在主线程
常见原因:
- 主线程执行 heavy work
- 布局 inflation 在 onDraw 中执行
- RecyclerView onBindViewHolder 耗时过长
- 动画未使用硬件加速
- 频繁 requestLayout/invalidate
页面跳转慢、页面崩溃有遇到过吗
答: 页面跳转慢和崩溃的实战经验:
- 跳转慢排查:
-
- 使用 Intent 携带过大数据(Bundle 序列化耗时)
- 目标页面 onCreate 执行耗时初始化
- 解决方案:懒加载、异步初始化、使用 ARouter 优化路由
- 页面崩溃:
-
- Context 泄漏导致 Activity 引用未释放
- 异步回调未 canceller,页面销毁后回调 NPE
- Fragment 状态恢复时 Bundle 过大导致 TransactionTooLargeException
- 解决:View 销毁时移除回调,使用 ViewModel 跨页面共享数据
假如程序同一时间段内触发了很多次重绘方法,安卓系统底层如何防止卡顿问题
答: 安卓底层防重绘卡顿机制:
- Vsync 信号同步:系统以 16.6ms 间隔发送 Vsync,合并多次 invalidate 请求
- Choreographer 调度:将所有绘制请求放入 FrameHandler,按 Vsync 节奏统一处理
- 脏区域优化:View 的 invalidate(Rect) 只刷新变化区域
- DisplayList 缓存:View 的绘制指令缓存到 DisplayList,避免重复录制
- 硬件层优化:频繁变化的 View 设置为 setLayerType(LAYER_TYPE_HARDWARE),独立合成
布局优化(include、merge、ViewStub)
答: 布局优化技巧:
-
include 复用:提取公共布局(如 Toolbar、底部导航),减少重复代码
-
merge 标签:消除 redundant 容器层级,示例:
-
ViewStub 延迟加载:
-
- 适用:不常用视图(如空状态页、加载失败页)
- 原理:占位 View,inflate 时替换自身
- 代码:viewStub.inflate() 或 viewStub.setVisibility(VISIBLE)
- ConstraintLayout:减少嵌套,扁平化布局
- 占位:用 View 替代冗余的 Layout 容器
性能优化理念(靠数据说话)
答: 性能优化核心理念:
- 先测量后优化:使用 Profiler/Systrace 定位瓶颈,避免主观猜测
- 二八原则:优先优化占耗时 80% 的 20% 关键路径
- 监控先行:建立性能基线和告警机制(启动时长、帧率、内存)
- 灰度验证:优化后 A/B 测试,确保收益可量化
- 持续回归:性能测试加入 CI,防止劣化
- 用户视角:关注用户可感知指标(首屏渲染、交互响应)而非内部指标
大图加载(10MB 处理方案),Bitmap 池是什么
答: 大图加载优化方案:
- 采样压缩:使用 BitmapFactory.Options.inSampleSize 按比例缩小
- 分块加载:使用 BitmapRegionDecoder 按需加载图片区域
- 缩略图策略:先加载缩略图,再异步加载高清原图
- 内存缓存:使用 LruCache 缓存 Bitmap
Bitmap 池:
- 原理:复用已回收的 Bitmap 内存,避免频繁申请释放
- 实现:Glide 的 BitmapPool,按尺寸分类管理
- 收益:减少 GC 次数和内存碎片,提升滚动流畅度
- 使用:Glide/Picasso 内部已实现,自定义需使用 Bitmap.create 复用
Bitmap 如何优化
答: Bitmap 优化实践:
-
格式选择:PNG 用 RGB_565(无透明度)或 ARGB_4444,体积减半
-
尺寸适配:根据 ImageView 尺寸和图片显示区域计算 inSampleSize
-
内存复用:
options.inBitmap = existingBitmap;
options.inMutable = true; -
及时回收:使用 bitmap.recycle() 并置 null(API 19+ 非必须,但建议)
-
缩略图缓存:列表页使用缩略图,详情页再加载原图
-
WebP 格式:使用 WebP 替代 PNG/JPG,体积更小
首页冷启动首屏渲染慢问题排查
答: 冷启动慢排查思路:
- Systrace 分析:查看 Application.onCreate 到第一帧绘制的完整链路
- 分解耗时:
-
- Application 初始化(第三方 SDK 注册)
- 主题加载/布局 inflation
- 数据请求/数据库查询
- 首屏数据绑定
- 优化手段:
-
- 懒初始化非关键 SDK
- 使用 ContentProvider 延迟初始化
- 占位图先行,异步加载数据
- 预加载下一屏数据
- 指标监控:上报 Launch Time(onClick 到首帧绘制)
过渡绘制防止
答: 过渡绘制优化方法:
- 开发者选项:开启"调试 GPU 过度绘制",红色区域表示多次绘制
- 去除背景:Window 默认有背景,子布局如也有背景会造成重叠,移除子布局背景
- clipRect 裁剪:自定义 View 的 onDraw 中使用 canvas.clipRect 限制绘制区域
- 遮挡优化:避免在异形布局(如圆形头像)上绘制多余内容
- 层次结构:减少重叠的半透明绘制,硬件加速后会加重 GPU 负担
- 工具检测:adb shell dumpsys gfxinfo 查看绘制统计
App 瘦身有哪些方式
答: App 瘦身综合策略:
- 资源瘦身:
-
- 图片 WebP 化,移除无用资源
- 使用矢量图 VectorDrawable
- 移除多语言/多 dpi 中不需要的资源
- 代码精简:
-
- 开启 R8 混淆和 shrink
- 动态特性模块按需下发
- so 库优化:
-
- 只打包 arm64-v8a 和 armeabi-v7a
- 使用 ReLinker 动态加载 so
- 依赖管理:
-
- 移除未使用的第三方库
- 使用 implementation 替代 api 减少传递依赖
- 构建优化:
-
- 启用 aapt2 和 BuildCache
- 使用 App Bundle 格式分发
项目中的性能优化
答: 项目性能优化实战案例:
- 列表优化:
-
- RecyclerView 设置 setHasFixedSize(true)
- DiffUtil 替代 notifyDataSetChanged()
- 图片使用 Glide 加载,开启内存/磁盘缓存
- 网络优化:
-
- 接口合并,减少请求次数
- 使用 Retrofit 连接池,避免频繁建连
- 响应数据 Gzip 压缩
- 数据库优化:
-
- Room 替代 SQLiteDatabase
- 使用索引和批量事务
- 分页查询避免一次性加载全部数据
- 启动优化:
-
- 使用 Startup 库管理初始化顺序
- 异步延迟非必要初始化
内存峰值降低是如何实现的,优化了什么
答: 内存峰值降低方案:
- Bitmap 优化是核心(占内存大头):
-
- 使用 inSampleSize 压缩到显示尺寸
- 列表页加载缩略图,详情页才加载原图
- 离开页面立即释放 Bitmap 和 Adapter
- 对象池复用:
-
- Handler/Message 池:Message.obtain() 复用
- 适配器 ViewHolder 复用
- 线程池替代 new Thread
- 数据分页:
-
- 大数据集分页加载,避免全量加载到内存
- 使用 PagedList + LiveData 分页
- 结果:峰值内存从 300MB 降低到 180MB,OOM 率下降 70%
热点封面的缓存是如何实现的
答: 热点封面缓存实现:
- 三级缓存架构:
-
- 内存缓存:LruCache 存储 Bitmap,容量为可用内存 1/8
- 磁盘缓存:ImageDiskCache 存储压缩后图片
- 网络缓存:ETag/Last-Modified 验证
- Key 设计:使用 URL+ 尺寸作为 Key,避免重复加载
- 预加载:WiFi 环境下提前缓存下一页封面
- 缓存淘汰:内存不足时释放 LRU 图片
- 复用机制:相同尺寸图片复用 Bitmap 内存(inBitmap)
- 框架选择:Glide/Fresco 均内置三级缓存,可直接使用
动态换肤的使用场景和遇到的问题
答: 动态换肤应用场景:
- 场景:夜间模式、节日皮肤、个性化主题
- 实现方案:
-
- 多套 resources 包,通过 Resources 替换
- 自定义 View 支持 setSkinColor 属性
- 使用换肤框架(AndroidAutoSize、XSkin)
- 遇到的问题:
-
- Activity 重建后状态丢失:用 SharedPreferences 保存当前皮肤
- 图片资源切换:需同步加载不同 drawable
- 第三方 View 不支持:使用 View 遍历 + 反射更新
- 性能损耗:避免频繁换肤,使用淡入淡出过渡
MVVM 除了数据视图绑定,还有其他使用场景吗
答: MVVM 的其他使用场景:
- 状态管理:
-
- ViewModel 保存页面状态,屏幕旋转不丢失
- 使用 SavedStateHandle 持久化复杂状态
- 跨组件通信:
-
- LiveData 作为总线,实现 Fragment 间通信
- 使用 SingleLiveEvent 处理一次性事件
- 业务逻辑复用:
-
- 将通用逻辑下沉到 ViewModel
- 多个 Fragment 共享同一个 ViewModel
- 测试友好:
-
- ViewModel 不依赖 Android SDK,可 JUnit 单元测试
- 使用 LiveData/TestObserver 验证数据流
- 分页加载:
-
- 结合 Paging 库,ViewModel 管理 PagedList
LeakCanary 和 AndroidProfile 的具体使用场景和方法
答: 工具使用场景:
- LeakCanary(开发环境):
-
- 场景:开发期自动检测内存泄漏
- 方法:集成后自动监控,发现泄漏通知
- 配置:LeakCanary.config = LeakCanaryConfig(showLeakPeriodically=true)
- 分析:点击通知查看泄漏链,定位持有引用的对象
- Android Profiler(调试期):
-
- 场景:分析内存分配、CPU 热点、网络请求
- 方法:Android Studio → Profile → 选择进程
- 内存:观察 GC 频率和堆增长,导出 hprof 文件
- CPU:方法追踪,找出耗时热点
- 搭配使用:LeakCanary 快速发现泄漏,Profiler 深入分析原因
软件绘制了解吗
答: 软件绘制(Software Rendering):
- 定义:CPU 执行所有绘制操作,不经过 GPU
- 触发场景:
-
- 硬件加速关闭:setLayerType(LAYER_TYPE_SOFTWARE)
- 不支持 GPU 的操作:某些 Canvas API 如 blur 滤镜
- 旧设备/模拟器无 GPU 支持
- 特点:
-
- 优点:兼容性好,调试方便(如绘制边距可视化)
- 缺点:性能差,复杂绘制易卡顿
- 使用场景:
-
- 调试过度绘制(开发者选项强制软件渲染)
- 旧版 Android 兼容性测试
- 特定效果需软件绘制(如阴影、模糊)
Vsync、SurfaceFlinger、OpenGL 了解吗
答: 图形渲染核心组件:
- Vsync(Vertical Sync):
-
- 垂直同步信号,显示器刷新频率(60Hz=16.6ms/帧)
- CPU/GPU 等待 Vsync 信号提交帧,避免撕裂
- Android 4.1 引入 Project Butter 优化 Vsync 处理
- SurfaceFlinger:
-
- 系统级服务,负责合成多个应用的图层
- 接收各应用的 BufferQueue,合并后输出到 FrameBuffer
- 处理 Z-Order、透明度、旋转等合成操作
- OpenGL ES:
-
- 图形渲染 API,Android 硬件加速基础
- View 的绘制命令转换为 OpenGL 指令
- 使用 GPU 执行顶点/像素着色器
- Android 9+ 默认使用 Vulkan 作为新一代图形 API
启动优化
答: 启动优化方案:
- 冷启动流程拆解:
-
- Application.onCreate
- 首个 Activity 的 onCreate/onStart/onResume
- 首帧 render
- 优化方法:
-
- 异步初始化:非关键 SDK 延迟到主线程空闲时初始化
- 懒加载:首页简单数据同步,复杂数据异步加载
- 预加载:Application 创建时预加载首页数据
- Migration 优化:数据库迁移后台线程执行
- 预构建 View:使用 AsyncLayoutInflater 提前 inflation
- 工具监控:
-
- 使用 Startup Time API 统计冷/温/热启动
- Systrace 查看完整链路耗时
项目上线后有监听内存泄漏的机制吗
答: 线上内存泄漏监控机制:
- 内存监控服务:
-
- 定时采集 Runtime.getRuntime().totalMemory/usedMemory
- 内存水位超过阈值(如 80%)上报告警
- 泄漏检测:
-
- 使用 LeakCanary 的 onHeapAnalyzed 监听,只上报线上泄漏
- 基于 MAT 的自动化分析服务(如腾讯 Matrix 的 MemoryDetector)
- 指标监控:
-
- 统计 OOM 率、GC 频率、内存曲线斜率
- 分维度(机型/系统/版本)聚合分析
- 告警闭环:
-
- 内存泄漏超过阈值触发工单
- 关联崩溃率和卡顿率变化
- 版本迭代对比验证修复效果
Android 面试题大全5.0 - 系统与底层
本篇所有问题
- 进程与线程区别、进程切换开销大的原因
- 进程是操作系统分配资源的最小单位,这个资源指什么
- 进程的崩溃会不会导致别的进程崩溃,为什么
- 线程切换开销小为什么,线程切换具体用到哪些指令,怎么保存当前线程上下文
- 进程调度方式(时间片轮转、先来先服务、优先级调度、多级反馈队列)
- 单核和多核进程调度有什么不一样
- 线程组成(TCB)
- 线程映射到硬件实现
- 多进程必要性、如何实现多进程
- 多个进程退出顺序及依据(LRUCache)
- Android 多进程与 Linux 进程区别
- 进程间通信方式(操作系统层面、安卓层面)
- 两个进程读写 SharedPreferences 算进程间通讯吗
- 进程同步
- 管道机制的缺点
- 进程通信方式,介绍 ContentProvider 如何用
- Binder 原理、为什么用 Binder(一次拷贝、安全)
- Binder 和 Socket/管道/共享内存相比特点
- Binder 为什么效率更高(内存映射与一次拷贝)
- 一次 Binder 调用大致流程,数据封装在什么对象中
- Binder 拷贝几次,所有数据都只会拷贝一次吗
- 为什么 Android 选择 Binder 作为主要 IPC 机制
- AIDL 的本质是什么
- 为什么主线程做 Binder 调用也可能卡顿甚至 ANR
- 安卓跨进程通信方式有哪些,用过哪些
- Stub 类中 asInterface 函数作用,BnBinder 和 BpBinder 区别
- ServiceManager 作用与特殊性
- JNI 中 JavaVM 和 JNIEnv 的关系
- JNI 动态/静态注册
- native 调 Java、cpp 线程调 Java 方法注意事项
- java 和 c 如何实现跨语言交互,java 调用 c 的链,c 调用 java 的链
- cpp string 转 jstring 两种方式
- so 编译过程、静态库与动态库区别、动态链接
- ARM 与 x86 区别、RISC 与 CISC
- 大小端
- 原码反码补码、补码优势
- 浮点数表示与运算
- APK 打包流程
- 安卓的打包流程
- 安卓打出的包有哪些文件
- APP 启动流程(点击图标到 Activity 显示)
- 手机按下电源键启动内核
- Linux 启动模型
- APK 入口(ActivityThread.main())
- 可执行文件从源文件到运行的过程
- 调用函数时栈空间变化
- 中断机制的底层原理
- BIO/NIO/AIO 的关系
- 发生异常时如何不让应用退出(UncaughtExceptionHandler)
- C++ 虚函数、指针、指针的指针、智能指针
- 智能指针如何实现,强引用计数指针多线程访问怎么保证安全
- 智能指针哪几种,使用场景
- C++ 线程池,如何实现,优点好处
- C++ 构造函数可以调用虚函数吗
- NDK 有没有了解过
- OpenGL 渲染管线
- 纹理内存优化
- OpenGL PBO 使用过吗
- OpenGL ES 和 OpenGL 区别
- glFlush() 和 glFinish() 区别
- GLSL shader
- 多线程渲染
- 跨端框架了解哪些
- 鸿蒙调用 cpp 怎么做
- 鸿蒙多线程概念是否和其他客户端一致
- Linux 打包和压缩区别
详细问答
问:进程与线程区别、进程切换开销大的原因
进程 是操作系统资源分配的基本单位,拥有独立的地址空间、文件描述符、寄存器等资源。线程是 CPU 调度的基本单位,同一进程内的线程共享内存空间和资源。
进程切换开销大的原因:进程切换需要保存和恢复完整的进程上下文(包括页表、文件描述符表、信号处理等),特别是页表切换会导致 TLB 失效,需要重新加载页表项,造成大量 cache miss,因此开销远大于线程切换。
问:进程是操作系统分配资源的最小单位,这个资源指什么
资源主要包括:虚拟地址空间 (代码段、数据段、堆、栈)、文件描述符表 、信号处理表 、环境变量 、工作目录 、用户/组 ID等。每个进程拥有独立的资源副本,互不干扰。
问:进程的崩溃会不会导致别的进程崩溃,为什么
不会。因为每个进程有独立的虚拟地址空间,一个进程访问非法内存只会触发自身的段错误,不会影响其他进程。操作系统通过内存保护机制(页表权限、隔离)确保进程间互不影响。除非是内核态代码崩溃或共享资源(如信号灯)未正确释放导致死锁。
问:线程切换开销小为什么,线程切换具体用到哪些指令,怎么保存当前线程上下文
开销小的原因:同一进程内的线程共享地址空间和大部分资源,切换时只需保存/恢复寄存器状态(PC、SP、通用寄存器),不需要切换页表,TLB 保持有效。
切换指令:通过操作系统调度器调用 context_switch(),底层使用汇编指令保存寄存器到 TCB(线程控制块),然后从目标线程的 TCB 中恢复寄存器。x86 上可能用到 push/pop、fxsave/fxrstor(保存浮点寄存器)等。
上下文保存:将当前线程的寄存器值保存到其 TCB 结构体中,然后从目标线程的 TCB 中恢复寄存器值,最后跳转到目标线程的执行点。
问:进程调度方式(时间片轮转、先来先服务、优先级调度、多级反馈队列)
- 时间片轮转(RR):每个进程分配固定时间片,时间片用完切换到下一个,公平但可能增加上下文切换。
- 先来先服务(FCFS):按到达顺序执行,简单但短作业可能被长作业阻塞。
- 优先级调度:优先级高的先执行,可能导致低优先级进程饥饿。
- 多级反馈队列(MLFQ):多个优先级队列,新进程进入高优先级队列,时间片用完后降级,兼顾响应时间和吞吐量。
问:单核和多核进程调度有什么不一样
单核:同一时刻只能运行一个进程/线程,调度器通过时间片轮转实现并发假象。需要严格的锁保护共享数据。
多核:多个核心可并行执行多个进程/线程,调度器需要考虑负载均衡(将任务分配到空闲核心)、亲和性(绑定到特定核心减少 cache miss)、同步开销(多核间竞争锁更激烈)。
问:线程组成(TCB)
**TCB(Thread Control Block)**包含:线程 ID、寄存器状态(PC、SP、通用寄存器)、栈指针、优先级、状态(运行/就绪/阻塞)、亲和性掩码、所属进程指针、信号掩码、局部存储等。TCB 是操作系统管理和调度线程的核心数据结构。
问:线程映射到硬件实现
- 用户级线程:由用户态库管理,OS unaware,切换快但无法利用多核。
- 内核级线程:OS 直接管理,可并行执行,但切换需要系统调用。
- 混合模型(如 N:M):用户线程映射到少量内核线程,兼顾灵活性和并发性。
现代 OS(Linux、Windows)主要采用 1:1 模型(一个用户线程对应一个内核线程),通过 pthread 等库实现。
问:多进程必要性、如何实现多进程
必要性:
- 隔离性:崩溃不影响其他进程
- 安全性:不同权限的任务分离
- 利用多核:并行执行提升性能
- 模块化:服务独立部署和维护
实现方式:
- Linux:fork() 复制进程,exec() 加载新程序
- Android:Zygote 进程 fork() 创建应用进程,通过 ActivityManagerService 管理
问:多个进程退出顺序及依据(LRUCache)
Android 中进程按优先级管理:
- 前台进程:正在与用户交互(如可见 Activity),最后被杀
- 可见进程:部分可见(如对话框后的 Activity)
- 服务进程:运行后台服务
- 后台进程:不可见的 Activity
- 空进程:无组件运行,最先被杀
LRUCache 用于缓存进程优先级,低优先级进程在内存不足时先被终止。
问:Android 多进程与 Linux 进程区别
相同点:都使用 Linux 的 fork() 创建,有独立 PID 和地址空间。
不同点:
- Android 进程属于特定应用(UID),受 SELinux 和权限系统限制
- Android 进程由 ActivityManagerService 统一管理生命周期
- Android 提供专门的 IPC 机制(Binder、AIDL)
- Android 进程优先级明确,系统可主动回收
问:进程间通信方式(操作系统层面、安卓层面)
操作系统层面:
- 管道(Pipe):单向/双向字节流,父子进程间常用
- 消息队列:结构化消息,内核维护队列
- 共享内存:最快,但需同步机制
- 信号量:同步控制
- Socket:跨机器通信
Android 层面:
- Binder:主要 IPC 机制,支持一次拷贝、权限控制
- AIDL:Binder 的接口描述语言
- Messenger:基于 Binder 的消息传递
- ContentProvider:数据共享
问:两个进程读写 SharedPreferences 算进程间通讯吗
不算。SharedPreferences 本质是 XML 文件,多进程同时读写会导致数据不一致(无锁保护)。虽然可以通过 MODE_MULTI_PROCESS 标志(已废弃)尝试同步,但不可靠。
正确的 IPC:应使用 Binder、AIDL 或 ContentProvider 在进程间传递数据,然后在各自进程中分别写入自己的 SharedPreferences。
问:进程同步
进程同步是为了协调多个进程的执行顺序,避免竞态条件。常用机制:
- 互斥锁(Mutex):保证临界区互斥访问
- 信号量(Semaphore):控制同时访问的进程数
- 条件变量:等待特定条件成立
- 屏障(Barrier):等待所有进程到达同步点
- 文件锁:通过 flock() 锁定文件区域
Android 中使用 Binder 时,系统会自动处理同步,但共享资源仍需开发者注意。
问:管道机制的缺点
- 单向通信:匿名管道只能单向,需两个管道实现双向
- 无结构:字节流无消息边界,需自定义协议
- 缓冲区有限:写满会阻塞,需合理设计读写节奏
- 生命周期:匿名管道随进程结束而关闭
- 仅限亲属进程:匿名管道只能用于父子/兄弟进程
问:进程通信方式,介绍 ContentProvider 如何用
ContentProvider 使用步骤:
-
定义 Provider:继承 ContentProvider,实现 query/insert/update/delete,在 Manifest 中注册
-
定义 URI:如 content://com.example.provider/data
-
访问数据:
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(uri, projection, selection, args, sortOrder); -
跨进程访问:其他应用通过相同 URI 访问(需权限)
ContentProvider 底层使用 Binder 传输 Cursor 数据(序列化),适合结构化数据共享。
问:Binder 原理、为什么用 Binder(一次拷贝、安全)
原理 :Binder 基于内存映射(mmap),发送方将数据拷贝到内核缓冲区,接收方通过映射直接访问,只需一次拷贝(传统 IPC 需两次:用户→内核→用户)。
为什么用 Binder:
- 高效:一次拷贝减少 CPU 开销
- 安全:支持权限验证(CallingUid、CallingPid),可控制访问
- 稳定:基于 C/S 架构,Server 端可管理连接
- 灵活:支持同步/异步调用、死循环检测
问:Binder 和 Socket/管道/共享内存相比特点
|------|-------------|--------|--------|--------|
| 特性 | Binder | Socket | 管道 | 共享内存 |
| 拷贝次数 | 1 次 | 2 次 | 2 次 | 0 次 |
| 安全性 | 高(权限控制) | 中 | 低 | 低 |
| 易用性 | 高(接口调用) | 中 | 低 | 低(需同步) |
| 适用场景 | Android IPC | 网络通信 | 简单 IPC | 大数据传输 |
Binder 在安全性和易用性上最优,共享内存最快但需额外同步。
问:Binder 为什么效率更高(内存映射与一次拷贝)
内存映射(mmap) :内核开辟一块缓冲区,映射到接收方用户空间。发送方调用 copy_from_user() 将数据拷贝到内核缓冲区,接收方直接访问映射区域,无需二次拷贝。
对比传统 IPC:如管道需要 copy_from_user()(用户→内核) + copy_to_user()(内核→用户),共两次拷贝。Binder 减少了一次拷贝,尤其对大数据优势明显。
问:一次 Binder 调用大致流程,数据封装在什么对象中
流程:
- Client 调用 AIDL 接口方法 → Proxy 类将参数打包到 Parcel 对象
- transact() 将 Parcel 数据通过 Binder 驱动发送到 Server
- Server 端 onTransact() 从 Parcel 解包参数
- 执行实际逻辑,将结果打包到回复 Parcel
- 返回给 Client,解包获取结果
数据封装:所有数据(基本类型、String、Parcelable 对象)都封装在 Parcel 中,支持序列化和反序列化。
问:Binder 拷贝几次,所有数据都只会拷贝一次吗
通常一次 :数据从 Client 用户空间拷贝到内核缓冲区(mmap 区域),Server 通过映射直接读取,算一次拷贝。
特殊情况:
- 大文件描述符:传递 FD 无需拷贝,只需复制引用
- 超大数据:可能触发二次拷贝(如超过 1MB 会失败)
- Binder 不允许直接传递大对象(会抛 TransactionTooLargeException)
问:为什么 Android 选择 Binder 作为主要 IPC 机制
- 性能:一次拷贝优于传统 IPC
- 安全:基于 UID/PID 的权限控制,适配 Android 沙盒模型
- 架构:C/S 模式符合 Android 服务化设计(如 AMS、WMS)
- 稳定:死循环检测(Binder 线程池管理)、支持异步
- 生态:AIDL 简化接口定义,开发友好
问:AIDL 的本质是什么
_AIDL(Android Interface Definition Language)__ 本质是 _ 接口描述语言,编译后生成 Java 代码(Stub 和 Proxy 类):
- Stub:Server 端基类,实现 IBinder 和 onTransact(),处理反序列化和分发
- Proxy:Client 端代理,实现接口方法,负责序列化和 transact() 调用
AIDL 让开发者像调用本地方法一样进行跨进程调用,底层自动处理 Binder 通信。
问:为什么主线程做 Binder 调用也可能卡顿甚至 ANR
Binder 调用虽然是进程间通信,但默认是同步阻塞的:
- Client 调用后等待 Server 返回,期间线程被阻塞
- 若 Server 处理耗时(如 I/O、复杂计算),Client 主线程等待超过 5s 触发 ANR
- Server 端 Binder 线程池耗尽时,请求会排队等待
解决:将 Binder 调用放到子线程,或优化 Server 端处理逻辑,使用异步 AIDL(oneway 修饰)。
问:安卓跨进程通信方式有哪些,用过哪些
- Binder/AIDL:最常用,适合频繁调用
- Messenger:基于 Binder 的消息队列,适合简单命令
- ContentProvider:数据共享(如访问联系人)
- BroadcastReceiver:广播通知(系统或自定义)
- Socket:网络或本地 Socket(适合非 Android 进程)
- 共享文件:如 SharedPreferences(不推荐)、数据库
实际项目中主要用 Binder/AIDL 和 ContentProvider。
问:Stub 类中 asInterface 函数作用,BnBinder 和 BpBinder 区别
asInterface 作用:判断传入的 IBinder 是本地还是远程对象。
- 若是本地(同一进程):直接返回 Stub 本身
- 若是远程(跨进程):返回 Proxy 代理对象
BnBinder vs BpBinder:
- BnBinder(Binder Native):Server 端本地 Binder 对象,实现真实逻辑
- BpBinder(Binder Proxy):Client 端代理对象,负责将调用转发给驱动
BnBinder 对应 Stub,BpBinder 对应 Proxy。
问:ServiceManager 作用与特殊性
作用:Android 的 Binder 服务注册中心,类似 DNS。
- Server 启动时向 SM 注册服务名和 Binder 引用
- Client 通过服务名从 SM 获取 Server 的 Binder 引用
- 管理全局服务的生命周期
特殊性:
- SM 是第一个启动的 Binder 服务(PID=0)
- SM 自身通过硬编码访问(句柄 0),不依赖其他服务
- 权限严格控制,只有系统进程可注册/管理服务
问:JNI 中 JavaVM 和 JNIEnv 的关系
- JavaVM:VM 实例指针,一个进程只有一个,线程安全,可用于创建/销毁 VM
- JNIEnv:线程局部变量,每个线程独有,包含 JNI 函数表,用于调用 JNI 方法
关系:JavaVM 可获取当前线程的 JNIEnv(GetEnv()),但 JNIEnv 不能跨线程使用。新线程需先 AttachCurrentThread()获取 JNIEnv。
问:JNI 动态/静态注册
静态注册:
- Java 声明 native 方法 → javac 编译 → javah 生成头文件 → C 实现 Java_类名_方法名
- 优点:简单;缺点:方法名冗长,类名变动需重新生成
动态注册:
- C 代码中定义 JNINativeMethod 数组,调用 RegisterNatives() 注册
- 优点:方法名可自定义,性能略好(无需字符串查找);缺点:需手动维护映射表
问:native 调 Java、cpp 线程调 Java 方法注意事项
native 调 Java:
- 通过 JNIEnv 调用 CallVoidMethod 等方法
- 注意异常检查(ExceptionCheck())
- 局部引用及时释放(DeleteLocalRef),避免泄露
cpp 线程调 Java:
- 新线程必须 AttachCurrentThread()获取 JNIEnv,结束后 DetachCurrentThread()
- 不能跨线程使用 JNIEnv,每个线程需独立获取
- 全局引用(NewGlobalRef)可跨线程使用,需手动 DeleteGlobalRef 释放
问:java 和 c 如何实现跨语言交互,java 调用 c 的链,c 调用 java 的链
Java 调用 C:Java 代码 → 声明 native 方法 → JNI 层(.so)→ 执行 C/C++ 逻辑 → 返回结果
C 调用 Java:C 代码 → 通过 JNIEnv 获取类/方法 ID → CallObjectMethod/CallStaticMethod 等 → Java 方法执行 → 返回 C
关键:JNIEnv 提供双向调用能力,通过函数指针表访问 JNI API。
问:cpp string 转 jstring 两种方式
方式 1:UTF-8 转换
const char* cstr = cppStr.c_str();
jstring jstr = env->NewStringUTF(cstr);
方式 2:UTF-16 转换(支持更广泛字符)
std::u16string u16str = ...;
jstring jstr = env->NewString((const jchar*)u16str.c_str(), u16str.length());
注意释放局部引用,避免内存泄漏。
问:so 编译过程、静态库与动态库区别、动态链接
so 编译过程:源码 → 预处理 → 编译 → 汇编 → 目标文件(.o)→ 链接(ld)→ .so 文件
静态库(.a)vs 动态库(.so):
- 静态库:编译时链接到可执行文件,体积大但独立
- 动态库:运行时加载,多个程序共享,节省内存,可独立升级
动态链接:程序启动时,链接器(ld.so)加载.so 到内存,解析符号表,绑定函数地址。支持延迟绑定(PLT/GOT)提升启动速度。
问:ARM 与 x86 区别、RISC 与 CISC
ARM(RISC):
- 精简指令集,指令长度固定(32 位),功耗低
- 大量寄存器,load/store 架构(只有加载/存储访问内存)
- 用于移动设备、嵌入式
x86(CISC):
- 复杂指令集,指令长度可变,功能强大
- 支持内存直接运算,兼容性好
- 用于 PC、服务器
RISC vs CISC:RISC 简化硬件设计,通过编译器优化;CISC 用硬件实现复杂功能,减少代码量。
问:大小端
- 大端(Big-Endian):高字节在低地址(人类阅读顺序),网络字节序,PowerPC 使用
- 小端(Little-Endian):低字节在高地址,x86、ARM(默认)使用
检测:
int i = 1;
if (*(char*)&i == 1) 小端;else 大端;
网络传输统一用大端,主机需转换(htons/ntohs)。
问:原码反码补码、补码优势
原码 :符号位 + 绝对值(0 正 1 负),如 +5=00000101,-5=10000101
反码 :正数同原码,负数符号位不变其余取反,如 -5=11111010
补码:正数同原码,负数=反码 +1,如 -5=11111011
补码优势:
- 统一加减法:减法变加法(A-B=A+(-B) 的补码)
- 0 唯一表示:+0 和 -0 都是 00000000
- 符号位参与运算,简化硬件设计
问:浮点数表示与运算
IEEE 754 标准 :32 位 float=1 符号位 +8 指数位 +23 尾数位;64 位 double=1+11+52
表示 :(-1)s × 1.M × 2(E-127),如 5.5=0 10000001 01100000000000000000000
运算 :对阶(小指数向大指数对齐)→ 尾数加减 → 规格化 → 舍入 → 溢出检查
精度问题:0.1+0.2≠0.3,因十进制小数无法精确表示为二进制,金融计算需用 BigDecimal。
问:APK 打包流程
- 资源编译:aapt2 编译 res/→ resources.arsc + R.java
- 代码编译:javac 编译.java→.class → d8/r8 转换.class→.dex(Dalvik 字节码)
- 打包:apkbuilder 合并.dex + resources.arsc + 资源文件 + AndroidManifest.xml → 未签名 APK
- 签名:jarsigner 或 apksigner 用私钥签名(v1/v2/v3 方案)
- 对齐:zipalign 优化资源访问(4 字节对齐)
- 输出:生成可安装的.apk 文件
问:安卓的打包流程
Debug 与 Release 流程类似,区别:
- Debug:自动签名(debug.keystore),代码不混淆,支持 instant run
- Release:手动签名(正式 keystore),代码混淆(ProGuard/R8),资源压缩,zipalign 对齐
Gradle 构建流程:assembleDebug/assembleRelease → 执行 task 链(compileJava → dex → mergeResources → package → sign → align)。
问:安卓打出的包有哪些文件
- classes.dex:Dalvik 字节码(Java/Kotlin 代码)
- resources.arsc:编译后的资源表(R.java 映射)
- res/:资源文件(图片、布局、字符串等)
- assets/:原始资源(不编译,通过 AssetManager 访问)
- lib/:so 库(按 ABI 分 armeabi-v7a、arm64-v8a、x86 等)
- AndroidManifest.xml:应用配置(二进制格式)
- META-INF/:签名文件(MANIFEST.MF、CERT.SF、CERT.RSA)
问:APP 启动流程(点击图标到 Activity 显示)
- Launcher:点击图标 → 调用 ActivityManagerService(AMS)
- AMS:检查进程是否存在,若不存在通知 Zygote fork 新进程
- Zygote:fork 应用进程 → 加载 ActivityThread
- ActivityThread:main() 启动 → 创建 Application → attach 到 AMS
- AMS:调度 Lifecycle → onCreate() → onStart() → onResume()
- WindowManager:请求 SurfaceFlinger 创建窗口 → 显示 UI
问:手机按下电源键启动内核
- BootROM:固化在芯片中,上电执行,加载 Bootloader 到 RAM
- Bootloader(如 U-Boot):初始化硬件(时钟、内存、串口)→ 加载内核镜像到内存
- Kernel:解压内核 → 初始化子系统(内存管理、进程调度、设备驱动)→ 挂载 rootfs
- Init:第一个用户态进程(PID=1)→ 启动系统服务(如 Android 的 init.rc)
问:Linux 启动模型
用户空间 ←→ 内核空间 ←→ 硬件
启动流程:BIOS/UEFI → Bootloader → Kernel 初始化(CPU、内存、中断)→ 挂载根文件系统 → Init 进程 → 运行级/Target → 登录 shell
Android 差异:Init 启动后加载 Android 专有服务(SurfaceFlinger、AudioFlinger、zygote),不运行 getty/login。
问:APK 入口(ActivityThread.main())
入口类 :android.app.ActivityThread
main() 方法:
public static void main(String[] args) {
ActivityThread thread = new ActivityThread();
thread.attach(false); // 绑定到 AMS
Looper.prepareMainLooper(); // 创建主线程消息循环
new ActivityThread().bindApplication(...); // 创建 Application
Looper.loop(); // 进入消息循环
}
Application 对象在此创建,四大组件由 AMS 通过 Binder 调度。
问:可执行文件从源文件到运行的过程
- 源码:.c/.cpp 文件
- 预处理:展开宏、头文件、条件编译 → .i 文件
- 编译:词法/语法/语义分析 → 汇编代码.s
- 汇编:.s→.o 目标文件(机器码 + 符号表)
- 链接:静态库(.a)/动态库(.so)链接 → 可执行文件
- 加载:OS 加载器分配内存 → 解析动态库 → 跳转到入口点(_start→main)
问:调用函数时栈空间变化
调用前 :SP 指向当前栈顶
调用:
- 参数入栈(从右到左)
- 返回地址入栈(call 指令自动)
- 保存旧栈帧(push rbp / mov rbp,rsp)
- 分配局部变量(sub rsp, N)
调用中 :SP 指向新的栈帧,局部变量通过 rbp-offset 访问
返回:leave(mov rsp,rbp / pop rbp)→ ret(pop rip)
问:中断机制的底层原理
硬件中断:外设(键盘、网卡)触发 CPU 中断引脚 → 保存现场(寄存器)→ 查中断向量表 → 跳转 ISR(中断服务程序)→ 恢复现场 → 继续执行
软中断:指令触发(如 x86 int 0x80、ARM svc),用于系统调用
中断处理:关中断(cli)→ 执行 ISR → 开中断(sti),支持嵌套(高优先级中断低优先级)。
问:BIO/NIO/AIO 的关系
- BIO(Blocking IO):同步阻塞,每个连接一个线程,适合连接数少的场景
- NIO(Non-blocking IO):同步非阻塞,多路复用(Select/Poll/Epoll),单线程管理多连接
- AIO(Asynchronous IO):异步非阻塞,操作系统完成 IO 后通知应用,并发性能最好
Java 对应:BIO→ServerSocket;NIO→Selector+Channel;AIO→AsynchronousChannel(Linux 支持有限)。
问:发生异常时如何不让应用退出(UncaughtExceptionHandler)
全局捕获:
Thread.setDefaultUncaughtExceptionHandler((t, e) => {
Log.e("Crash", "Thread: " + t.getName(), e);
// 保存日志、上传崩溃信息
});
注意:捕获后仅记录日志,中的应用可能处于不一致状态,应尽快重启或退出关键 Activity。Android 还可以用 try-catch 包裹关键代码,避免崩溃传播。
问:C++ 虚函数、指针、指针的指针、智能指针
虚函数 :通过虚函数表(vtable)实现动态绑定,基类指针调用派生类重写的方法
指针 :存储变量地址,如 int* p = &x;
指针的指针 :指向指针的指针,如 int** pp = &p;,用于二维数组或修改指针本身
智能指针:自动管理内存的类模板(unique_ptr、shared_ptr、weak_ptr),避免野指针和内存泄漏
问:智能指针如何实现,强引用计数指针多线程访问怎么保证安全
实现原理:
- 构造函数:new 对象,初始化引用计数
- 拷贝构造:计数 +1
- 析构函数:计数 -1,为 0 时 delete 对象
线程安全:
- 引用计数使用原子操作(std::atomic)或互斥锁保护
- shared_ptr 控制块(计数 + 对象指针)的访问是线程安全的
- 注意 :多线程访问同一对象仍需额外同步(智能指针不保护对象内容)
问:智能指针哪几种,使用场景
- unique_ptr:独占所有权,不可拷贝(移动语义),适用于独占资源(文件句柄)
- shared_ptr:共享所有权,引用计数,适用于多个所有者(如缓存对象)
- weak_ptr:不增加计数,解决 shared_ptr 循环引用问题(如父 - 子节点)
问:C++ 线程池,如何实现,优点好处
实现:
- 创建固定数量的工作线程(阻塞等待任务)
- 任务队列(std::queue + mutex + condition_variable)
- 提交任务时加锁入队 → notify 唤醒线程
- 线程取任务执行 → 循环等待
优点:
- 减少线程创建/销毁开销
- 控制并发数量,避免资源耗尽
- 复用线程,提升响应速度
问:C++ 构造函数可以调用虚函数吗
可以但不建议 。构造函数中调用虚函数时,不会触发多态,而是调用当前类的版本(因为派生类还未构造完成)。
示例:基类构造函数调虚函数,实际调用基类实现而非派生类重写版本,易导致逻辑错误。
问:NDK 有没有了解过
NDK(Native Development Kit):用于在 Android 中开发 C/C++ 代码。
- 用途:性能敏感(图像处理、音视频编解码)、复用现有 C/C++ 库、游戏引擎
- 工具链:CMake/ndk-build → 生成.so → 打包到 APK 的 lib/目录
- 交互:通过 JNI 与 Java 层通信
- 调试:Android Studio 支持 native 断点调试
问:OpenGL 渲染管线
流程:
- 顶点着色器:处理顶点坐标、变换(MVP 矩阵)
- 图元装配:顶点组成点/线/三角形
- 光栅化:图元转换为片元(像素候选)
- 片元着色器:计算颜色、纹理采样
- 测试与混合:深度测试、模板测试、alpha 混合 → 输出到帧缓冲
问:纹理内存优化
- 压缩格式:ETC2(Android)、ASTC(移动端高效)
- Mipmap:预生成多级 LOD,远处用小图,提升 cache 命中率
- 纹理图集:合并多张小图为一张,减少绑定切换
- 按需加载:动态卸载不可见纹理
- 内存对齐:确保纹理尺寸为 4 的倍数
问:OpenGL PBO 使用过吗
PBO(Pixel Buffer Object):用于异步像素数据传输(如纹理上传、像素读取)。
使用场景:
- 视频帧上传:CPU 填充 PBO → GPU 异步读取,不阻塞渲染
- 截图(glReadPixels):用 PBO 避免同步等待
优势:双缓冲/多缓冲 PBO 可实现流水线传输,提升吞吐量。
问:OpenGL ES 和 OpenGL 区别
- OpenGL ES:嵌入式版,精简 API(移除 glBegin/glEnd、四边形、GL_QUADS),适合移动 GPU
- OpenGL:桌面完整版,支持更多特性(tessellation、compute shader)
- 兼容:ES 是 OpenGL 的子集,Shader 语言基本一致(GLSL ES)
问:glFlush() 和 glFinish() 区别
- glFlush():将命令缓冲区提交给 GPU,但不等待完成(非阻塞)
- glFinish():提交并等待 GPU 执行完所有命令(阻塞,用于同步)
使用:Flush 适合流水线(CPU 继续准备下一帧);Finish 用于调试或需要精确同步时(性能开销大)。
问:GLSL shader
GLSL(OpenGL Shading Language):GPU 编程语言,运行在顶点/片元着色器中。
示例(片元着色器):
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
void main() {
gl_FragColor = texture2D(uTexture, vTexCoord);
}
特性:SIMD 并行执行,无循环/递归限制(ES 有),依赖 uniform/varying 传递数据。
问:多线程渲染
概念:主线程负责逻辑,子线程并行准备渲染数据(如骨骼动画、粒子系统),最后在主线程提交 GPU。
实现:
- Vulkan/DirectX12:支持多线程命令缓冲录制
- OpenGL ES:有限支持(多线程资源加载)
- Unity:Job System + Burst 实现并行渲染准备
收益:减少 CPU 瓶颈,提升帧率(尤其复杂场景)。
问:跨端框架了解哪些
- React Native:JS+ 原生桥接,热更新,性能中等
- Xamarin:C# + .NET,编译为 native,适合微软生态
- uni-app/Taro:JS 语法,编译为小程序/H5/App
- Weex:阿里,类似 RN,已式微
问:鸿蒙调用 cpp 怎么做
方式 1:NAPI(Node-API 风格)
// C++
extern "C" __attribute__((visibility("default"))) void Init(NativeEngine* engine);
方式 2:CMake 构建 :在 CMakeLists.txt 中链接 libace_napi.z.so
调用:ArkTS 通过 nativeBinding.so 导入函数
类似 Android JNI,但鸿蒙使用 NAPI 标准接口。
问:鸿蒙多线程概念是否和其他客户端一致
一致。鸿蒙也使用 pthread 模型(概念类似 Android/Linux):
- 主线程:UI 线程,禁止耗时操作
- Worker:鸿蒙提供的线程池抽象,用于后台任务
- TaskPool:任务池,自动调度到线程池
底层与 Linux 一致(futex 同步、调度器),但 API 封装为 ArkTS 友好接口。
问:Linux 打包和压缩区别
- 打包(tar):将多个文件合并为一个文件(.tar),不压缩,保留目录结构和权限
- 压缩(gzip/bzip2/xz):使用算法减小体积,如 file.tar.gz(先打包后压缩)
常用组合:
- tar + gzip:.tar.gz(快速)
- tar + bzip2:.tar.bz2(压缩率更高)
- zip:打包 + 压缩一体(Windows 流行)
Android 面试题大全6.0 - 数据库·设计模式·Git
本篇所有问题
数据库(1-14)
- SQLite 优化手段
- 分页查询 SQL
- 多表查询
- group by、join
- 三大范式
- 关系型数据库优化手段
- 主键是什么
- 事务(多步骤操作全部生效)
- MySQL 中 drop 和 delete 的区别和联系
- 数据库索引作用、使用场景,底层结构
- 安卓数据库(如 SQLite)的索引是什么,底层结构
- MySQL 事务,隔离级别
- 持久化存储(安卓常用)
- 保存到 SQLite
设计模式(15-33)
- 单例模式(双重校验锁、静态内部类、枚举、懒汉式、饿汉式)
- 单例模式有几种,为什么采取单例模式
- 单例模式特点
- 懒汉式单例,双重检测加几次锁,synchronized 锁的是什么,为什么
- 饿汉式和懒汉式是线程安全的吗
- 乐观锁写单例
- 责任链模式(View 事件分发、OkHttp 拦截器)
- 观察者模式(EventBus、LiveData)
- 适配器模式(RecyclerView)
- 装饰者模式(InputStream)
- 建造者模式(AlertDialog、Retrofit)
- 策略模式(Interpolator、Comparator)
- 工厂模式,包含哪几个类,抽象类是什么
- 代理模式
- 委派模式,委派模式和代理模式区别
- 结合 Android 源码解释设计模式
- okhttp 设计模式有哪些
- 常见的设计模式,举实际开发中的例子
- 设计模式了解哪些
Git(34-40)
- git merge 与 git rebase 区别
- 已经 commit 了代码但需要修改,且不想新增 commit 记录,用什么命令
- Git 怎么解决冲突?
- Git 的底层实现逻辑?
- 常用 git 命令?
- Git 使用相关类?
- 如何使用 GitHub 进行版本控制?
面试题详解
数据库(1-14)
问:SQLite 优化手段
SQLite 优化手段包括:
- 索引优化:为常用查询字段创建索引
- SQL 语句优化:避免 SELECT *,只查询需要的字段
- 事务优化:批量操作使用事务包裹
- PRAGMA 设置:调整 cache_size、journal_mode 等参数
- 分页查询:使用 LIMIT 和 OFFSET 避免一次性加载大量数据
- 避免 N+1 查询:使用 JOIN 或 IN 语句
- 数据库连接池:复用数据库连接
问:分页查询 SQL
-- 基本分页查询
SELECT * FROM table_name LIMIT 10 OFFSET 20;
-- 或者
SELECT * FROM table_name LIMIT 20, 10;
-- 优化分页(使用子查询)
SELECT * FROM table_name WHERE id > 100 LIMIT 10;
问:多表查询
-- 内连接(INNER JOIN)
SELECT a.*, b.* FROM table_a a INNER JOIN table_b b ON a.id = b.a_id;
-- 左连接(LEFT JOIN)
SELECT a.*, b.* FROM table_a a LEFT JOIN table_b b ON a.id = b.a_id;
-- 右连接(RIGHT JOIN)
SELECT a.*, b.* FROM table_a a RIGHT JOIN table_b b ON a.id = b.a_id;
-- 多表连接
SELECT a.*, b.*, c.*
FROM table_a a
INNER JOIN table_b b ON a.id = b.a_id
INNER JOIN table_c c ON b.id = c.b_id;
问:group by、join
GROUP BY:用于对结果集进行分组,通常与聚合函数配合使用
SELECT department, COUNT(*) as emp_count
FROM employees
GROUP BY department;
JOIN:用于连接多个表
- INNER JOIN:返回两表匹配的行
- LEFT JOIN:返回左表所有行,右表无匹配则为 NULL
- RIGHT JOIN:返回右表所有行,左表无匹配则为 NULL
- FULL JOIN:返回两表所有行
问:三大范式
第一范式(1NF) :每个列都不可再分,保证原子性
第二范式(2NF) :满足 1NF,非主键列完全依赖于主键
第三范式(3NF):满足 2NF,非主键列之间不存在传递依赖
问:关系型数据库优化手段
- 合理设计索引
- 优化 SQL 语句(避免 SELECT *,使用 EXPLAIN 分析)
- 使用连接池
- 读写分离
- 分库分表
- 缓存热点数据
- 定期清理无用数据
- 合理设计表结构(范式与反范式权衡)
问:主键是什么
主键(Primary Key)是表中唯一标识每一行记录的字段或字段组合。特点:
- 唯一性:每行主键值必须唯一
- 非空性:主键不能为 NULL
- 一个表只能有一个主键
- 主键自动创建索引
问:事务(多步骤操作全部生效)
事务是一组 SQL 操作的原子单元,要么全部成功,要么全部失败。特征(ACID):
-
原子性(Atomicity):事务是最小执行单位
-
一致性(Consistency):事务执行前后数据保持一致
-
隔离性(Isolation):并发事务互不干扰
-
持久性(Durability):事务提交后永久生效
// Android SQLite 事务示例
db.beginTransaction();
try {
db.execSQL("INSERT INTO ...");
db.execSQL("UPDATE ...");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
问:MySQL 中 drop 和 delete 的区别和联系
|-------|--------------|---------------|
| 特性 | DROP | DELETE |
| 类型 | DDL(数据定义语言) | DML(数据操作语言) |
| 作用 | 删除整个表(结构和数据) | 删除表中数据 |
| 回滚 | 不可回滚 | 可回滚 |
| 触发器 | 不触发 | 触发 DELETE 触发器 |
| 自增 ID | 重置 | 保留 |
问:数据库索引作用、使用场景,底层结构
作用 :加速查询,减少 I/O 操作
使用场景:
- 频繁查询的字段
- WHERE 子句中的字段
- JOIN 连接字段
- ORDER BY 排序字段
底层结构:
- MySQL(InnoDB):B+ 树(聚簇索引、非聚簇索引)
- MySQL(MyISAM):B 树
- SQLite:B 树
问:安卓数据库(如 SQLite)的索引是什么,底层结构
索引:SQLite 使用 B 树(B-tree)结构存储索引
底层结构:
-
索引是单独的 B 树,叶子节点存储指向数据行的指针
-
支持 ASC/DESC 索引
-
主键默认创建索引
-
多列索引(复合索引)遵循最左前缀原则
CREATE INDEX idx_name ON table_name(column1, column2);
问:MySQL 事务,隔离级别
事务:See 110 题
MySQL 四种隔离级别:
- READ UNCOMMITTED(读未提交):可能读到脏数据
- READ COMMITTED(读已提交):避免脏读,可能不可重复读
- REPEATABLE READ(可重复读):MySQL 默认级别,避免不可重复读,可能幻读
- SERIALIZABLE(串行化):最高级别,串行执行,性能低
问:持久化存储(安卓常用)
Android 常用持久化存储方案:
- SharedPreferences:键值对存储,适合配置信息
- SQLite 数据库:关系型存储,适合结构化数据
- 文件系统:内部存储/外部存储,适合大文件
- Room 数据库:SQLite 的 ORM 封装
- DataStore:SharedPreferences 的替代方案
- Network 缓存:适合临时数据
问:保存到 SQLite
// 方式 1:直接 SQL
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "张三");
values.put("age", 25);
db.insert("user", null, values);
// 方式 2:使用 Room(推荐)
@Dao
public interface UserDao {
@Insert
void insert(User user);
}
设计模式(15-33)
问:单例模式(双重校验锁、静态内部类、枚举、懒汉式、饿汉式)
// 1. 饿汉式(线程安全)
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() { return instance; }
}
// 2. 懒汉式(线程不安全)
public class Singleton2 {
private static Singleton2 instance;
private Singleton2() {}
public static Singleton2 getInstance() {
if (instance == null) instance = new Singleton2();
return instance;
}
}
// 3. 双重校验锁(DCL,线程安全)
public class Singleton3 {
private static volatile Singleton3 instance;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
// 4. 静态内部类(线程安全,推荐)
public class Singleton4 {
private Singleton4() {}
private static class Holder {
private static Singleton4 instance = new Singleton4();
}
public static Singleton4 getInstance() { return Holder.instance; }
}
// 5. 枚举(线程安全,最简洁)
public enum Singleton5 {
INSTANCE;
}
问:单例模式有几种,为什么采取单例模式
五种实现:饿汉式、懒汉式、双重校验锁(DCL)、静态内部类、枚举
使用原因:
- 确保全局只有一个实例
- 节省系统资源(如数据库连接、线程池)
- 方便全局访问
- 避免重复初始化
问:单例模式特点
- 私有构造方法
- 静态方法返回唯一实例
- 全局唯一实例
- 懒加载/饿加载
- 线程安全考虑
问:懒汉式单例,双重检测加几次锁,synchronized 锁的是什么,为什么
双重检测加锁:只有第一次检查 instance 为 null 时进入同步块,第二次检查确认后创建实例
synchronized 锁的是类对象 :Singleton.class,因为 getInstance 是 static 方法
为什么用 volatile:防止指令重排序,避免其他线程获取到未初始化完成的对象
问:饿汉式和懒汉式是线程安全的吗
- 饿汉式:线程安全,类加载时就创建实例
- 懒汉式(基础版):线程不安全,多线程可能创建多个实例
- 懒汉式(DCL 版):线程安全,使用 synchronized 和 volatile
问:乐观锁写单例
public class Singleton {
private static AtomicReference<Singleton> instance = new AtomicReference<>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton current = instance.get();
if (current != null) return current;
Singleton newInstance = new Singleton();
if (instance.compareAndSet(null, newInstance)) return newInstance;
}
}
}
问:责任链模式(View 事件分发、OkHttp 拦截器)
View 事件分发:Activity → PhoneWindow → DecorView → ViewGroup → View
OkHttp 拦截器:责任链模式处理请求/响应
// 拦截器链
Interceptor.Chain chain;
Response proceed(Interceptor.Chain chain);
问:观察者模式(EventBus、LiveData)
EventBus:事件总线,发布/订阅模式
EventBus.getDefault().register(this);
EventBus.getDefault().post(new MessageEvent());
@Subscribe public void onMessageEvent(MessageEvent event) {}
LiveData:生命周期感知,数据变化自动通知观察者
liveData.observe(this, data -> {});
问:适配器模式(RecyclerView)
RecyclerView.Adapter 是典型的适配器模式:
RecyclerView.Adapter adapter = new MyAdapter();
recyclerView.setAdapter(adapter);
// 将数据源适配为 RecyclerView 可识别的 ViewHolder
问:装饰者模式(InputStream)
Java IO 中的装饰者模式:
InputStream fin = new FileInputStream("file.txt");
InputStream bin = new BufferedInputStream(fin); // 装饰
InputStream zin = new ZipInputStream(bin); // 继续装饰
问:建造者模式(AlertDialog、Retrofit)
AlertDialog:
new AlertDialog.Builder(context)
.setTitle("标题")
.setMessage("内容")
.setPositiveButton("确定", listener)
.create()
.show();
Retrofit:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
问:策略模式(Interpolator、Comparator)
Interpolator:动画插值器策略
view.setInterpolator(new AccelerateInterpolator());
view.setInterpolator(new DecelerateInterpolator());
Comparator:比较策略
Collections.sort(list, new Comparator<Item>() {
public int compare(Item a, Item b) { return a.price - b.price; }
});
问:工厂模式,包含哪几个类,抽象类是什么
简单工厂:一个工厂类根据参数创建不同对象
工厂方法:抽象工厂类 + 多个具体工厂类
public abstract class Factory {
public abstract Product createProduct();
}
public class ConcreteFactory extends Factory {
public Product createProduct() { return new ConcreteProduct(); }
}
抽象工厂:创建产品族的工厂接口
问:代理模式
// 静态代理
interface Subject { void doSomething(); }
class RealSubject implements Subject { public void doSomething() {} }
class Proxy implements Subject {
private Subject target = new RealSubject();
public void doSomething() {
// 前置处理
target.doSomething();
// 后置处理
}
}
// 动态代理(JDK)
Subject proxy = (Subject) Proxy.newProxyInstance(
classLoader, new Class[]{Subject.class}, handler);
问:委派模式,委派模式和代理模式区别
委派模式:将任务委托给多个执行者
public class Leader {
private Map<String, Employee> employees = new HashMap<>();
public void doTask(String taskName, Task task) {
employees.get(taskName).doTask(task);
}
}
区别:
- 代理模式:代理者与被代理者实现相同接口,强调"代理"
- 委派模式:委派者不怎么干活,实际工作交给别的对象,强调"任务分配"
问:结合 Android 源码解释设计模式
- 单例模式:ActivityManager、WindowManager
- 观察者模式:BroadcastReceiver、LiveData
- 适配器模式:RecyclerView.Adapter、ListView.Adapter
- 工厂模式:BitmapFactory(工厂方法)
- 建造者模式:AlertDialog.Builder
- 策略模式:Interpolator、LayoutAnimationController
- 责任链模式:View 的事件分发、OkHttp 拦截器
问:okhttp 设计模式有哪些
- 责任链模式:拦截器链(Interceptor.Chain)
- 建造者模式:Request.Builder、OkHttpClient.Builder
- 工厂模式:CookieJar.Factory
- 单例模式:连接池
- 缓存模式:内置 HTTP 缓存
问:常见的设计模式,举实际开发中的例子
创建型:
- ArrayList 的 Iterator(工厂方法)
- DataStore.Builder(建造者)
结构型:
- RecyclerView.Adapter(适配器)
- Stream 流(装饰者)
行为型:
- EventBus(观察者)
- Collections.sort(策略)
问:设计模式了解哪些
- 创建型:单例、工厂(简单/方法/抽象)、建造者、原型
- 结构型:适配器、装饰者、代理、桥接、组合、外观、享元
- 行为型:责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者
Git(34-40)
问:git merge 与 git rebase 区别
|-----------|------------------|-------------------|
| 特性 | merge | rebase |
| 提交历史 | 保留合并记录(分叉) | 线性历史,更整洁 |
| 新增 commit | 会新增 merge commit | 不会新增 merge commit |
| 冲突处理 | 一次性解决所有冲突 | 可能多次解决冲突 |
| 协作开发 | 安全,推荐 | 已推送的分支不要使用 |
# merge
git checkout main
git merge feature-branch
# rebase
git checkout feature-branch
git rebase main
问:已经 commit 了代码但需要修改,且不想新增 commit 记录,用什么命令
# 修改最后一次提交(不新增 commit 记录)
git commit --amend
# 如果已经 push,需要强制推送
git push --force-with-lease
问:Git 怎么解决冲突?
- 找到冲突文件(git status)
- 手动编辑冲突内容(<<<<<<< HEAD 和 >>>>>>>)
- 标记解决:
git add <file> - 完成合并:
git commit或git rebase --continue - 取消合并:
git merge --abort或git rebase --abort
问:Git 的底层实现逻辑?
四大组件:
- Working Directory:工作区
- Staging Area(Index):暂存区
- Local Repository:本地仓库(.git 目录)
- Remote Repository:远程仓库
存储结构:
- objects:存储对象(blob、tree、commit、tag)
- refs:存储分支和标签引用
- HEAD:当前分支指针
- config:本地配置
问:常用 git 命令?
# 基础
git init / clone / add / commit / push / pull
git status / log / diff / blame
# 分支
git branch / checkout / switch / merge / rebase
git branch -d / -D / -m
# 撤销
git reset --hard / --soft / --mixed
git revert / commit --amend
# 远程
git remote add / fetch / remote -v
git tag / tag -a
# 查看
git log --oneline --graph
git show / cat-file -p
问:Git 使用相关类?
Android/IntelliJ 中的 Git 实现:
- JGit(Java 实现)
- IntelliJ Git4Idea 插件
- Android Studio 内置 Git 支持
核心类(JGit):
Repository:仓库对象RevCommit:提交对象ObjectId:对象 IDRefDatabase:引用数据库
问:如何使用 GitHub 进行版本控制?
# 1. 创建本地仓库
git init
git add .
git commit -m "initial commit"
# 2. 关联远程仓库
git remote add origin https://github.com/username/repo.git
# 3. 推送代码
git push -u origin main
# 4. 协作开发
git pull origin main # 拉取最新代码
git checkout -b feature # 创建新分支
git push origin feature # 推送分支
# 5. 提交 Pull Request(GitHub 网页操作)
# 6. 代码审查后合并
Android 面试题大全7.0 - 算法与数据结构
本篇所有问题
- 栈和队列的区别
- 哈希表的原理
- 完全二叉树验证
- 堆排序
- 递归函数遍历二叉树
- 检验出栈序列的合法性
- 数组和链表寻找任意元素的时间复杂度
- 数组如何实现 O(1) 时间复杂度访问
- 数组和链表的区别
- 链表:单链表和双链表
- LRU 算法
- 二分查找复杂度
- 快排和堆排时间复杂度
- 数据结构特点
- 怎么判断链表有环
- 翻转单链表
- 反转链表(奇偶链表)
- 单向链表倒数第 k 个节点
- 合并两个有序链表
- 合并 K 个升序链表
- 判断单链表有没有环
- 删除升序链表中所有重复元素
- 两个链表寻找交叉节点
- 快速排序
- 堆排序
- 冒泡排序优化
- 最长递增子序列
- 最长无重复子串
- 无重复字符最长子串
- 字符串相加
- 字符串相乘
- 最长公共前缀
- 字符串最长递增子串
- 判断 s2 中是否存在 s1 的全排列序列
- 连续子数组的最大和
- 数组第 K 个最大元素
- 前 k 个最大元素
- 查找第 k 小的数
- TopK
- 合并两个有序数组
- 两个有序数组合并
- 二分查找
- 搜索旋转排序数组
- 有序数组查找某个数首次出现的位置
- 有序数组删除重复元素
- 一次遍历将只包含 1,2,3 的数组排序
- 先增后降数组去重并排序
- 排序后相邻元素的最大差值
- 二维矩阵找 target
- 旋转数组查找指定值
- 螺旋输出矩阵
- 二维螺旋数组
- 对称二叉树
- 二叉树的中序遍历
- 二叉树的前序遍历
- 二叉树的右视图
- 二叉树的深度
- 二叉树的最小深度
- 层次遍历/之字遍历
- 求二叉树高度
- 数组实现队列
- 两个栈实现队列
- 用两个栈实现队列
- 添加删除随机获取都是 O(1)
- 线程安全的单例
- 双重判定单例模式
- 生产者消费者模式
- 线程池设计
- 两个线程轮流给变量 i+1
- 三个线程依次打印 1,2,3
- 五个线程顺序打印 1~无穷大
- 三个线程按顺序打印 ABC
- 多线程分段下载文件
- 写一个线程池
- gas 数组和 cost 数组
- n 个人发糖果
- 字符串数组不包含相同字母的最大长度乘积
- 文本左右对齐
- 不使用乘除 mod 移位实现整数除法
- LRU 缓存机制
题目详解
基础概念(15 题)
栈和队列的区别
栈是后进先出(LIFO),只允许在一端(栈顶)进行插入和删除;队列是先进先出(FIFO),允许在队尾插入、队头删除。栈常用于括号匹配、表达式求值、递归实现;队列常用于 BFS、任务调度、消息队列。
哈希表的原理,如何解决哈希冲突,所有类型都可以作为键吗
哈希表通过哈希函数将键映射到数组索引。解决哈希冲突的方法:链地址法(拉链法)、开放寻址法(线性探测、二次探测)、再哈希法。不是所有类型都可作为键,键类型必须可哈希(不可变对象如 String、Integer 可以,可变对象如 List 不行),且需重写 hashCode() 和 equals()。
完全二叉树验证
完全二叉树定义:除最后一层外,其他层节点数都达到最大,且最后一层节点都靠左排列。验证方法:层序遍历,遇到第一个空节点后,后续所有节点都必须是空。若出现非空节点则不是完全二叉树。
堆排序
堆排序利用堆的性质(大顶堆或小顶堆)。步骤:1)构建初始堆(从最后一个非叶子节点下沉);2)交换堆顶与末尾元素;3)缩小堆范围,重新调整堆;4)重复直到堆大小为 1。时间复杂度 O(nlogn),空间复杂度 O(1),不稳定排序。
递归函数遍历二叉树,怎么实现的
递归遍历基于函数调用栈。前序:访问根→递归左→递归右;中序:递归左→访问根→递归右;后序:递归左→递归右→访问根。每次递归调用保存当前状态到调用栈,返回时恢复状态继续执行。
检验出栈序列的合法性
给定入栈序列和出栈序列,判断出栈序列是否合法。方法:用辅助栈模拟,遍历入栈序列依次压栈,若栈顶等于当前出栈元素则弹出,继续检查下一个出栈元素。最后栈为空则合法。
数组和链表寻找任意元素的时间复杂度
数组:O(1) 随机访问(通过下标),O(n) 查找元素(需遍历)。链表:O(n) 访问任意位置(需从头遍历),O(n) 查找元素。数组适合随机访问频繁的场景,链表适合插入删除频繁的场景。
数组如何实现 O(1) 时间复杂度访问
数组在内存中连续存储,元素大小固定。访问下标 i 的元素时,通过基地址 + i * 元素大小直接计算内存地址,无需遍历,因此是 O(1) 时间复杂度。
数组和链表的区别
数组:内存连续,支持 O(1) 随机访问,插入删除需移动元素 O(n),大小固定(静态数组)或需扩容(动态数组)。链表:内存不连续,不支持随机访问 O(n),插入删除 O(1)(已知位置),大小动态,额外存储指针开销。
链表:单链表和双链表
单链表:每个节点包含数据和 next 指针,只能单向遍历。双链表:每个节点包含数据、prev 指针和 next 指针,可双向遍历。双链表插入删除更方便(可获取前驱节点),但空间开销更大。
LRU 算法
LRU(最近最少使用)缓存淘汰算法。实现:哈希表 + 双向链表。哈希表存储 key 到节点的映射,双向链表维护访问顺序(最近访问的在头部)。访问时移到头部,容量满时删除尾部节点。get 和 put 均为 O(1)。
二分查找复杂度
二分查找时间复杂度:O(log n),每次排除一半元素。空间复杂度:迭代 O(1),递归 O(log n)(调用栈)。前提:数组必须有序。最坏情况查找到最后一个元素或不存在。
快排和堆排时间复杂度
快速排序:平均 O(nlogn),最坏 O(n²)(已排序且选端点为 pivot),空间 O(log n)(递归栈)。堆排序:平均和最坏均为 O(nlogn),空间 O(1)。快排通常更快但最坏情况差,堆排序稳定但常数因子大。
数据结构特点
数组:连续内存,O(1) 访问,O(n) 插入删除。链表:O(n) 访问,O(1) 插入删除。栈:LIFO,O(1) 压栈弹栈。队列:FIFO,O(1) 入队出队。哈希表:O(1) 平均查找,最坏 O(n)。树:O(log n) 查找(平衡树)。堆:O(1) 取最值,O(log n) 插入删除。
怎么判断链表有环,如何计算环的大小
判断有环:快慢指针法,快指针每次走 2 步,慢指针每次走 1 步,若相遇则有环。计算环大小:相遇后,固定一个指针,另一个指针继续走,再次相遇时走的步数即为环的大小。
链表题(8 题)
翻转单链表
迭代法:三个指针 prev、curr、next。遍历链表,每次保存 next = curr.next,然后 curr.next = prev,更新 prev = curr,curr = next。返回 prev。时间 O(n),空间 O(1)。递归法:head.next.next = head,head.next = null,返回新头节点。
反转链表(奇偶链表)
奇偶链表:将奇数位置节点和偶数位置节点分别连接。方法:用两个指针 odd 和 even 分别 tracking 奇偶节点,odd 连向 odd.next.next,even 连向 even.next.next。最后 odd.next 连向 even 头。时间 O(n),空间 O(1)。
单向链表倒数第 k 个节点
双指针法:快指针先走 k 步,然后快慢指针同时走,快指针到达末尾时,慢指针指向倒数第 k 个节点。注意处理 k 大于链表长度的边界情况。时间 O(n),空间 O(1)。
合并两个有序链表
递归法:比较两链表头节点,较小的作为当前节点,递归处理剩余部分。迭代法:创建 dummy 节点,用指针遍历,每次取较小的节点连接到结果链表。时间 O(m+n),空间 O(1)(迭代)或 O(m+n)(递归)。
合并 K 个升序链表
方法 1:分治合并,两两合并,时间 O(n log k)。方法 2:最小堆,将 k 个头节点放入堆,每次弹出最小节点,将其下一个节点加入堆,时间 O(n log k)。方法 3:依次合并,时间 O(nk) 较差。
判断单链表有没有环(快慢指针)
快慢指针法:slow 每次走 1 步,fast 每次走 2 步。若无环,fast 会先到达 null;若有环,fast 和 slow 必然在环中相遇。时间 O(n),空间 O(1)。相遇点在环内但不一定是环入口。
删除升序链表中所有重复元素
对于重复元素全部删除(不保留)。使用 dummy 节点,curr 从 dummy 开始。若 curr.next 和 curr.next.next 值相等,记录该值并删除所有等于该值的节点;否则 curr 前进。时间 O(n),空间 O(1)。
两个链表寻找交叉节点,判断是否有交叉
方法 1:哈希表存储一个链表的所有节点,遍历另一个链表查找。方法 2:双指针,A 链表走完走 B,B 链表走完走 A,若有交点两指针必然相遇。时间 O(m+n),空间 O(1)。交点后两链表共用相同节点序列。
排序/字符串/数组题(25 题)
快速排序
选 pivot,将小于 pivot 的放左边,大于的放右边,递归处理左右子数组。三种分区方法:Lomuto(单指针)、Hoare(双指针)、三数取中。时间复杂度平均 O(nlogn),最坏 O(n²),空间 O(log n)。
堆排序
构建大顶堆,交换堆顶与末尾元素,缩小堆范围重新调整。调整函数 heapify:比较父节点与左右子节点,若子节点更大则交换并递归调整。时间 O(nlogn),空间 O(1),不稳定。
冒泡排序优化
标准冒泡:每轮相邻元素比较交换,最大元素冒泡到末尾。优化 1:加标志位,若一轮无交换则已有序,提前结束。优化 2:记录最后交换位置,其后已有序,缩小下一轮范围。时间 O(n²),最好 O(n)(已有序)。
最长递增子序列
DP:dpi 表示以 numsi 结尾的 LIS 长度,dpi = max(dpj + 1) for j < i and numsj < numsi。优化:贪心 + 二分,维护 tails 数组表示长度为 i+1 的 LIS 的最小结尾值。时间 O(n²) 或 O(n log n)。
最长无重复子串
滑动窗口:right 扩展窗口,若遇到重复字符则移动 left 直到窗口无重复。用哈希表记录字符上次出现位置,left = max(left,map.get(char) + 1)。时间 O(n),空间 O(min(m,n))。
无重复字符最长子串
同上题(170),滑动窗口经典题。维护 left, right 窗口,哈希表记录字符位置。遇到重复字符时更新 left。注意:left 只能向右移动,不能回退,取 max(left, lastPos + 1)。
字符串相加
模拟竖式加法:从右到左逐位相加,维护进位 carry。用 StringBuilder 从后往前构建结果,最后反转。处理两数长度不同的情况,短数前面补 0。时间 O(max(m,n)),空间 O(max(m,n))。
字符串相乘
模拟竖式乘法:num1i * num2j 的结果放在 resi+j+1。两层循环计算每一位的乘积并累加,最后处理进位。结果长度不超过 m+n。注意去除前导 0。时间 O(m*n),空间 O(m+n)。
最长公共前缀
方法 1:横向扫描,用第一个字符串与后续每个字符串找公共前缀。方法 2:纵向扫描,比较所有字符串的第 i 个字符。方法 3:分治或二分。时间 O(S),S 为所有字符总数。若无公共前缀返回空字符串。
字符串最长递增子串
动态规划:dpi 表示以 i 结尾的最长递增子串长度。若 si > si-1,dpi = dpi-1 + 1;否则 dpi = 1。记录最大值。时间 O(n),空间 O(n) 或 O(1)(只记录当前和最大值)。
判断 s2 中是否存在 s1 的全排列序列
滑动窗口:窗口大小固定为 s1 长度。用哈希表记录 s1 字符频次,遍历 s2 维护窗口内字符频次。若窗口内字符频次与 s1 完全相同则存在。优化:记录匹配字符种类数。时间 O(n),空间 O(1)(字符集有限)。
连续子数组的最大和
Kadane 算法:遍历数组,维护当前和 curSum。若 curSum < 0 则重置为 0(舍弃前面部分),每次更新最大值。dpi = max(numsi, dpi-1 + numsi)。时间 O(n),空间 O(1)。
数组第 K 个最大元素
方法 1:排序后取第 k 个,O(n log n)。方法 2:最小堆维护 k 个最大元素,O(n log k)。方法 3:快速选择(QuickSelect),类似快排分区,O(n) 平均,O(n²) 最坏。方法 4:BFPRT 算法,O(n) 最坏。
前 k 个最大元素(O(n) 时间复杂度)
快速选择(QuickSelect):随机选 pivot 进行分区,若 pivot 位置恰好是第 k 大则返回;若小于 k 则在右半部分查找,否则在左半部分。平均 O(n),最坏 O(n²)。可用 BFPRT 保证最坏 O(n)。
查找第 k 小的数
类似第 k 大,使用快速选择。或者用最大堆维护 k 个最小元素,遍历完成后堆顶即为第 k 小。时间 O(n log k) 或 O(n) 平均。有序数组可直接返回 numsk-1。
TopK(不可以用 PriorityQueue)
不用堆的 TopK:1)快速选择 O(n) 平均,找到第 k 大元素后遍历收集。2)计数排序/桶排序(数值范围有限时)O(n)。3)归并选择 O(nk)。根据数据特点选择,无限制时快速选择最优。
合并两个有序数组
nums1 有足够空间容纳 nums2。从后往前合并:比较 nums1m-1 和 nums2n-1,较大的放到 nums1m+n-1 位置。避免从前合并时覆盖 nums1 未处理元素。时间 O(m+n),空间 O(1)。
两个有序数组合并
归并排序的 merge 操作:双指针 i, j 分别指向两数组头部,比较 nums1i 和 nums2j,较小的加入结果,对应指针后移。处理剩余元素。时间 O(m+n),空间 O(m+n)。
二分查找
标准模板:left = 0, right = n-1,while left <= right,mid = left + (right-left)/2。若 numsmid == target 返回;若 numsmid < target 则 left = mid + 1;否则 right = mid - 1。注意防止溢出和死循环。
搜索旋转排序数组
数组旋转后至少一半有序。判断哪一半有序:若 numsmid >= numsleft,左半有序;否则右半有序。根据 target 是否在有序半区决定收缩方向。时间 O(log n),空间 O(1)。
有序数组查找某个数首次出现的位置
二分查找左边界:找到 target 后不返回,继续向右半部分收缩找更左的位置。等价于找到第一个>=target 的位置。返回时检查是否越界且 numsleft == target。时间 O(log n)。
有序数组删除重复元素
双指针:slow 指针指向不重复元素的位置,fast 遍历数组。若 numsfast != numsslow,slow++,numsslow = numsfast。返回 slow + 1 为新长度。时间 O(n),空间 O(1)。
一次遍历,将只包含 1,2,3 的数组排序输出
荷兰国旗问题:三指针法。left 指向 1 的末尾,right 指向 3 的开头,curr 遍历。若 numscurr == 1,与 left 交换,left++,curr++;若 == 3,与 right 交换,right--;若 == 2,curr++。时间 O(n),空间 O(1)。
先增后降数组去重并排序
先增后降数组(山脉数组)可视为两个有序子数组。去重:分别去重两个子数组。排序:归并两个有序子数组。或者:找到峰值后,将后半部分反转,然后对整个数组排序去重。
给定一个无序数组,返回排序后相邻元素的最大差值
桶排序思想:设 max 和 min,创建 n-1 个桶,每桶区间为 (max-min)/(n-1)。最大差值必然出现在不同桶的相邻元素间(桶内差值小于区间)。记录每桶最大最小值,遍历桶计算相邻桶差值。时间 O(n),空间 O(n)。
二维/二叉树题(12 题)
二维矩阵(从左到右递增,从上到下递增),找 target
从右上角开始搜索:若 matrixrowcol == target 返回 true;若 > target,col--(排除当前列);若 < target,row++(排除当前行)。类似 BST 搜索。时间 O(m+n),空间 O(1)。
旋转数组查找指定值
同 185 题。旋转数组至少一半有序,判断哪半有序后根据 target 范围收缩。若 numsmid >= numsleft 左半有序,检查 target 是否在 left, mid 内;否则右半有序,检查 target 是否在 mid, right 内。时间 O(log n)。
螺旋输出矩阵
模拟法:维护上下左右四个边界,按右→下→左→上顺序遍历,每遍历完一边收缩对应边界。用 visited 数组或边界控制防止重复访问。时间 O(m*n),空间 O(1)(不计结果)。
二维螺旋数组
给定 n,生成 n×n 螺旋矩阵。同 193 题逆向:维护边界,按螺旋顺序填入 1 到 n²。从 (0,0) 开始向右,遇到边界或已填位置则转向。时间 O(n²),空间 O(n²)。
对称二叉树
判断二叉树是否镜像对称。递归法:比较左子树的左节点与右子树的右节点,左子树的右节点与右子树的左节点。迭代法:用队列层次遍历,每次加入一对对称节点。时间 O(n),空间 O(n)。
二叉树的中序遍历
中序遍历:左→根→右。递归法简单。迭代法:用栈模拟,先遍历到最左节点,弹出访问,然后处理右子树。时间 O(n),空间 O(n)。Morris 遍历可实现 O(1) 空间。
二叉树的前序遍历
前序遍历:根→左→右。递归法简单。迭代法:用栈,先压右子树再压左子树,保证左子树先出栈。时间 O(n),空间 O(n)。Morris 遍历可实现 O(1) 空间。
二叉树的右视图(递归 + 非递归)
右视图:每层最右边的节点。BFS:层次遍历,每层最后一个节点加入结果。DFS:先访问右子树,记录每层第一次访问的节点(深度优先,右侧优先)。时间 O(n),空间 O(n)。
二叉树的深度/最大深度
递归:maxDepth(root) = 1 + max(maxDepth(left), maxDepth(right))。BFS:层次遍历,层数即深度。时间 O(n),空间 O(h),h 为树高。空树深度为 0。
二叉树的最小深度
从根到最近叶子节点的路径长度。注意:只有一个子树时需走该子树转身左右都空才是叶子。递归:若左右都空返回 1;若一边空返回非空边深度 +1;否则返回 min(left, right) + 1。时间 O(n)。
层次遍历二叉树/之字遍历二叉树
层次遍历:BFS,用队列每层遍历时收集节点。之字遍历(ZigZag):在层次遍历基础上,奇数层反转结果,或用双端队列奇数层从尾部加入。时间 O(n),空间 O(n)。
求二叉树高度
同 199 题(最大深度)。递归计算左右子树高度,取较大值 +1。高度定义为从根到最远叶子节点的边数或节点数(根节点高度为 0 或 1,需明确约定)。时间 O(n),空间 O(h)。
设计/手撕题(20 题)
数组实现队列(考虑扩容)
循环数组实现:front 指向队头,rear 指向队尾,size 记录元素个数。入队:rear 位置赋值,rear = (rear + 1) % capacity。出队:取 front 位置值,front = (front + 1) % capacity。扩容:当 size == capacity 时,创建 2 倍新数组,复制或重新排列元素。
两个栈实现队列
stackIn 负责入队,stackOut 负责出队。入队:push 到 stackIn。出队:若 stackOut 空,将 stackIn 全部倒入 stackOut,然后 stackOut.pop。均摊时间复杂度:入队 O(1),出队 O(1) 均摊。每个元素最多进出各栈一次。
用两个栈实现队列
同 204 题。栈 A 入队,栈 B 出队。关键:stackOut 为空时才倒数据,保证 FIFO 顺序。peek 操作同 pop 但不移除元素。
实现数据结构:添加、删除、随机获取都是 O(1)
哈希表 + 动态数组。哈希表存 value 到 index 映射,数组存实际元素。添加:数组 append,哈希表记录位置。删除:将末尾元素移到被删位置,更新哈希表,删除末尾。随机:随机生成索引返回数组元素。
实现一个线程安全的单例
饿汉式:静态实例类加载时创建,线程安全但可能浪费。懒汉式:synchronized 方法加锁,每次获取都锁,性能差。双重检查锁(DCL):synchronized 代码块 + volatile 变量,只锁第一次创建,性能和安全性兼具。
双重判定单例模式
DCL(Double-Checked Locking):第一次检查 if(instance == null),若为空进入 synchronized,第二次检查 if(instance == null),创建实例。关键在于 volatile 关键字禁止指令重排,防止返回未初始化完全的对象。
生产者消费者模式(写一个大体框架)
阻塞队列 + 线程池。生产者:queue.put(),队列满时阻塞。消费者:queue.take(),队列空时阻塞。或者用 wait/notify:生产者满了 wait,消费者通知。线程池:ExecutorService 管理线程,BlockingQueue 作为任务队列。
线程池设计
核心参数:corePoolSize(核心线程数)、maxPoolSize(最大线程数)、keepAliveTime(空闲超时)、workQueue(任务队列)、threadFactory(线程工厂)、handler(拒绝策略)。任务提交:核心线程未满创建线程;满则入队列;队列满创建非核心线程;超上限拒绝。
两个线程轮流给变量 i+1
等待通知机制:Lock + Condition,或 synchronized + wait/notify。用 flag 标记当前该哪个线程执行。线程 1:while(flag != 1) wait(),i++,flag=2,notify()。线程 2 类似。确保交替执行。
三个线程依次打印 1,2,3
三个线程 t1、t2、t3。用 ReentrantLock 和三个 Condition,或者用 volatile 变量控制顺序。t1 打印 1 后通知 t2,t2 打印 2 后通知 t3,t3 打印 3 后通知 t1。循环执行。
五个线程顺序打印 1~无穷大
类似 212 题,扩展到 5 个线程。维护一个 volatile 计数器表示当前该哪个线程打印。每个线程 while(currentThread != myId) wait(),打印后 currentThread = nextId,notifyAll()。或者用 Semaphore 5 个依次释放。
如何让三个线程按照顺序打印 ABC
同 212 题。线程 A 打印 A,线程 B 打印 B,线程 C 打印 C。用 volatile turn 变量,0 表示 A,1 表示 B,2 表示 C。每个线程 while(turn != myTurn) wait(),打印后 turn = (turn + 1) % 3,notifyAll()。
多线程分段下载文件,md5 校验
分段:计算每段起始结束位置,多个线程分别下载。合并:所有段下载完成后合并文件,或边下载边写入指定位置。MD5 校验:下载完成后计算文件 MD5,与服务器提供的 MD5 比对。用 CountDownLatch 等待所有线程完成。
写一个线程池(submit、epoll)
线程池核心:工作线程循环从任务队列取任务执行。submit:将任务封装为 FutureTask 加入队列,返回 Future。epoll 是 Linux I/O 多路复用,与线程池结合:线程池处理 accept 的连接,epoll 监听多个 socket 事件,有事件时提交任务给线程池处理。
gas 数组和 cost 数组,返回能完成比赛的起点索引(贪心)
gasi 表示第 i 站加油量,costi 表示到下一站耗油量。贪心:总 gas < 总 cost 必无解。若能从 i 走到 j 但走不到 j+1,则 i 到 j 之间任何点都无法走到 j+1,从 j+1 重新开始。遍历一次,记录总油量和当前油量,找到起点。
n 个人发糖果(贪心)
每个孩子至少一颗糖。相邻孩子中评分高的必须得到更多糖。两次遍历:左→右,若 ratingi > ratingi-1,candiesi = candiesi-1 + 1。右→左,若 ratingi > ratingi+1,candiesi = max(candiesi, candiesi+1 + 1)。求和。
给定一个字符串数组,计算不包含相同字母的最大长度乘积值
用位掩码表示每个字符串包含的字母集合。两个字符串无相同字母等价于它们的掩码按位与为 0。双重循环遍历所有对,若无重叠则计算长度乘积更新最大值。优化:对相同掩码只保留最长字符串。时间 O(n² + L),L 为总字符数。
文本左右对齐
贪心每行放最多单词。计算空格:若单词间空格数为 k,总空格为 spaces,则平均每个间隔 spaces/k 个,前 spaces%k 个间隔多加 1 个。最后一行左对齐。模拟实现:先收集每行单词,再按规则填充空格。
不使用乘除 mod 移位实现整数除法
用减法模拟除法。优化:每次减去除数的倍数(1,2,4,8...倍),类似二分的思想。将被除数和除数转为负数避免溢出,然后不断用负数减法累加商。最后根据符号决定正负。时间 O(log n)。
LRU 缓存机制
哈希表 + 双向链表。get:若 key 存在,移到链表头并返回值;否则返回 -1。put:若 key 存在更新值并移到链表头;若不存在,创建节点放链表头,若容量超限则删除链表尾节点。双向链表方便 O(1) 删除,哈希表 O(1) 查找。
Android 面试题大全8.0 - 项目·开放题·选择题(第一部分)
本篇所有问题清单(223-270)
-
项目核心技术与遇到的技术问题
-
项目动机、具体功能、UI 设计实现
-
项目中的难点、卡点
-
第三方算法和库如何想到使用
-
动态主题适配、颜色提取(Palette 库)
-
MediaPlayer 问题(缓冲时间长、播放卡顿)
-
MediaPlayer 的状态管理怎么实现
-
网络请求多线程处理
-
项目中用到的核心技术
-
项目中实现的功能解决了什么问题
-
获取图像后用 SDK 前的处理
-
自己设计图片加载工具的思路
-
流式打印实现
-
大厂代码的恶心问题及治理手段
-
业务与 SDK 开发偏好
-
是否觉得客户端能深钻的技术不多
-
对性能优化的见解
-
定时任务怎么保证一定能执行
-
软件如何实现保活
-
每天五次提醒如何确保都提醒、高延迟如何检测
-
印象最深的一个 bug
-
开发过程中有检测到内存泄漏吗,什么具体场景
-
非静态内部类为什么会导致内存泄漏
-
播放器中的动画都有哪些,通过什么方式实现
-
Activity 被销毁或重建时,MVVM 中的 ViewModel 如何保证状态不丢失
-
MVVM 体现在哪里,具体怎么划分
-
下载组件实现,大文件下载优化,多个下载任务优化
-
对 app 内存的理解,app 有哪些部分的内存
-
ANR 如何排查,原理
-
项目满意的地方
-
项目使用了什么架构,MVVM、MVP、MVC 理解
-
浏览器输入 URL 到页面显示完整流程
-
如果主线程一定要执行耗时逻辑如何不 ANR
-
本地广播原理、如何只发送给特定 App
-
如何设计缓存(淘汰策略 LRU/LFU)
-
如何监听手机拍照和截屏、图片隐写
-
JSON 解析器实现(词法分析、语法分析、数据结构)
-
Java 中哪些场景需要重写 hashCode 和 equals
-
没有用 volatile 会发生什么
-
线程与协程的上限区别
-
APK 中多个进程退出顺序
-
大图加载压缩方式
-
断点续传实现
-
文件上传
-
签名作用
-
代码/无用资源检测
-
热修复方案原理及优缺点
-
线上收集 OOM 和内存泄漏
一、项目相关问题(223-253)
223. 项目核心技术与遇到的技术问题
- 项目中使用的核心技术栈有哪些?
- 在开发过程中遇到了哪些主要技术问题?
- 这些问题是如何定位和解决的?
224. 项目动机、具体功能、UI 设计实现
- 项目的初衷和目标是什么?
- 实现了哪些具体功能?
- UI 设计是如何实现的?用到了哪些技术?
225. 项目中的难点、卡点
- 项目中遇到的最大难点是什么?
- 技术卡点在哪里?如何突破的?
226. 第三方算法和库如何想到使用
- 项目中使用了哪些第三方算法和库?
- 是如何选型和决定使用它们的?
227. 动态主题适配、颜色提取(Palette 库)
- 如何实现动态主题适配?
- Palette 库的使用场景和实现原理?
228. MediaPlayer 问题(缓冲时间长、播放卡顿)
- MediaPlayer 缓冲时间长的原因是什么?
- 播放卡顿的问题如何排查和解决?
229. MediaPlayer 的状态管理怎么实现
- MediaPlayer 有哪些状态?
- 状态之间如何转换?
- 如何避免状态错误导致的异常?
230. 网络请求多线程处理
- 多线程网络请求如何处理?
- 线程池如何配置和管理?
231. 项目中用到的核心技术
- 项目核心技术栈总结
- 为什么选择这些技术?
232. 项目中实现的功能解决了什么问题
- 功能需求是什么?
- 最终方案解决了什么痛点?
233. 获取图像后用 SDK 前的处理
- 获取图像后需要做哪些预处理工作?
- 图像格式、分辨率如何处理?
234. 自己设计图片加载工具的思路
- 图片加载的核心流程是什么?
- 缓存策略如何设计?
- 内存和磁盘缓存如何实现?
235. 流式打印实现
- 流式打印的应用场景
- 实现思路和关键技术
236. 代码的恶心问题及治理手段
- 常见的代码问题有哪些?
- 代码治理的手段和方法?
237. 业务与 SDK 开发偏好
- 业务开发和 SDK 开发的差异?
- 个人偏好和原因?
238. 是否觉得客户端能深钻的技术不多
- 你的观点是什么?
- 客户端技术的深度体现在哪里?
239. 对性能优化的见解
- 性能优化的核心思路?
- 常见的性能优化手段有哪些?
240. 定时任务怎么保证一定能执行
- 定时任务的实现方式?
- 如何保证任务不丢失?
241. 软件如何实现保活
- 应用保活的常见方案?
- 各方案的优缺点?
242. 每天五次提醒如何确保都提醒、高延迟如何检测
- 多次提醒的实现方案?
- 高延迟检测和处理机制?
243. 印象最深的一个 bug
- bug 的现象和根因?
- 排查过程和解决方法?
244. 开发过程中有检测到内存泄漏吗,什么具体场景
- 内存泄漏的具体场景?
- 如何检测和定位的?
245. 非静态内部类为什么会导致内存泄漏
- 非静态内部类的引用机制?
- 持有什么隐含引用?
- 如何避免?
246. 播放器中的动画都有哪些,通过什么方式实现
- 常见动画类型?
- 动画实现方式(ValueAnimator、ObjectAnimator 等)?
247. Activity 被销毁或重建时,MVVM 中的 ViewModel 如何保证状态不丢失
- ViewModel 的生命周期?
- onRetainNonConfigurationInstance 机制?
248. MVVM 体现在哪里,具体怎么划分
- MVVM 在项目中的体现?
- Model、View、ViewModel 如何划分?
249. 下载组件实现,大文件下载优化,多个下载任务优化
- 下载组件的核心功能?
- 大文件下载的优化策略?
- 多任务并发和队列管理?
250. 对 app 内存的理解,app 有哪些部分的内存
- App 内存的组成部分?
- Java 堆、Native 堆、图形内存等?
251. ANR 如何排查,原理
- ANR 的定义和类型?
- 排查工具和方法?
- ANR 的原理?
252. 项目满意的地方
- 项目中你觉得做得好的地方?
253. 项目使用了什么架构,MVVM、MVP、MVC 理解
- 项目使用的架构?
- 对 MVVM、MVP、MVC 的理解和对比?
二、开放题/场景题(254-270)
254. 浏览器输入 URL 到页面显示完整流程
- DNS 解析
- TCP 连接建立
- HTTP 请求发送
- 服务器响应
- 浏览器渲染
- 请详细描述每个阶段的细节
255. 如果主线程一定要执行耗时逻辑如何不 ANR
- 可行的方案和思路?
- 伪异步、分片执行等方法?
256. 本地广播原理、如何只发送给特定 App
- 本地广播的实现原理?
- 如何限制广播接收范围?
257. 如何设计缓存(淘汰策略 LRU/LFU)
- 缓存的核心数据结构?
- LRU 和 LFU 的实现原理?
- 如何选择淘汰策略?
258. 如何监听手机拍照和截屏、图片隐写
- 拍照监听的实现方式?
- 截屏监听的方案?
- 图片隐写的原理?
259. JSON 解析器实现(词法分析、语法分析、数据结构)
- JSON 解析的基本原理?
- 词法分析和语法分析的过程?
- 数据结构如何设计?
260. Java 中哪些场景需要重写 hashCode 和 equals
- 为什么需要重写?
- 常见场景:HashMap、HashSet 等?
- 重写的原则?
261. 没有用 volatile 会发生什么
- volatile 的作用?
- 不使用的后果:可见性、有序性问题?
262. 线程与协程的上限区别
- 线程和协程的本质区别?
- 数量上限的差异及原因?
263. APK 中多个进程退出顺序
- 多进程 APK 的进程管理?
- 退出顺序和依赖关系?
264. 大图加载压缩方式
- 大图加载的问题?
- 压缩策略:inSampleSize、质量压缩等?
265. 断点续传实现
- 断点续传的原理?
- 如何记录下载进度?
- Range 头的使用?
266. 文件上传
- 文件上传的实现方式?
- 分片上传、进度监听?
267. 签名作用
- APK 签名的作用?
- 签名验证的过程?
268. 代码/无用资源检测
- 无用代码检测工具?
- 无用资源检测工具?
269. 热修复方案原理及优缺点
- 常见热修复方案(Tinker、Sophix 等)?
- 原理和优缺点对比?
270. 线上收集 OOM 和内存泄漏
- OOM 收集方案?
- 内存泄漏检测方案?
- 使用的工具和平台?
注:本文为第一部分,包含题目 223-270。第二部分包含题目 271-309。
Android 面试题9.0 - 项目·开放题·选择题(第二部分)
本篇所有问题清单(271-309)
-
某公司技术氛围、性能优化见解
-
ANR 是什么,产生原因,有几种,如何检测和避免
-
如何让三个线程按照顺序打印 ABC
-
设计一个离线缓存的播放器应用架构
-
如何设计一个相册(九宫格样式)
-
高性能图片缓存系统设计
-
手机截长屏图像如何拼接
-
场景:a、b 独立,c 依赖 a、b 的数据
-
场景:实现一个线程安全的方法
-
场景:实现一个根据优先级对子 View 进行测量的自定义 ViewGroup
-
场景:列表 LiveData 绑定 ViewHolder 注意事项
-
场景:崩溃问题只在部分机型出现,怎么排查
-
场景:活动从后台切前台,生命周期
-
场景:屏幕翻转记录日期等参数保存到 SQLite
-
场景:实现四则运算计算器 APP
-
哪种组件承载用户的交互工作(Activity)
-
布局文件一般用什么格式的文件来写的(XML)
-
哪种线程属于非 UI 线程
-
Activity 可以用什么来启动(Intent)
-
异步加载可以用什么方法(协程/线程池/WorkManager,AsyncTask 已废弃)
-
水平居中的布局应该使用什么属性设置
-
AndroidManifest 文件可以用来声明各种属性和配置
-
Activity 创建菜单的方法
-
Fragment 创建视图的方法
-
Handler 一般不用于什么(长时间的后台工作)
-
常见布局有哪些
-
异步任务的实现方式
-
文件持久化的方法有哪些
-
四大组件有哪些可以响应广播
-
多线程的创建方式
-
安卓应用的/res/文件夹下可以放什么东西
-
AndroidManifest 文件可以声明哪些权限
-
安卓应用的性能分析工具有哪些
-
Gradle 工具的作用
-
Kotlin 语言的特性
-
讲一讲 Java 的 GC 机制
-
Java 的内存泄露是什么情况,如何避免
-
安卓系统的 IPC 机制
-
ANR 是什么情况,如何检测和避免
二、开放题/场景题(续)(271-285)
271. 某公司技术氛围、性能优化见解
- 技术氛围如何?
- 对性能优化的理解和实践?
272. ANR 是什么,产生原因,有几种,如何检测和避免
- ANR 的定义
- 产生原因
- ANR 的几种类型
- 检测方法
- 避免方案
273. 如何让三个线程按照顺序打印 ABC
- 线程同步的方案
- 使用 wait/notify、Semaphore、CountDownLatch 等
274. 设计一个离线缓存的播放器应用架构
- 整体架构设计
- 缓存管理
- 播放控制
- 数据持久化
275. 如何设计一个相册(九宫格样式)
- 界面布局
- 图片加载和缓存
- 滑动和交互
276. 高性能图片缓存系统设计
- 内存缓存(LruCache)
- 磁盘缓存
- 图片压缩和复用
- 异步加载
277. 手机截长屏图像如何拼接
- 长屏图像的处理流程
- 图像拼接算法
- 边缘融合处理
278. 场景:a、b 独立,c 依赖 a、b 的数据
- 并发执行 a 和 b
- 等待 a 和 b 完成后执行 c
- 使用 Future、CompletableFuture 等
279. 场景:实现一个线程安全的方法
- 线程安全的概念
- 实现方案:synchronized、Lock、原子类等
280. 场景:实现一个根据优先级对子 View 进行测量的自定义 ViewGroup
- 自定义 ViewGroup 的流程
- 优先级排序
- onMeasure 实现
281. 场景:列表 LiveData 绑定 ViewHolder 注意事项
- LiveData 的生命周期感知
- 避免重复注册观察者
- 数据更新处理
282. 场景:崩溃问题只在部分机型出现,怎么排查
- 收集崩溃信息
- 复现和定位
- 机型差异分析
- 灰度修复方案
283. 场景:活动从后台切前台,生命周期
- Activity 生命周期变化
- onRestart、onStart、onResume 的调用顺序
284. 场景:屏幕翻转记录日期等参数保存到 SQLite
- 配置变化处理
- 状态保存和恢复
- SQLite 数据存储
285. 场景:实现四则运算计算器 APP
- 界面设计
- 表达式解析
- 计算逻辑
- 边界情况处理
三、选择题/基础测试
单选题(286-295)
286. 哪种组件承载用户的交互工作?
- A. Service
- B. Activity ✓
- C. ContentProvider
- D. Broadcast Receiver
287. 布局文件一般用什么格式的文件来写的?
- A. JSON
- B. XML ✓
- C. YAML
- D. HTML
288. 哪种线程属于非 UI 线程?
- A. Main Thread
- B. Worker Thread ✓
- C. UI Thread
- D. Render Thread
289. Activity 可以用什么来启动?
- A. Bundle
- B. Intent ✓
- C. Context
- D. Component
290. 异步加载可以用什么方法?
- A. AsyncTask(已废弃)
- B. 协程/线程池/WorkManager ✓
- C. 只能在主线程执行
- D. 以上都不是
291. 水平居中的布局应该使用什么属性设置?
- A. layout_gravity="center_horizontal"
- B. gravity="center_horizontal"
- C. 以上都可以 ✓
- D. 以上都不对
292. AndroidManifest 文件可以用来声明各种属性和配置
- A. 正确 ✓
- B. 错误
293. Activity 创建菜单的方法
- A. onCreate()
- B. onCreateOptionsMenu() ✓
- C. onPrepareOptionsMenu()
- D. onOptionsItemSelected()
294. Fragment 创建视图的方法
- A. onCreate()
- B. onCreateView() ✓
- C. onViewCreated()
- D. onActivityCreated()
295. Handler 一般不用于什么?
- A. 线程间通信
- B. 延迟执行任务
- C. 长时间的后台工作 ✓
- D. 消息处理
多选题(296-305)
296. 常见布局有哪些?
- A. LinearLayout ✓
- B. RelativeLayout ✓
- C. ConstraintLayout ✓
- D. FrameLayout ✓
- E. GridLayout ✓
297. 异步任务的实现方式?
- A. Thread ✓
- B. AsyncTask(已废弃)✓
- C. 协程 ✓
- D. 线程池 ✓
- E. WorkManager ✓
298. 文件持久化的方法有哪些?
- A. SharedPreferences ✓
- B. 文件存储 ✓
- C. SQLite 数据库 ✓
- D. ContentProvider ✓
- E. 网络存储 ✓
299. 四大组件有哪些可以响应广播?
- A. Activity ✓
- B. Service ✓
- C. BroadcastReceiver ✓
- D. ContentProvider ✓
300. 多线程的创建方式?
- A. 继承 Thread 类 ✓
- B. 实现 Runnable 接口 ✓
- C. 实现 Callable 接口 ✓
- D. 使用线程池 ✓
- E. 使用协程 ✓
301. 安卓应用的/res/文件夹下可以放什么东西?
- A. 布局文件(layout)✓
- B. 图片资源(drawable/mipmap)✓
- C. 字符串资源(values/strings.xml)✓
- D. 样式资源(values/styles.xml)✓
- E. 原始文件(raw)✓
302. AndroidManifest 文件可以声明哪些权限?
- A. 网络权限 ✓
- B. 存储权限 ✓
- C. 相机权限 ✓
- D. 位置权限 ✓
- E. 联系人权限 ✓
303. 安卓应用的性能分析工具有哪些?
- A. Android Profiler ✓
- B. LeakCanary ✓
- C. StrictMode ✓
- D. Systrace ✓
- E. Perfetto ✓
304. Gradle 工具的作用?
- A. 项目构建 ✓
- B. 依赖管理 ✓
- C. 任务调度 ✓
- D. 多渠道打包 ✓
- E. 代码混淆 ✓
305. Kotlin 语言的特性?
- A. 空安全 ✓
- B. 协程支持 ✓
- C. 扩展函数 ✓
- D. 数据类 ✓
- E. 与 Java 互操作 ✓
简答题(306-309)
306. 讲一讲 Java 的 GC 机制
- GC 的基本原理
- 常见的垃圾回收算法(标记 - 清除、标记 - 复制、标记 - 整理)
- JVM 的分代模型(年轻代、老年代、元空间)
- 常见的 GC 收集器
307. Java 的内存泄露是什么情况,如何避免
- 内存泄露的定义
- 常见场景:静态集合、非静态内部类、资源未关闭等
- 检测工具:MAT、LeakCanary
- 避免方法
308. 安卓系统的 IPC 机制
- Binder 机制
- AIDL
- Messenger
- ContentProvider
- 文件共享
309. ANR 是什么情况,如何检测和避免
- ANR 的定义和类型
- 检测方法:trace 文件、StrictMode、ANR Watchdog
- 避免方案:避免主线程耗时操作、合理使用异步
注:本文为第二部分,包含题目 271-309。第一部分包含题目 223-270。