安卓面试题

本文内容由ai整理,答案谨慎辨别与相信。

从牛客网上搜刮了最近1、2年面试题,大部分是校招题,可能偏基础,因为我基础不好,所以这一篇整理的基础,偏八股文比较多。感兴趣的也可以看另外一篇文章,另外一篇是去年社招面试被问到的凉经。

面试题kkk-CSDN博客文章浏览阅读786次,点赞13次,收藏18次。继承自View或ViewGroup、style、attr、https://blog.csdn.net/mix39/article/details/155139927?spm=1001.2014.3001.5501安卓面试最常问的:

  1. 安卓基础:(几乎必问)启动流程、handler、事件方法与滑动冲突、自定义view、binder机制&aidl、activity、fragment、四大组件。(可能会问到)window、数据库、retrofit、协程、四大组件、hook、recyclerview、glide、多点触控、拖拽

  2. 数据结构:常用数据结构,java类。hashmap(几乎必问)、链表

  3. 面向对象、设计原则、设计模式与架构设计:

  4. jvm:内存模型、gc算法、弱引用、虚引用、类加载机制与实际应用场景

  5. 性能优化、检测、埋点:内存抖动/泄露原理检测、线上检测、启动优化、埋点链路、保活

  6. 基础:进程、线程、多线程、线程池、git、java异常机制

  7. 其他:跨端组件、音视频、多媒体、平行视界、图片压缩算法、音频焦点(因为我是有点这个基础和相关业务的,所以会问一些,大部分会根据之前项目经历来问)

  8. 项目: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_TOPFLAG_ACTIVITY_NEW_TASK

1.3 Activity 生命周期

正常生命周期onCreate → onStart → onResume → onPause → onStop → onDestroy

A跳转BA.onPause → B.onCreate → B.onStart → B.onResume → A.onStop

A跳转透明BA.onPause → B.onCreate → B.onStart → B.onResume(A不执行onStop,因为仍部分可见)

按返回键B返回AB.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 额外有 onAttachonCreateViewonActivityCreatedonDestroyViewonDetach
  • 关系:一对多,一个 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

核心方法:onCreatequeryinsertupdatedeletegetType。用于跨进程数据共享,底层基于 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(设为正方形)+ onDrawcanvas.drawCircle),需重写 onMeasureonDraw

自定义时间轴:涉及缩放(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

drawCircledrawRectdrawLinedrawPathdrawBitmapdrawTextdrawArcclipRectclipPathsave/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
  • 鸿蒙 UIAbilityonCreate → 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_DOWNACTION_MOVEACTION_UPACTION_CANCEL

常用属性:getX/getY(相对 View)、getRawX/getRawY(相对屏幕)、getPointerIdgetPressuregetSizegetEventTime

3.3 滑动冲突

|-----------|--------------------------------------------------------------------|-------------|
| 方案 | 做法 | 适用场景 |
| 外部拦截法 | 父 View 的 onInterceptTouchEvent 中判断 | 父View 需要控制时 |
| 内部拦截法 | 子 View dispatchTouchEventrequestDisallowInterceptTouchEvent | 子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 耗时操作
  • 共用 RecyclerViewRecycledViewPool

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,或者 onDestroyremoveCallbacksAndMessages(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(延迟时间)排序,postpostDelay 都是按时间顺序插入
  • 停止子线程 Looperquit()(立即移除所有消息)、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。

真正加入 ActivityonActivityCreated 之后

6.2 Fragment 常见问题

  • 生命周期错乱:异步回调时 Fragment 可能已 detach,需判空
  • 重复请求onCreateView 可能多次调用,VM + 缓存策略
  • 重复订阅 :使用 viewLifecycleOwner 而非 this 作为 LiveData observer

6.3 Fragment 交互

  • Fragment → Activity :接口回调或 getActivity()
  • Activity → Fragmentfragment.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 转 pxpx = dp * densitydensity = dpi / 160
  • 适配方案:smallestWidth 限定符、今日头条方案(修改 density)、ConstraintLayout 百分比
  • 半屏适配 :ConstraintLayout 的 percentWidthGuideline
  • 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 三次握手

  1. 客户端 → SYN → 服务端
  1. 服务端 → SYN+ACK → 客户端
  1. 客户端 → ACK → 服务端

为什么三次:防止已失效的连接请求到达服务端导致资源浪费。两次无法确认客户端接收能力。

第一次握手失败:客户端超时重传 SYN(指数退避,默认5次)

SYN 重传序列号:不同,每次 SYN 的序列号随机生成

第一个带数据的包:第三次握手的 ACK 可以携带数据

8.3 TCP 四次挥手

  1. 主动方 → FIN → 被动方
  1. 被动方 → ACK → 主动方
  1. 被动方 → FIN → 主动方
  1. 主动方 → 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

生产者消费者BlockingQueuewait()/notify()

读写场景 :读多写少用 CopyOnWriteArrayListReentrantReadWriteLock;写多用 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·网络·框架

去重整理版,含答案。保留原始问题格式。

本篇所有问题

  1. JVM 内存结构(堆栈方法区/元空间程序计数器本地方法栈)?
  2. JDK1.7 和 1.8 的内存模型变化?
  3. Java 内存区域?
  4. Java 并发模型?
  5. 哪些区域线程共享哪些是线程私有的?
  6. 堆存放什么?
  7. 堆和栈分别用来存什么?
  8. 栈帧结构?
  9. 方法调用时栈空间变化?
  10. 方法返回值保存在哪里(操作数栈)?
  11. 程序地址空间?
  12. 堆区和栈区作用?
  13. 物理内存和虚拟内存区别?
  14. 32 位 CPU 架构对进程的虚拟内存是多大?
  15. 内存管理?
  16. 垃圾回收算法(标记清除标记复制标记整理分代收集)?
  17. 垃圾回收器分类并行收集器原理?
  18. CMS/G1?
  19. 分代回收(新生代/老年代)思想为什么要这么划分?
  20. GC 什么时候发生?
  21. GC Root 有哪些?
  22. 安卓中哪些可以作为 GC Root?
  23. 如何判断一个对象是垃圾对象?
  24. 可达性分析两个变量互相引用会不会被 GC?
  25. 如何确保所有对象被穷举(安全点安全区域)?
  26. 引用计数和可达性分析区别 Java 使用哪种?
  27. 强/软/弱/虚引用区别与使用场景?
  28. Java 引用类型和 C++ 引用有什么区别?
  29. Java 引用类型在实际开发中有哪些应用场景?
  30. 图片缓存用什么引用?
  31. 弱引用如何恢复?
  32. 内存泄漏定义常见场景?
  33. 安卓常见的内存泄漏场景?
  34. 从内存泄漏角度静态内部类和匿名内部类有什么区别?
  35. 匿名内部类声明为静态的可以持有外部类吗为什么?
  36. 内存泄漏怎么排查?
  37. native 内存泄漏第三方 SDK 泄漏没代码怎么办?
  38. 内存泄漏工具怎么使用?
  39. 如何定位 Android 中的内存泄漏具体操作流程?
  40. Android Studio 内置的 Android Profiler 如何操作?
  41. 定位内存泄漏打印快照时机如何选择?
  42. 内存占用的分析中的火焰图怎么看?
  43. LeakCanary 原理检测机制?
  44. LeakCanary 如何实现应用启动后自动初始化?
  45. LeakCanary 检测出来什么类型的内存泄漏?
  46. 真正检测的对象是哪个?
  47. 怎么判断是哪一种情况导致的内存泄漏?
  48. 从检测到内存泄漏到弹出提示引导修复链路如何实现?
  49. 什么时候触发内存泄漏检测?
  50. LeakCanary 监控什么安卓组件?
  51. 监控 ViewModel 怎么注册的?
  52. 场景 Activity 创建 Handler 并 post 消息 Activity 销毁但消息仍在队列中 LeakCanary 能检测到吗?
  53. 已发送的 message 无法被 remove 怎么处理?
  54. LeakCanary 告诉内存泄漏开发者如何验证排查是否真实泄漏?
  55. 如何避免内存泄漏开发过程中?
  56. 内存峰值降低 18% 有没有分析是哪类对象的优化?
  57. 类加载机制(加载→验证→准备→解析→初始化)?
  58. ClassLoader 的整体架构和理解?
  59. ClassLoader 在安卓里的应用场景?
  60. 双亲委派机制(是组合不是继承)好处?
  61. 如何打破双亲委派机制?
  62. 利用 ClassLoader 怎么实现热修复原理是什么?
  63. 热修复方案?
  64. 如何自定义类加载器生产环境用途?
  65. 如果自定义一个 String 类会怎么加载能编译成功吗?
  66. PathClassLoader 与 DexClassLoader 区别?
  67. 假如有个 jar 包不能被 maven 下载到本地 library 应该怎么办?
  68. new String() 创建几个对象?
  69. JVM 怎么保证一个类只加载一次?
  70. 类加载时对静态成员变量和非静态成员变量处理有什么不同?
  71. 一个 static 变量更新值另一个进程能获取最新值吗另一个线程呢?
  72. 静态变量存放在哪里?局部变量存放在哪里?
  73. 启动一个 Activity 设置 TextView 文字标题什么存在堆中什么存在栈中?
  74. JMM 的意义?
  75. 为什么 Java 会有线程不安全(并发)底层原因(OS)?
  76. Kotlin 与 Java 区别?
  77. Kotlin 相比 Java 有哪些独有的好用优势?
  78. Kotlin 语法特性?
  79. Kotlin 怎么保证空安全?
  80. Kotlin 空异常检查是编译时还是运行时?
  81. 看过 Kotlin 的书吗?
  82. Kotlin 中===是什么作用?
  83. java 调用 kotlin 可能出现什么问题?
  84. by lazy 原理会不会有多线程问题?
  85. by lazy 和 lateinit 的区别?
  86. 内联函数原理和优点?
  87. 密封类(sealed class)是什么优势是什么?
  88. 高阶函数是什么?
  89. 扩展函数怎么实现的(编译时转化为静态函数)?
  90. 协程与线程区别?协程为什么轻量?节省的内存在哪里?
  91. 协程是什么有什么用?
  92. 协程挂起是怎么实现的本质是什么(异步回调)?
  93. 协程调度实现为什么不需独立线程?
  94. 平时是怎么用协程的?
  95. 启动协程的几种方式?
  96. 协程怎么取消?
  97. 协程里 Job 存在的意义是什么?
  98. 在项目里用协程做什么了?
  99. 协程内联函数?
  100. DSL 理解(高阶函数 + 带接收者的 Lambda)?
  101. let also apply run 区别?
  102. Kotlin 非空参数在 Java 调用时是否安全(不安全抛 IllegalArgumentException)?
  103. KAPT 与 KSP 区别?
  104. Kotlin 使用过哪些单例?
  105. lazy?
  106. TCP 与 UDP 区别?安卓开发中的使用场景?
  107. TCP 三次握手(为什么是三次?两次不行吗)?
  108. TCP 第一次握手失败后客户端的重试策略?
  109. TCP 第一个带数据的包是第几个包(第三个)?
  110. TCP 四次挥手(为什么是四次)?
  111. 三次握手服务端返回的 ACK 报文丢失客户端和服务端分别会发生什么?
  112. 重传的 SYN 报文的序列号和之前一样吗(不一样)?
  113. TCP 滑动窗口机制窗口大小动态变化(接收方缓冲区)两端窗口大小一样吗?
  114. TCP 流量控制机制?
  115. TCP 拥塞控制(慢启动拥塞避免快重传快恢复)?
  116. TCP 超时重传算法超时时间设置?
  117. TCP 首部 20 个字节包含哪些内容?
  118. TCP 如何保证可靠传输?
  119. 服务端用 80 端口接受多个连接如何区分不同连接(四元组)?
  120. 什么时候用 UDP?游戏为什么适合 UDP?网络差时 UDP 优势还能发挥吗?
  121. 长连接短连接区别?怎么保持长连接?
  122. HTTP 报文结构?请求头包含内容?请求首部?
  123. HTTP1.0/1.1/2.0 区别?报文头部有什么区别?
  124. HTTP2.0 做了什么优化?
  125. HTTP2.0 最大改进?
  126. HTTP2.0 基于什么协议?HTTP3.0 基于 UDP 的好处?
  127. HTTP 中的同步和异步?
  128. HTTP 是什么协议?
  129. 应用里用了什么协议(GET POST PUT)?
  130. GET 和 POST 区别?实际使用中参数有何不同?
  131. GET 查询数据怎么携带?POST 怎么携带?
  132. POST 的 body 不可见解释为什么?
  133. 四种 HTTP 请求区别和使用场景?
  134. HTTPS 与 HTTP 区别?
  135. HTTPS 为什么更安全?
  136. HTTPS 如何建立连接?
  137. HTTPS 原理加密算法和整体流程?
  138. 对称加密和非对称加密 HTTPS 什么时候用对称什么时候用非对称?
  139. 对称加密和非对称加密加密解密流程?
  140. 常见对称加密和非对称加密算法?
  141. CA 证书加解密过程?
  142. 证书在 HTTPS 中起的作用?
  143. 如何校验证书有效性(如访问百度时)?
  144. HTTPS 能否完全防御中间人攻击?
  145. 抓包了解吗?
  146. WebSocket 协议的理解?
  147. WebSocket 原理与 Socket 区别?好处?
  148. 表示层和会话层功能?
  149. IP 层负责什么?TCP 负责什么?
  150. 浏览器输入 URL 到页面渲染的完整过程?
  151. 网络请求用哪个第三方库?看过源码吗?
  152. 网络错误情况怎么处理?
  153. 打开浏览器白屏可能是什么情况(不是 403 404)?
  154. 网络请求如何完成(结合项目)?
  155. 通信协议了解哪些(HTTP HTTPS MQTT)?
  156. HTTP 属于 TCP/IP 协议哪一层?底层用哪种传输协议?
  157. TCP 属于哪一层?
  158. 五层网络模型?
  159. 网络七层结构?
  160. 传输层和网络层能合并吗?
  161. Socket 位于哪一层?
  162. 网络如何确保安全?如何确保秘钥正确性?
  163. IPv4 和 IPv6 区别?
  164. HTTP 实体压缩?有针对头部的压缩吗?
  165. 差量更新/下载断点续传?
  166. OkHttp 源码拦截器链(责任链模式)?
  167. 责任链好处是什么?
  168. OkHttp DNS 注入处理?
  169. OkHttp 设计模式有哪些?
  170. OkHttp 底层原理(连接复用?缓存)?缓存用哪种数据结构存储?
  171. OkHttp 请求有同步和异步两种有什么区别?
  172. Retrofit 底层动态代理原理?为什么接口不用实现?
  173. Retrofit 的核心原理?
  174. Retrofit 与 OkHttp 区别?
  175. Retrofit 用到了哪些设计模式(动态代理工厂适配器建造者)?
  176. Retrofit 如何通过注解实现网络请求?
  177. 两个接口会生成不同的实例对象吗?
  178. 动态代理优势?
  179. Retrofit 发送请求如何记录日志(拦截器)?
  180. Glide 源码整体运作流程?
  181. Glide 工作原理?
  182. Glide 比其他的优势?
  183. Glide 三级缓存 LRU 算法?
  184. Glide 缓存原理?
  185. Glide 图片缓存 key 生成规则(url 图片签名等)?
  186. 在客户端如何实现 Glide 图片缓存?
  187. 如何判断下次请求 Glide 是不是需要更新?
  188. 排行榜头像一直在更新如何实现缓存?
  189. Glide 一次请求多少个图片?
  190. Glide 加载网络资源实际请求方法(HEAD 非 GET)?
  191. Glide 如何绑定生命周期?Activity 和 Fragment 生命周期在 Glide 里有什么区别?
  192. Glide 与其他图片框架优劣比较?
  193. Glide 处理大图加载的逻辑?
  194. Glide 在项目中用到了什么地方?
  195. 如何解决图片加载的错位问题?
  196. 图片加载用过哪些库?Glide 还用过什么其他功能?
  197. EventBus 原理优劣使用场景?
  198. EventBus 手写实现(单例 + 观察者模式)?
  199. LeakCanary 原理检测机制?
  200. LeakCanary 如何实现应用启动后自动初始化?
  201. Room 与 SQLite 区别?Room 原理?
  202. Flow 使用场景?
  203. LiveData 与普通的观察者模式有什么区别?
  204. 处于后台时多次更新 LiveData 的 value 会触发 onChange 吗?
  205. 如果想在处于后台时每次更新 value 都触发更新怎么做?
  206. LiveData 如何与多个 ViewModel 共享数据?
  207. LiveData 数据更新通知 UI 的核心链路?
  208. LiveData 生命周期感知原理?
  209. LiveData 的版本号是干啥的?为什么需要版本号?
  210. LiveData 发相同数据会通知观察者吗?
  211. LiveData 的 setValue 和 postValue 区别?
  212. 子线程更新 LiveData 数据涉及过吗?
  213. ViewModel 如何应对屏幕旋转?
  214. ViewModel 的持久化是怎么实现的?
  215. ViewModel 为什么比 Activity 生命周期长?Activity 销毁后 ViewModel 怎么保证不销毁?
  216. 正常的退出和销毁重建是怎么区分的?
  217. ViewModel 中的 safe state handle 是干嘛的?
  218. 协程+ViewModel 实现?
  219. Litepal 与其他 ORM 对比?
  220. RxJava(网络请求后展示到 UI 十个请求全部完成打印输出线程切换)?
  221. 总线类的框架用过吗?

十二、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 中的内存泄漏具体操作流程?

  1. 复现场景,使用 Profiler 监控内存
  2. 触发 GC,观察内存是否下降
  3. Dump heap 文件用 MAT 分析
  4. 查找直方图 Top 类实例异常增长
  5. 查看 GC Roots 追踪引用链

问:Android Studio 内置的 Android Profiler 如何操作?

  1. 运行 App 连接 Profiler
  2. Memory 面板观察内存曲线
  3. 点击 Dump Java Heap 生成快照
  4. 分析 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 三次握手(为什么是三次?两次不行吗)?

  1. Client→Server:SYN
  2. Server→Client:SYN+ACK
  3. Client→Server:ACK
    两次不行:防止已失效连接请求突然传到服务端产生错误连接。

问:TCP 第一次握手失败后客户端的重试策略?

指数退避重传 SYN,默认 1s、2s、4s、8s...重传多次后放弃。

问:TCP 第一个带数据的包是第几个包(第三个)?

第三次握手开始可携带数据。

问:TCP 四次挥手(为什么是四次)?

  1. Client→Server:FIN
  2. Server→Client:ACK
  3. Server→Client:FIN
  4. 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 如何建立连接?

  1. ClientHello(加密套件、随机数)
  2. ServerHello+ 证书
  3. 客户端验证证书
  4. 生成 session key(非对称加密传输)
  5. 切换到对称加密通信

问: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 到页面渲染的完整过程?

  1. DNS 解析
  2. TCP 三次握手
  3. HTTP 请求
  4. 服务器响应
  5. 浏览器解析 HTML/CSS/JS
  6. 构建 DOM/CSSOM、渲染树
  7. Layout、Paint、Composite
  8. 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 源码整体运作流程?

  1. with(context) 获取 DrawableTypeRequest
  2. load(url) 创建 RequestBuilder
  3. into(imageView) 构建 Target
  4. Engine 缓存查询
  5. 加载资源(网络/磁盘/内存)
  6. 解码 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 - 性能优化

本篇所有问题

  1. APK 瘦身(资源压缩、混淆、WebP、移除无用资源)
  2. 包大小优化措施,提升了哪些性能
  3. 无用资源/代码线上判断(类加载机制)
  4. 代码层面瘦身优化
  5. 内存优化
  6. 内存泄漏排查(MAT、LeakCanary)
  7. OOM 线上收集方案
  8. 卡顿优化(Systrace、Perfetto、BlockCanary)
  9. 如何监听 UI 卡顿
  10. 线程绘制的时长
  11. 卡顿排查工具(Profiler、Perfetto)
  12. UI 卡顿原因(掉帧)
  13. 怎么排查掉帧问题,原因有哪些
  14. 页面跳转慢、页面崩溃有遇到过吗
  15. 假如程序同一时间段内触发了很多次重绘方法,安卓系统底层如何防止卡顿问题
  16. 布局优化(include、merge、ViewStub)
  17. 性能优化理念(靠数据说话)
  18. 大图加载(10MB 处理方案),Bitmap 池是什么
  19. Bitmap 如何优化
  20. 首页冷启动首屏渲染慢问题排查
  21. 过渡绘制防止
  22. App 瘦身有哪些方式
  23. 项目中的性能优化
  24. 内存峰值降低是如何实现的,优化了什么
  25. 热点封面的缓存是如何实现的
  26. 动态换肤的使用场景和遇到的问题
  27. MVVM 除了数据视图绑定,还有其他使用场景吗
  28. LeakCanary 和 AndroidProfile 的具体使用场景和方法
  29. 软件绘制了解吗
  30. Vsync、SurfaceFlinger、OpenGL 了解吗
  31. 启动优化
  32. 项目上线后有监听内存泄漏的机制吗

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 导致线程阻塞

怎么排查掉帧问题,原因有哪些

答: 掉帧排查步骤:

  1. 使用 Systrace 抓取:定位耗时超过 16ms 的帧
  2. 分析 CPU 时间轴:查看主线程、渲染线程的耗时分布
  3. 检查 RenderThread:是否有长时间 GPU 命令提交
  4. 查看 GC 事件:GC Pause 是否导致帧丢失
  5. 分析 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,体积更小

首页冷启动首屏渲染慢问题排查

答: 冷启动慢排查思路:

  1. Systrace 分析:查看 Application.onCreate 到第一帧绘制的完整链路
  2. 分解耗时
    • Application 初始化(第三方 SDK 注册)
    • 主题加载/布局 inflation
    • 数据请求/数据库查询
    • 首屏数据绑定
  1. 优化手段
    • 懒初始化非关键 SDK
    • 使用 ContentProvider 延迟初始化
    • 占位图先行,异步加载数据
    • 预加载下一屏数据
  1. 指标监控:上报 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

启动优化

答: 启动优化方案:

  • 冷启动流程拆解
    1. Application.onCreate
    2. 首个 Activity 的 onCreate/onStart/onResume
    3. 首帧 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 - 系统与底层

本篇所有问题

  1. 进程与线程区别、进程切换开销大的原因
  2. 进程是操作系统分配资源的最小单位,这个资源指什么
  3. 进程的崩溃会不会导致别的进程崩溃,为什么
  4. 线程切换开销小为什么,线程切换具体用到哪些指令,怎么保存当前线程上下文
  5. 进程调度方式(时间片轮转、先来先服务、优先级调度、多级反馈队列)
  6. 单核和多核进程调度有什么不一样
  7. 线程组成(TCB)
  8. 线程映射到硬件实现
  9. 多进程必要性、如何实现多进程
  10. 多个进程退出顺序及依据(LRUCache)
  11. Android 多进程与 Linux 进程区别
  12. 进程间通信方式(操作系统层面、安卓层面)
  13. 两个进程读写 SharedPreferences 算进程间通讯吗
  14. 进程同步
  15. 管道机制的缺点
  16. 进程通信方式,介绍 ContentProvider 如何用
  17. Binder 原理、为什么用 Binder(一次拷贝、安全)
  18. Binder 和 Socket/管道/共享内存相比特点
  19. Binder 为什么效率更高(内存映射与一次拷贝)
  20. 一次 Binder 调用大致流程,数据封装在什么对象中
  21. Binder 拷贝几次,所有数据都只会拷贝一次吗
  22. 为什么 Android 选择 Binder 作为主要 IPC 机制
  23. AIDL 的本质是什么
  24. 为什么主线程做 Binder 调用也可能卡顿甚至 ANR
  25. 安卓跨进程通信方式有哪些,用过哪些
  26. Stub 类中 asInterface 函数作用,BnBinder 和 BpBinder 区别
  27. ServiceManager 作用与特殊性
  28. JNI 中 JavaVM 和 JNIEnv 的关系
  29. JNI 动态/静态注册
  30. native 调 Java、cpp 线程调 Java 方法注意事项
  31. java 和 c 如何实现跨语言交互,java 调用 c 的链,c 调用 java 的链
  32. cpp string 转 jstring 两种方式
  33. so 编译过程、静态库与动态库区别、动态链接
  34. ARM 与 x86 区别、RISC 与 CISC
  35. 大小端
  36. 原码反码补码、补码优势
  37. 浮点数表示与运算
  38. APK 打包流程
  39. 安卓的打包流程
  40. 安卓打出的包有哪些文件
  41. APP 启动流程(点击图标到 Activity 显示)
  42. 手机按下电源键启动内核
  43. Linux 启动模型
  44. APK 入口(ActivityThread.main())
  45. 可执行文件从源文件到运行的过程
  46. 调用函数时栈空间变化
  47. 中断机制的底层原理
  48. BIO/NIO/AIO 的关系
  49. 发生异常时如何不让应用退出(UncaughtExceptionHandler)
  50. C++ 虚函数、指针、指针的指针、智能指针
  51. 智能指针如何实现,强引用计数指针多线程访问怎么保证安全
  52. 智能指针哪几种,使用场景
  53. C++ 线程池,如何实现,优点好处
  54. C++ 构造函数可以调用虚函数吗
  55. NDK 有没有了解过
  56. OpenGL 渲染管线
  57. 纹理内存优化
  58. OpenGL PBO 使用过吗
  59. OpenGL ES 和 OpenGL 区别
  60. glFlush() 和 glFinish() 区别
  61. GLSL shader
  62. 多线程渲染
  63. 跨端框架了解哪些
  64. 鸿蒙调用 cpp 怎么做
  65. 鸿蒙多线程概念是否和其他客户端一致
  66. 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 中进程按优先级管理:

  1. 前台进程:正在与用户交互(如可见 Activity),最后被杀
  2. 可见进程:部分可见(如对话框后的 Activity)
  3. 服务进程:运行后台服务
  4. 后台进程:不可见的 Activity
  5. 空进程:无组件运行,最先被杀

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 使用步骤

  1. 定义 Provider:继承 ContentProvider,实现 query/insert/update/delete,在 Manifest 中注册

  2. 定义 URI:如 content://com.example.provider/data

  3. 访问数据:

    ContentResolver resolver = context.getContentResolver();
    Cursor cursor = resolver.query(uri, projection, selection, args, sortOrder);

  4. 跨进程访问:其他应用通过相同 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 调用大致流程,数据封装在什么对象中

流程

  1. Client 调用 AIDL 接口方法 → Proxy 类将参数打包到 Parcel 对象
  2. transact() 将 Parcel 数据通过 Binder 驱动发送到 Server
  3. Server 端 onTransact() 从 Parcel 解包参数
  4. 执行实际逻辑,将结果打包到回复 Parcel
  5. 返回给 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 打包流程

  1. 资源编译:aapt2 编译 res/→ resources.arsc + R.java
  2. 代码编译:javac 编译.java→.class → d8/r8 转换.class→.dex(Dalvik 字节码)
  3. 打包:apkbuilder 合并.dex + resources.arsc + 资源文件 + AndroidManifest.xml → 未签名 APK
  4. 签名:jarsigner 或 apksigner 用私钥签名(v1/v2/v3 方案)
  5. 对齐:zipalign 优化资源访问(4 字节对齐)
  6. 输出:生成可安装的.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 显示)

  1. Launcher:点击图标 → 调用 ActivityManagerService(AMS)
  2. AMS:检查进程是否存在,若不存在通知 Zygote fork 新进程
  3. Zygote:fork 应用进程 → 加载 ActivityThread
  4. ActivityThread:main() 启动 → 创建 Application → attach 到 AMS
  5. AMS:调度 Lifecycle → onCreate() → onStart() → onResume()
  6. WindowManager:请求 SurfaceFlinger 创建窗口 → 显示 UI

问:手机按下电源键启动内核

  1. BootROM:固化在芯片中,上电执行,加载 Bootloader 到 RAM
  2. Bootloader(如 U-Boot):初始化硬件(时钟、内存、串口)→ 加载内核镜像到内存
  3. Kernel:解压内核 → 初始化子系统(内存管理、进程调度、设备驱动)→ 挂载 rootfs
  4. 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 调度。

问:可执行文件从源文件到运行的过程

  1. 源码:.c/.cpp 文件
  2. 预处理:展开宏、头文件、条件编译 → .i 文件
  3. 编译:词法/语法/语义分析 → 汇编代码.s
  4. 汇编:.s→.o 目标文件(机器码 + 符号表)
  5. 链接:静态库(.a)/动态库(.so)链接 → 可执行文件
  6. 加载:OS 加载器分配内存 → 解析动态库 → 跳转到入口点(_start→main)

问:调用函数时栈空间变化

调用前 :SP 指向当前栈顶

调用

  1. 参数入栈(从右到左)
  2. 返回地址入栈(call 指令自动)
  3. 保存旧栈帧(push rbp / mov rbp,rsp)
  4. 分配局部变量(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++ 线程池,如何实现,优点好处

实现

  1. 创建固定数量的工作线程(阻塞等待任务)
  2. 任务队列(std::queue + mutex + condition_variable)
  3. 提交任务时加锁入队 → notify 唤醒线程
  4. 线程取任务执行 → 循环等待

优点

  • 减少线程创建/销毁开销
  • 控制并发数量,避免资源耗尽
  • 复用线程,提升响应速度

问:C++ 构造函数可以调用虚函数吗

可以但不建议 。构造函数中调用虚函数时,不会触发多态,而是调用当前类的版本(因为派生类还未构造完成)。

示例:基类构造函数调虚函数,实际调用基类实现而非派生类重写版本,易导致逻辑错误。

问:NDK 有没有了解过

NDK(Native Development Kit):用于在 Android 中开发 C/C++ 代码。

  • 用途:性能敏感(图像处理、音视频编解码)、复用现有 C/C++ 库、游戏引擎
  • 工具链:CMake/ndk-build → 生成.so → 打包到 APK 的 lib/目录
  • 交互:通过 JNI 与 Java 层通信
  • 调试:Android Studio 支持 native 断点调试

问:OpenGL 渲染管线

流程

  1. 顶点着色器:处理顶点坐标、变换(MVP 矩阵)
  2. 图元装配:顶点组成点/线/三角形
  3. 光栅化:图元转换为片元(像素候选)
  4. 片元着色器:计算颜色、纹理采样
  5. 测试与混合:深度测试、模板测试、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)

  1. SQLite 优化手段
  2. 分页查询 SQL
  3. 多表查询
  4. group by、join
  5. 三大范式
  6. 关系型数据库优化手段
  7. 主键是什么
  8. 事务(多步骤操作全部生效)
  9. MySQL 中 drop 和 delete 的区别和联系
  10. 数据库索引作用、使用场景,底层结构
  11. 安卓数据库(如 SQLite)的索引是什么,底层结构
  12. MySQL 事务,隔离级别
  13. 持久化存储(安卓常用)
  14. 保存到 SQLite

设计模式(15-33)

  1. 单例模式(双重校验锁、静态内部类、枚举、懒汉式、饿汉式)
  2. 单例模式有几种,为什么采取单例模式
  3. 单例模式特点
  4. 懒汉式单例,双重检测加几次锁,synchronized 锁的是什么,为什么
  5. 饿汉式和懒汉式是线程安全的吗
  6. 乐观锁写单例
  7. 责任链模式(View 事件分发、OkHttp 拦截器)
  8. 观察者模式(EventBus、LiveData)
  9. 适配器模式(RecyclerView)
  10. 装饰者模式(InputStream)
  11. 建造者模式(AlertDialog、Retrofit)
  12. 策略模式(Interpolator、Comparator)
  13. 工厂模式,包含哪几个类,抽象类是什么
  14. 代理模式
  15. 委派模式,委派模式和代理模式区别
  16. 结合 Android 源码解释设计模式
  17. okhttp 设计模式有哪些
  18. 常见的设计模式,举实际开发中的例子
  19. 设计模式了解哪些

Git(34-40)

  1. git merge 与 git rebase 区别
  2. 已经 commit 了代码但需要修改,且不想新增 commit 记录,用什么命令
  3. Git 怎么解决冲突?
  4. Git 的底层实现逻辑?
  5. 常用 git 命令?
  6. Git 使用相关类?
  7. 如何使用 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 四种隔离级别

  1. READ UNCOMMITTED(读未提交):可能读到脏数据
  2. READ COMMITTED(读已提交):避免脏读,可能不可重复读
  3. REPEATABLE READ(可重复读):MySQL 默认级别,避免不可重复读,可能幻读
  4. SERIALIZABLE(串行化):最高级别,串行执行,性能低
问:持久化存储(安卓常用)

Android 常用持久化存储方案:

  1. SharedPreferences:键值对存储,适合配置信息
  2. SQLite 数据库:关系型存储,适合结构化数据
  3. 文件系统:内部存储/外部存储,适合大文件
  4. Room 数据库:SQLite 的 ORM 封装
  5. DataStore:SharedPreferences 的替代方案
  6. 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)、静态内部类、枚举

使用原因

  • 确保全局只有一个实例
  • 节省系统资源(如数据库连接、线程池)
  • 方便全局访问
  • 避免重复初始化
问:单例模式特点
  1. 私有构造方法
  2. 静态方法返回唯一实例
  3. 全局唯一实例
  4. 懒加载/饿加载
  5. 线程安全考虑
问:懒汉式单例,双重检测加几次锁,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 怎么解决冲突?
  1. 找到冲突文件(git status)
  2. 手动编辑冲突内容(<<<<<<< HEAD 和 >>>>>>>)
  3. 标记解决:git add <file>
  4. 完成合并:git commitgit rebase --continue
  5. 取消合并:git merge --abortgit 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:对象 ID
  • RefDatabase:引用数据库
问:如何使用 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 - 算法与数据结构

本篇所有问题

  1. 栈和队列的区别
  2. 哈希表的原理
  3. 完全二叉树验证
  4. 堆排序
  5. 递归函数遍历二叉树
  6. 检验出栈序列的合法性
  7. 数组和链表寻找任意元素的时间复杂度
  8. 数组如何实现 O(1) 时间复杂度访问
  9. 数组和链表的区别
  10. 链表:单链表和双链表
  11. LRU 算法
  12. 二分查找复杂度
  13. 快排和堆排时间复杂度
  14. 数据结构特点
  15. 怎么判断链表有环
  16. 翻转单链表
  17. 反转链表(奇偶链表)
  18. 单向链表倒数第 k 个节点
  19. 合并两个有序链表
  20. 合并 K 个升序链表
  21. 判断单链表有没有环
  22. 删除升序链表中所有重复元素
  23. 两个链表寻找交叉节点
  24. 快速排序
  25. 堆排序
  26. 冒泡排序优化
  27. 最长递增子序列
  28. 最长无重复子串
  29. 无重复字符最长子串
  30. 字符串相加
  31. 字符串相乘
  32. 最长公共前缀
  33. 字符串最长递增子串
  34. 判断 s2 中是否存在 s1 的全排列序列
  35. 连续子数组的最大和
  36. 数组第 K 个最大元素
  37. 前 k 个最大元素
  38. 查找第 k 小的数
  39. TopK
  40. 合并两个有序数组
  41. 两个有序数组合并
  42. 二分查找
  43. 搜索旋转排序数组
  44. 有序数组查找某个数首次出现的位置
  45. 有序数组删除重复元素
  46. 一次遍历将只包含 1,2,3 的数组排序
  47. 先增后降数组去重并排序
  48. 排序后相邻元素的最大差值
  49. 二维矩阵找 target
  50. 旋转数组查找指定值
  51. 螺旋输出矩阵
  52. 二维螺旋数组
  53. 对称二叉树
  54. 二叉树的中序遍历
  55. 二叉树的前序遍历
  56. 二叉树的右视图
  57. 二叉树的深度
  58. 二叉树的最小深度
  59. 层次遍历/之字遍历
  60. 求二叉树高度
  61. 数组实现队列
  62. 两个栈实现队列
  63. 用两个栈实现队列
  64. 添加删除随机获取都是 O(1)
  65. 线程安全的单例
  66. 双重判定单例模式
  67. 生产者消费者模式
  68. 线程池设计
  69. 两个线程轮流给变量 i+1
  70. 三个线程依次打印 1,2,3
  71. 五个线程顺序打印 1~无穷大
  72. 三个线程按顺序打印 ABC
  73. 多线程分段下载文件
  74. 写一个线程池
  75. gas 数组和 cost 数组
  76. n 个人发糖果
  77. 字符串数组不包含相同字母的最大长度乘积
  78. 文本左右对齐
  79. 不使用乘除 mod 移位实现整数除法
  80. 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)

  1. 项目核心技术与遇到的技术问题

  2. 项目动机、具体功能、UI 设计实现

  3. 项目中的难点、卡点

  4. 第三方算法和库如何想到使用

  5. 动态主题适配、颜色提取(Palette 库)

  6. MediaPlayer 问题(缓冲时间长、播放卡顿)

  7. MediaPlayer 的状态管理怎么实现

  8. 网络请求多线程处理

  9. 项目中用到的核心技术

  10. 项目中实现的功能解决了什么问题

  11. 获取图像后用 SDK 前的处理

  12. 自己设计图片加载工具的思路

  13. 流式打印实现

  14. 大厂代码的恶心问题及治理手段

  15. 业务与 SDK 开发偏好

  16. 是否觉得客户端能深钻的技术不多

  17. 对性能优化的见解

  18. 定时任务怎么保证一定能执行

  19. 软件如何实现保活

  20. 每天五次提醒如何确保都提醒、高延迟如何检测

  21. 印象最深的一个 bug

  22. 开发过程中有检测到内存泄漏吗,什么具体场景

  23. 非静态内部类为什么会导致内存泄漏

  24. 播放器中的动画都有哪些,通过什么方式实现

  25. Activity 被销毁或重建时,MVVM 中的 ViewModel 如何保证状态不丢失

  26. MVVM 体现在哪里,具体怎么划分

  27. 下载组件实现,大文件下载优化,多个下载任务优化

  28. 对 app 内存的理解,app 有哪些部分的内存

  29. ANR 如何排查,原理

  30. 项目满意的地方

  31. 项目使用了什么架构,MVVM、MVP、MVC 理解

  32. 浏览器输入 URL 到页面显示完整流程

  33. 如果主线程一定要执行耗时逻辑如何不 ANR

  34. 本地广播原理、如何只发送给特定 App

  35. 如何设计缓存(淘汰策略 LRU/LFU)

  36. 如何监听手机拍照和截屏、图片隐写

  37. JSON 解析器实现(词法分析、语法分析、数据结构)

  38. Java 中哪些场景需要重写 hashCode 和 equals

  39. 没有用 volatile 会发生什么

  40. 线程与协程的上限区别

  41. APK 中多个进程退出顺序

  42. 大图加载压缩方式

  43. 断点续传实现

  44. 文件上传

  45. 签名作用

  46. 代码/无用资源检测

  47. 热修复方案原理及优缺点

  48. 线上收集 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 到页面显示完整流程

  1. DNS 解析
  1. TCP 连接建立
  1. HTTP 请求发送
  1. 服务器响应
  1. 浏览器渲染
  • 请详细描述每个阶段的细节

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)

  1. 某公司技术氛围、性能优化见解

  2. ANR 是什么,产生原因,有几种,如何检测和避免

  3. 如何让三个线程按照顺序打印 ABC

  4. 设计一个离线缓存的播放器应用架构

  5. 如何设计一个相册(九宫格样式)

  6. 高性能图片缓存系统设计

  7. 手机截长屏图像如何拼接

  8. 场景:a、b 独立,c 依赖 a、b 的数据

  9. 场景:实现一个线程安全的方法

  10. 场景:实现一个根据优先级对子 View 进行测量的自定义 ViewGroup

  11. 场景:列表 LiveData 绑定 ViewHolder 注意事项

  12. 场景:崩溃问题只在部分机型出现,怎么排查

  13. 场景:活动从后台切前台,生命周期

  14. 场景:屏幕翻转记录日期等参数保存到 SQLite

  15. 场景:实现四则运算计算器 APP

  16. 哪种组件承载用户的交互工作(Activity)

  17. 布局文件一般用什么格式的文件来写的(XML)

  18. 哪种线程属于非 UI 线程

  19. Activity 可以用什么来启动(Intent)

  20. 异步加载可以用什么方法(协程/线程池/WorkManager,AsyncTask 已废弃)

  21. 水平居中的布局应该使用什么属性设置

  22. AndroidManifest 文件可以用来声明各种属性和配置

  23. Activity 创建菜单的方法

  24. Fragment 创建视图的方法

  25. Handler 一般不用于什么(长时间的后台工作)

  26. 常见布局有哪些

  27. 异步任务的实现方式

  28. 文件持久化的方法有哪些

  29. 四大组件有哪些可以响应广播

  30. 多线程的创建方式

  31. 安卓应用的/res/文件夹下可以放什么东西

  32. AndroidManifest 文件可以声明哪些权限

  33. 安卓应用的性能分析工具有哪些

  34. Gradle 工具的作用

  35. Kotlin 语言的特性

  36. 讲一讲 Java 的 GC 机制

  37. Java 的内存泄露是什么情况,如何避免

  38. 安卓系统的 IPC 机制

  39. 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。

相关推荐
码云骑士1 小时前
Android Launcher启动过程
android
Java面试题总结2 小时前
MySQL EXISTS 详解:存在性判断、NOT EXISTS 与实战示例
android·数据库·mysql
_李小白2 小时前
【android opencv学习笔记】Day 30: 滤波算法之拉普拉斯算子
android·opencv·学习
NiceCloud喜云11 小时前
Opus 4.8 的 Effort Control 怎么选:Low 到 Max 五档策略
android·java·大数据·前端·c++·python·spring
日光明媚14 小时前
一步生成视频!One-Forcing:DMD + 零成本 GAN,训练 200 步超越多步 SOTA
android·开发语言·kotlin
帅次15 小时前
Android 17 开发者实战:核心更新与应用场景落地指南
android·java·ios·android studio·iphone·android jetpack·webview
大鹏说大话15 小时前
SQL 排序与分组实战:解决“分组后取最新数据“
android·java·数据库
搜狐技术产品小编202318 小时前
破局与重构:纯端侧 Android 自动化引擎的尝试与未来推演
android·运维·重构·自动化
码云骑士19 小时前
Android SystemServer启动过程
android·systemserver