1、MD5介绍:
MD5是一种广泛使用的哈希函数,能生成一个128位(16字节)的哈希值,通常表示为32个字符的十六进制字符串。它有几个关键特性:
- 不可逆性:从哈希值无法反向推导出原始数据。
- 固定长度输出:无论输入数据多大,输出长度固定。
- 抗修改性:原始数据哪怕只改动一个字节,生成的MD5值也会有很大区别。
2、MD5的原理是什么?是否可逆?
- 考察点:对MD5基本特性的理解。
- 回答思路:明确MD5是哈希算法,不是加密算法。其设计目的就是单向转换,因此不可逆。解释其不可逆的原因在于它是一种“摘要”算法,会有信息损失。
3、MD5在Android中有哪些实际应用场景?
- 考察点:对技术应用场景的理解。
- 回答思路:可以提及:
- 文件完整性校验:下载文件后,计算其MD5值并与服务端提供的对比,验证文件是否完整或未被篡改。
- 密码存储:不在数据库中直接存储用户明文密码,而是存储其MD5哈希值(但强烈建议加盐,见下文)。
- 数据唯一性判断:例如,可以利用MD5来快速判断两个大文件是否相同。
4、MD5是否绝对安全?有哪些已知风险?
- 考察点:对MD5安全局限性的认知。
- 回答思路:MD5目前不再被认为是高强度的哈希算法。主要风险是碰撞攻击,即攻击者可以找到两个不同的输入产生相同的MD5值。这意味着其用于数据完整性校验和数字签名时的可靠性已大打折扣。
5、如何提高MD5在密码存储中的安全性?
- 考察点:对安全实践的了解。
- 回答思路:核心方法是加盐。即在原始密码前后拼接一个随机生成的字符串(盐值)后,再计算MD5。每个用户的盐值都应不同并单独存储,这能极大增加破解难度。同时,也应了解更安全的替代方案,如 SHA-256、SHA-3 等
import java.security.MessageDigest // 导入加密相关的类
fun String.toMD5(): String { // 为String类扩展一个toMD5()方法
val digest = MessageDigest.getInstance("MD5") // 1. 获取MD5加密对象
val result = digest.digest(this.toByteArray()) // 2. 执行加密,得到字节数组
val hexString = StringBuilder()
for (b in result) { // 3. 遍历字节数组中的每个字节
hexString.append(String.format("%02x", b)) // 4. 将每个字节转为2位十六进制
}
return hexString.toString() // 返回最终的32位十六进制字符串
}
6、AES(高级加密标准)
AES是一种对称加密算法,意味着加密和解密使用同一把密钥。它的特点是速度快、安全性高。
- 工作原理:对称分组加密,将数据分成固定大小的块,用密钥进行多轮替换和置换。
- 关键参数:
-
密钥长度:128位、192位、256位。长度越长越安全,但计算开销略增。通常256位用于最高安全要求。
-
工作模式:如 ECB, CBC, CTR, GCM。这是面试高频点!
- ECB:简单不安全,相同明文块产生相同密文块,绝不使用。
- CBC:常用,需要初始化向量(IV),可并行解密。
- GCM:推荐模式,同时提供加密和认证,能检测密文是否被篡改。
-
7、AES是什么?对称加密和非对称加密有何区别?
- 考察点:对加密体系的基本分类理解。
- 回答思路:解释AES是对称加密的代表。核心区别在于:对称加密使用同一把密钥进行加解密,效率高;非对称加密(如RSA)使用公钥和私钥两把密钥,更安全但速度较慢。AES通常用于加密数据本身。
8、AES有哪些常用的工作模式?(如ECB, CBC)
- 考察点:对AES具体实现方式的了解。
- 回答思路:这是高频考点。
- ECB模式:最简单,但相同的明文块会加密成相同的密文块,安全性较差,不推荐用于加密大量数据。
- CBC模式:更安全,需要提供一个初始化向量,使得即使明文相同,加密出的密文也不同。这是目前常用的模式。
9、在Android中实现AES加解密时,密钥如何安全管理和存储?
- 考察点:对安全最佳实践的掌握。
- 回答思路:这是体现安全意识的重点。绝对不要将密钥硬编码在代码中。建议:
- 使用Android系统提供的 Keystore 系统来生成和存储密钥,这是最安全的方式。
- 如果密钥由服务端下发,可以考虑利用非对称加密(如RSA)来安全传输AES密钥。
10、Android 7.0(N)以后在AES密钥生成方面有什么需要注意的?
- 考察点:对Android系统版本差异和安全更新的关注。
- 回答思路:在Android N之前,有些开发者会使用基于密码(种子)和
SecureRandom的方式生成固定密钥。但Android N开始,废弃了默认的Crypto提供商,导致这种写法可能失败。应转向使用更规范的方式,如直接使用SecretKeySpec加载原始密钥字节。
11、GC Roots详细描述
通过一系列称为 "GC Roots" 的根对象作为起始点,向下搜索,搜索走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,则此对象不可达,可以被回收。
GC Roots 就像是一栋大楼的总电闸,而对象之间的引用关系就是大楼里的电线。垃圾回收器(GC)的工作就是检查哪些“房间”(对象)还亮着灯(可达),哪些可以断电(回收)。
| 概念 | 生活比喻 | 技术解释 |
|---|---|---|
| GC Roots | 大楼的总电闸、紧急照明系统 | 一组作为起点的特殊对象,是判断对象存活的“生命线”。 |
| 引用链 (Reference Chain) | 从总电闸连接到各个房间的电线 | 从GC Roots出发,通过对象间的引用关系连接形成的路径。 |
| 可达 (Reachable) | 房间的灯亮着 | 对象通过引用链与GC Roots相连,说明程序还在使用它,是“活的”,不能被回收。 |
| 不可达 (Unreachable) | 房间的灯灭了,且没有任何电线连接回总电闸 | 对象无法通过任何引用链连接到GC Roots,说明程序已经无法访问它,是“死的”,可以被回收。 |
12、JVM内存区域(运行时数据区)
这是理解垃圾回收的基础。你需要清晰地区分哪些区域会进行垃圾回收,哪些不会。
| 内存区域 | 作用 | 线程共享? | 是否GC? | 异常 | 特别说明 |
|---|---|---|---|---|---|
| 程序计数器 | 当前线程执行的字节码行号指示器。 | 私有 | 否 | 无 | 唯一一个没有规定任何 OutOfMemoryError 的区域。 |
| Java虚拟机栈 | 存储栈帧,对应Java方法调用。栈帧中包含局部变量表、操作数栈等。 | 私有 | 否 | StackOverflowError OutOfMemoryError | 局部变量表中的引用是重要的 GC Roots。 |
| 本地方法栈 | 为Native方法服务。 | 私有 | 否 | 同上 | 与虚拟机栈作用类似。 |
| 堆 | 存放所有对象实例和数组。是GC管理的主要区域。 | 共享 | 是 | OutOfMemoryError | 可细分为新生代、老年代等。 |
| 方法区 | 存储已被加载的类信息、常量、静态变量等。 | 共享 | 是(主要回收废弃常量和类) | OutOfMemoryError | JDK 8后称为 “元空间” ,使用本地内存。 |
13、如何判断对象“已死”?
垃圾回收的首要问题是确定哪些对象是“垃圾”。JVM主要使用可达性分析算法。
-
引用计数法(Java未采用) :无法解决循环引用问题。
-
可达性分析算法(Java采用) :从一系列
GC Roots对象出发,向下搜索,走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,则此对象不可达,可以被回收。- 哪些对象可以作为GC Roots?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
- Java虚拟机内部的引用(如基本数据类型对应的Class对象)。
- 所有被同步锁(
synchronized)持有的对象。
- 哪些对象可以作为GC Roots?
14、经典垃圾收集算法
| 算法 | 核心思想 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|---|
| 标记-清除 | 1. 标记所有需要回收的对象。 2. 统一回收。 | 简单 | 1. 效率不高 2. 产生内存碎片 | 老年代(CMS收集器) |
| 复制 | 将内存分为两块,只用一块。存活对象复制到另一块,然后清空当前块。 | 高效无碎片 | 内存利用率低(50%) | 新生代(Survivor区) |
| 标记-整理 | 1. 标记存活对象。 2. 将存活对象向一端移动,然后清理边界外内存。 | 无碎片,利用率高 | 移动对象有开销 | 老年代(Parallel Old, G1) |
| 分代收集 | 根据对象存活周期将堆分为新生代和老年代,采用不同算法。 | 综合最优 | 实现复杂 | 现代JVM主流策略 |
15、堆内存的分代模型(HotSpot为例)
-
新生代:对象“朝生夕死”,GC(Minor GC)频繁。
- Eden区:新对象在此分配。
- Survivor区 (From/To) :存放每次Minor GC后存活的对象。
- 流程:
Eden -> Minor GC -> 存活对象进入 Survivor-> 年龄增长 -> 年龄阈值 -> 晋升老年代。
-
老年代:存放长期存活的对象。GC(Major GC / Full GC)较慢,通常伴随STW停顿。
-
永久代/元空间:存储类元数据。Full GC时也会进行回收(如卸载类)。
16、Android的运行时环境(ART)与标准JVM有显著区别,这也是面试重点
| 面 | 标准JVM (HotSpot) | Android ART | 对开发者的启示 |
|---|---|---|---|
| 执行方式 | 解释执行 + JIT即时编译 | AOT预先编译 + JIT (Android 7.0+) | 安装应用略慢,但运行更快、更省电。 |
| 垃圾回收器 | 多种选择(Serial, Parallel, CMS, G1, ZGC) | 单一、深度定制的GC(如并发压缩式GC) | 无法选择GC,优化重点在减少内存分配和避免泄漏。 |
| GC特性 | 不同GC器策略差异大。 | 并发压缩式GC(Android 8.0+) :大部分工作与应用线程并发执行,且每次GC都压缩堆消除碎片。 | GC停顿更短、更可预测。堆利用率高,但压缩有额外CPU开销。 |
| 内存共享 | 每个进程独立堆。 | Zygote进程:系统服务和大部分App进程由其fork,共享基础框架的代码和资源,极大节省内存。 | 理解Android进程启动模型。 |
| 调试工具 | jstat, jmap, VisualVM | Android Profiler (Memory View), adb shell dumpsys meminfo | 掌握Android专属的内存分析工具。 |
18、当被问到“谈谈JVM的内存管理和垃圾回收”时,可以这样组织答案:
“我会从内存区域划分、对象存活判断、回收算法和Android特性四个方面来谈。
首先,JVM内存分为线程私有的程序计数器、栈、本地方法栈,以及线程共享的堆和方法区。其中堆是GC的主要区域,方法区则回收常量和无用类。
其次,判断对象是否存活使用的是可达性分析算法,从
GC Roots(主要是栈中局部变量、静态变量、常量等)出发,寻找引用链。第三,回收算法主要有标记-清除、复制、标记-整理,现代JVM采用分代收集策略,将堆分为新生代(用复制算法)和老年代(用标记-清除或整理算法),分别进行Minor GC和Full GC。
最后,在Android的ART环境下,情况特殊:它使用单一的、高度优化的并发压缩式GC,大部分工作与应用并发,且会压缩堆。此外,所有应用进程由Zygote fork,共享代码段,内存模型与标准JVM不同。因此,我们在Android上的内存优化重点在于减少不必要的对象分配和避免Context泄漏。”
17、Java的四种引用(强、软、弱、虚)及其在Android中的应用场景?
- 弱引用:常用于
WeakHashMap或避免内存泄漏(如结合ReferenceQueue)。 - 软引用:适合做缓存(内存不足时回收)。
18、如何判断一个常量是废弃常量?一个类是无用类?
- “判断一个常量是否废弃,核心是看运行时常量池中该常量是否还有引用,包括来自代码字面量的直接引用和来自堆中String对象的间接引用。
- 判断一个类是否无用,条件非常严格,必须同时满足:1)该类所有实例被回收;2)加载它的ClassLoader被回收;3)该类的Class对象没有被任何地方引用。由于系统类加载器几乎不回收,所以普通应用的类很少被卸载。
19、Minor GC和Full GC分别在什么情况下触发?
这是一个非常核心的JVM调优问题。简单来说,Minor GC(年轻代GC)触发频繁且迅速,专注于回收“短命”对象;而Full GC(整堆GC)触发条件更严格,涉及整个堆和方法区,停顿时间长,是影响性能的主要瓶颈。
它们的触发条件和逻辑对比如下表所示:
| 对比维度 | Minor GC(年轻代GC) | Full GC(整堆GC) |
|---|---|---|
| 核心目标 | 回收年轻代(主要是Eden区)的死亡对象。 | 回收整个Java堆(包括年轻代、老年代)和方法区(元空间)。 |
| 触发条件 | 当JVM需要为新对象分配内存,但Eden区空间不足时触发。 | 情况复杂,主要有以下几种: 1. 老年代空间不足(常见原因)。 2. 方法区(元空间)空间不足。 3. 显式调用 System.gc() (仅建议,不保证)。 4. 空间分配担保失败(后文详述)。 |
| 执行速度 | 快(通常毫秒级)。 | 慢(通常秒级,是Minor GC的10倍以上)。 |
| 对应用影响 | 停顿时间短,通常可接受。 | 长时间“Stop-The-World” ,会明显导致应用卡顿或无响应。 |
| 发生频率 | 非常高。 | 较低,应尽量减少。 |
“Minor GC和Full GC的核心区别在于回收范围和对应用的影响。Minor GC在Eden区内存不足分配新对象时被动触发,只回收年轻代,速度快。Full GC则发生在老年代空间不足、方法区空间不足、显式调用System.gc()或空间分配担保失败时,会回收整个堆和方法区,导致长时间的‘Stop-The-World’停顿,是性能调优中需要重点规避的。
特别是在空间分配担保机制下,如果JVM判断Minor GC后存活对象可能太多,老年代无法容纳,会出于安全考虑直接触发Full GC。在Android的ART环境中,虽然触发逻辑类似,但由于采用了并发压缩式GC,Full GC的实际停顿时间更可控。因此,我们的优化重点应放在减少不必要的对象创建和避免内存泄漏上,从源头降低GC压力。”
20、GC Roots为什么不能是堆内的普通对象?
“GC Roots被定义为堆外部的引用,这首先是垃圾回收算法的逻辑需要。可达性分析必须从一个确定的、代表‘外部世界正在使用’的起点集合(GC Roots)出发。如果允许堆内对象作为GC Root,将导致判断逻辑失去起点,并可能因引用链的传递性使所有对象都被误判为存活,从而使GC失效。
21、Android中如何排查和避免内存泄漏?(结合Handler、单例、匿名内部类等)
1、核心排查工具详解
| 工具 | 核心用途 | 关键操作与解读 |
|---|---|---|
| Android Profiler (Memory) | 实时监控与手动堆转储。 | 1. 操作:触发疑似泄漏场景 -> 点击 “Dump Java heap” 。 2. 解读:在堆转储中,按类名过滤,查找本应被销毁的 Activity 或 Fragment 实例。选中实例,查看 “References” 标签页,找到是谁在持有它。 |
| LeakCanary | 自动检测与报告泄漏。自动化首选。 | 1. 集成:添加依赖即可。 2. 原理:自动监控 Activity/Fragment 销毁,触发GC后检测其是否被回收,若未回收则分析引用链并发送通知。 3. 输出:提供清晰的引用链,直接指向根源。 |
| adb shell dumpsys meminfo [package] | 命令行宏观分析。 | 查看应用整体内存概况,关注 Activities、Views 的数量是否异常增长。 |
2、Handler 泄漏(最常见)
-
泄漏原理:
Handler作为非静态内部类(包括匿名内部类) ,隐式持有外部Activity的引用。如果通过postDelayed()发送了延时消息,或者消息队列中还有未处理的消息,那么这条消息(Message)-> Handler -> Activity 的引用链会阻止Activity被回收。 -
解决方案:
kotlin
class SafeActivity : AppCompatActivity() { // 1. 使用静态内部类,切断与Activity的强引用 private class MyHandler(activity: SafeActivity) : Handler(Looper.getMainLooper()) { // 2. 使用弱引用持有Activity private val weakActivity = WeakReference(activity) override fun handleMessage(msg: Message) { val activity = weakActivity.get() activity?.run { // 使用前检查Activity是否还存在 updateUI() } } } private val handler = MyHandler(this) override fun onDestroy() { super.onDestroy() // 3. 移除所有回调和消息 handler.removeCallbacksAndMessages(null) } }
2、单例(或全局静态类)泄漏
-
泄漏原理:单例生命周期等于应用进程。如果单例直接或间接持有了一个
Activity的Context或View,就会导致该Activity无法释放。 -
解决方案:
kotlin
object AppDataManager { // ❌ 错误:可能传入Activity Context // fun init(context: Context) { ... } // ✅ 正确:始终使用 Application Context fun init(appContext: Context) { val context = appContext.applicationContext // ... 初始化操作 } // 如果必须持有View/Activity引用,使用弱引用 private val weakRefs = mutableListOf<WeakReference<SomeListener>>() }
3、 匿名内部类 / 非静态内部类泄漏
-
泄漏原理:在Java/Kotlin中,所有非静态内部类都隐式持有其外部类实例的强引用。 如果这个内部类的实例(如
Runnable,OnClickListener)被一个长生命周期对象(如后台线程、静态集合)引用,就会泄漏外部类。 -
解决方案:
kotlin
// ❌ 错误:匿名内部类泄漏Activity textView.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View?) { // 此匿名类持有Activity引用 doSomething() } }) // ✅ 方案A:使用Lambda(在Kotlin中,如果Lambda未捕获外部引用,则是安全的) textView.setOnClickListener { doSomething() } // ✅ 方案B:使用静态内部类(原理同Handler) private class MyClickListener(activity: MyActivity) : View.OnClickListener { private val weakActivity = WeakReference(activity) override fun onClick(v: View?) { weakActivity.get()?.doSomething() } }
4、注册监听器未取消
-
泄漏原理:在
Activity中注册了系统的监听器(如SensorManager,LocationManager的监听器),在销毁时未反注册,导致系统服务持有Activity引用。 -
解决方案:在
onStart/onResume注册,在对应的onStop/onPause或onDestroy中务必反注册。kotlin
override fun onResume() { super.onResume() sensorManager.registerListener(this, ...) } override fun onPause() { super.onPause() sensorManager.unregisterListener(this) // 必须! }
5、资源性对象未关闭
-
泄漏原理:
Cursor、File、Socket、Bitmap等资源性对象,不仅消耗Java堆内存,更消耗宝贵的本地内存(Native Heap)。未及时关闭会导致本地内存泄漏,而Profiler可能不易直接发现。 -
解决方案:使用
use扩展函数(Kotlin)或try-with-resources(Java)自动管理。kotlin
FileInputStream(file).use { stream -> // 自动关闭 }
6、 架构级预防策略
- 使用
Application Context:在工具类、单例、静态辅助方法中,除非必须操作UI,否则一律使用context.applicationContext。 - 拥抱
Lifecycle感知组件:使用LiveData、Flow、LifecycleObserver,它们能自动管理生命周期,避免手动解绑的疏漏。 - 对
Activity/Fragment的引用做“消毒” :在需要传递或缓存的场景,使用WeakReference,并养成先判空再使用的习惯。 - 代码审查与团队规范:将内存泄漏检查(如“非静态内部类”、“Handler”、“未反注册”)纳入代码审查清单。