2026小知识点-简(五)

66 阅读17分钟

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可细分为新生代、老年代等。
方法区存储已被加载的类信息、常量、静态变量等。共享是(主要回收废弃常量和类)OutOfMemoryErrorJDK 8后称为 “元空间” ,使用本地内存。

13、如何判断对象“已死”?

垃圾回收的首要问题是确定哪些对象是“垃圾”。JVM主要使用可达性分析算法

  • 引用计数法(Java未采用) :无法解决循环引用问题。

  • 可达性分析算法(Java采用) :从一系列 GC Roots 对象出发,向下搜索,走过的路径称为引用链。如果一个对象到GC Roots没有任何引用链相连,则此对象不可达,可以被回收。

    • 哪些对象可以作为GC Roots?
      1. 虚拟机栈(栈帧中的局部变量表)中引用的对象。
      2. 方法区中类静态属性引用的对象。
      3. 方法区中常量引用的对象。
      4. 本地方法栈中JNI引用的对象。
      5. Java虚拟机内部的引用(如基本数据类型对应的Class对象)。
      6. 所有被同步锁(synchronized)持有的对象。

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, VisualVMAndroid 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. 解读:在堆转储中,按类名过滤,查找本应被销毁的 ActivityFragment 实例。选中实例,查看 “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、单例(或全局静态类)泄漏

  • 泄漏原理:单例生命周期等于应用进程。如果单例直接或间接持有了一个 ActivityContextView,就会导致该 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/onPauseonDestroy 中务必反注册。

    kotlin

    override fun onResume() {
        super.onResume()
        sensorManager.registerListener(this, ...)
    }
    override fun onPause() {
        super.onPause()
        sensorManager.unregisterListener(this) // 必须!
    }
    

5、资源性对象未关闭

  • 泄漏原理CursorFileSocketBitmap 等资源性对象,不仅消耗Java堆内存,更消耗宝贵的本地内存(Native Heap)。未及时关闭会导致本地内存泄漏,而 Profiler 可能不易直接发现。

  • 解决方案:使用 use 扩展函数(Kotlin)或 try-with-resources(Java)自动管理。

    kotlin

    FileInputStream(file).use { stream ->
        // 自动关闭
    }
    

6、 架构级预防策略

  1. 使用 Application Context:在工具类、单例、静态辅助方法中,除非必须操作UI,否则一律使用 context.applicationContext
  2. 拥抱 Lifecycle 感知组件:使用 LiveDataFlowLifecycleObserver,它们能自动管理生命周期,避免手动解绑的疏漏。
  3. Activity/Fragment 的引用做“消毒” :在需要传递或缓存的场景,使用 WeakReference,并养成先判空再使用的习惯。
  4. 代码审查与团队规范:将内存泄漏检查(如“非静态内部类”、“Handler”、“未反注册”)纳入代码审查清单。

22、常见的垃圾回收器

71a3dbb3-f1d7-46dd-8aa7-6aa72b277da1.png