文章目录
- 一、Java 基础与进阶
- 二、 Android 核心组件与机制
- 三、性能优化
- 四、架构与设计模式
- 五、Framework 与原理(中高级/资深岗)
- 六、开源框架与新技术
- 七、场景题与开放性问题
一、Java 基础与进阶
1. 面向对象 (OOP)
1.1 面向对象的三大特性
1.封装
概念
- 将对象的**数据(属性)和操作数据的方法(行为)**捆绑在一起,并对外部隐藏对象内部实现的细节。它通过访问控制符(如 private,
protected, public)来限制对内部数据的直接访问,通常提供公共的 getter 和 setter 方法来间接操作数据。
作用
- 数据安全:防止外部代码随意修改对象的内部状态,保证数据的完整性和一致性。
- 降低耦合:使用者只需知道如何调用公共接口,无需关心内部实现,当内部实现改变时,只要接口不变,外部代码就不需要修改。
- 提高内聚:将相关的数据和方法组织在一个类中,使类的职责更单一、更清晰。
2.继承 (Inheritance)
概念
- 继承允许一个类(子类/派生类)基于另一个类(父类/基类)来创建。子类会自动拥有父类的所有非私有(private)成员(属性和方法),并可以添加自己特有的成员,或重写
(Override) 父类的方法以提供不同的实现
作用
- 代码复用:避免重复编写相同的代码,子类可以直接使用父类已有的功能。
- 建立层次关系:通过“is-a”关系对现实世界进行建模,使代码结构更符合人类思维。
- 扩展功能:在不修改父类代码的前提下,通过子类增加新功能或修改现有功能。
3.多态 (Polymorphism)
概念
- 多态是指同一个接口或父类引用,可以指向不同子类的对象,并在运行时调用实际对象所属类的方法。简单说,就是“一个接口,多种实现”。多态的核心是动态绑定 (Dynamic Binding) 或后期绑定 (LateBinding),即方法调用的决定是在程序运行时根据对象的实际类型做出的,而不是在编译时。
作用
- 提高灵活性和可扩展性:代码可以编写得更通用,不依赖于具体的实现类。新增子类时,无需修改使用父类引用的代码。
- 解耦:调用者与具体实现者分离,降低了模块间的依赖。
4.总结
| 特性 | 核心思想 | Android引用举例 | 作用 |
|---|---|---|---|
| 封装 | 隐藏实现,暴露接口 | Activity 生命周期、自定义 View、数据模型类 | 安全、解耦、易维护 |
| 继承 | 代码复用,建立层次 | 创建 Activity/Fragment、自定义 View、BaseActivity | 复用、扩展、结构化 |
| 多态 | 一个接口,多种实现 | View 事件分发、Adapter、回调接口、设计模式 | 灵活、可扩展、高内聚低耦合 |
这三大特性相辅相成,共同构成了 Android 应用健壮、灵活、可维护的代码基础。熟练掌握并灵活运用它们,是成为一名优秀 Android 开发者的必经之路。
1.2 接口和抽象类
1.抽象类 (Abstract Class):
- 概念 一个被 abstract 关键字修饰的类,它可以包含抽象方法(无实现)和具体方法(有实现),也可以包含成员变量(实例变量、静态变量等)。
- 设计意图:“是什么” (IS-A)。它代表一个不完整的、通用的基类,用于捕捉子类的共同特征(属性和行为),并为子类提供部分默认实现。抽象类强调的是类的继承关系和代码复用。
- 举例:Animal(动物)可以是一个抽象类,它定义了所有动物共有的属性(如 name)和行为(如 sleep() 的默认实现),以及必须由子类具体实现的抽象行为(如 makeSound())。
- 抽象类的核心作用
1.代码复用:为子类提供公共的属性和方法实现,避免重复代码。
2.强制规范:通过抽象方法强制要求子类实现特定功能。
3.定义模板:结合具体方法和抽象方法,可以实现模板方法模式,定义算法的骨架,将某些步骤延迟到子类中实现。
2. 接口 (Interface):
- 概念:一个被 interface 关键字修饰的完全抽象的“契约”或“协议”。在 Java 8 之前,它只能包含常量和抽象方法;Java 8 及以后,可以包含默认方法(default)和静态方法(static)。
- 设计意图:“能做什么” (HAS-A / CAN-DO)。它定义了一组行为规范,规定了实现该接口的类必须具备哪些能力或方法,而不关心这些类的内部状态或具体实现细节。接口强调的是功能的定义和解耦。
- 举例:Flyable(可飞行的)、Runnable(可运行的)、Comparable(可比较的)。一个 Bird 类和一个 Airplane 类可以毫无继承关系,但它们都可以实现 Flyable 接口,表明它们都“能飞”。
- 接口的核心作用
1.实现“多继承”:这是接口最独特的优势。Java 类不支持多继承,但一个类可以实现多个接口,从而获得多种能力。
2.定义契约/规范:明确地规定了实现类必须提供的方法,是团队协作和模块化开发的基础。
3.解耦与依赖倒置:客户端代码依赖于接口,而不是具体的实现类,大大降低了模块间的耦合度,提高了系统的灵活性和可维护性。
4.支持多态性:可以通过接口类型的引用来调用不同实现类的对象,实现运行时的动态绑定。
5.便于测试与扩展:易于编写 Mock 对象进行单元测试;添加新功能时,可以定义新接口,而不影响现有代码。
3.总结
| 接口 | 抽象类 | |
|---|---|---|
| 实现 | 子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 | 子类使用extends关键字继承抽象类。子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 |
| 构造器 | 接口不能有构造器 | 抽象类可以有构造器 |
| 与正常J类的区别 | 接口是完全不同的类型 | 除了你不能实例化抽象类之外,它和普通Java类没有任何区别 |
| 访问修饰符 | 接口方法默认修饰符是public,不可以使用其它修饰符 | 抽象方法可以有public、protected和default这些修饰符 |
| main方法 | 方法 接口没有main方法,因此我们不能运行它 | 抽象方法可以有main方法并且我们可以运行它 |
| 继承与实现 | 可以实现多个接口 | 只能继承一个抽象类 |
| 速度 | 接口是稍微有点慢的,它需要时间去寻找在类中实现的方法 | 速度较快 |
| 添加新方法 | 接口中添加方法,那么你必须改变实现该接口的类 | 抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码 |
1.3 内部类
1.分类
Java 内部类是一个功能强大且灵活的特性,它通过提供更细粒度的封装和组织方式,帮助开发者构建更优雅、更健壮的代码。理解四种内部类的区别和适用场景是关键:
- 成员内部类:需要紧密访问外部类实例成员时使用。
- 静态内部类:逻辑相关但不需要访问外部类实例成员时使用,是避免内存泄漏的首选。
- 局部内部类:在单个方法内需要定义一个较复杂的、可能被多次实例化的类时使用。
- 匿名内部类:用于快速实现接口或继承类,创建一次性使用的对象,尤其在事件监听和回调中应用广泛
2.核心作用与优势
- 增强封装性 (Enhanced Encapsulation):
可以将内部类设为 private,完全对外部世界隐藏。外部类可以控制哪些内部类对外暴露
内部类可以自由访问外部类的私有成员,使得它们能紧密协作,同时保持外部类成员对其他外部类的封装性。 - 实现“多重继承”的效果
Java 不支持类的多继承,但内部类可以独立地继承一个类或实现多个接口,而外部类可以继承另一个类。这在一定程度上模拟了多继承。 - 解决命名冲突
当外部类继承的父类和实现的接口中有同名方法时,可以通过在内部类中实现接口,从而避免冲突。 - 实现回调和事件处理 (Callback & Event Handling)
匿名内部类是实现事件监听器和回调机制的首选方式,代码简洁且能直接访问外部类状态。 - 代码组织与逻辑内聚 (Code Organization & Cohesion)
将逻辑上紧密相关的类放在一个外部类中,使代码结构更清晰,减少顶层类的数量,提高可读性和可维护性。 - 设计模式支持
方便实现如工厂模式、策略模式、观察者模式等。例如,静态内部类常用于单例模式;成员内部类可用于实现观察者。
3.使用内部类的注意事项
- 内存泄漏风险 (Memory Leak)
如果内部类对象的生命周期比外部类长(例如,内部类对象被传递给一个长生命周期的对象,如静态变量、线程池、Handler 等),就会导致外部类对象无法被垃圾回收,造成内存泄漏。可以通过 WeakReference 来持有外部类的弱引用 - 序列化问题
非静态内部类默认持有外部类引用,如果尝试序列化非静态内部类,也会尝试序列化其外部类。如果外部类没有实现 Serializable,则会抛出异常。通常,内部类很少需要被序列化 - 代码复杂性
过度使用内部类,尤其是嵌套多层的内部类,会使代码结构变得复杂,难以理解和调试。应遵循“简单优于复杂”的原则
2. 集合框架
2.1 ArryList和linkedList
1.核心区别:底层数据结构
ArrayList
- 底层结构:基于动态数组(Resizable Array)实现。
- 内存布局:元素在内存中连续存储,就像一个可以自动扩容的数组。
- 扩容机制:当元素数量超过当前数组容量时,会创建一个更大的新数组(通常是原容量的1.5倍),并将所有旧元素复制过去。
LinkedList
- 底层结构:基于双向链表(Doubly Linked List)实现。
- 内存布局:每个元素(称为节点
Node)包含三部分:数据本身、指向前一个节点的引用(prev)和指向后一个节点的引用(next)。节点在内存中分散存储,通过引用来连接。 - 扩容机制:无需扩容。添加新元素时,只需创建一个新节点并调整相邻节点的引用即可。
2. 性能对比 (时间复杂度)
| 操作 | ArrayList | LinkedList | 说明 |
|---|---|---|---|
| 随机访问 (get/set by index) | O(1) | O(n) | ArrayList 可以通过索引直接计算内存地址,速度极快。LinkedList 必须从头或尾开始遍历链表,直到找到目标位置,速度慢。 |
| 尾部插入/删除 (add/remove last) | O(1) (平均) | O(1) | ArrayList 在尾部操作通常很快,但如果触发扩容,成本为 O(n)。LinkedList 只需修改尾节点的引用,始终很快 |
| 头部插入/删除 (add/remove first) | O(n) | O(1) | ArrayList 在头部插入或删除,需要将后面所有元素向前或向后移动一位。LinkedList 只需修改头节点的引用,速度极快 |
| 中间插入/删除 (by index) | O(n) | O(n) | 两者都需要先找到指定位置(ArrayList 是 O(1),LinkedList 是 O(n)),然后 ArrayList 需要移动元素,LinkedList 需要修改引用。虽然修改引用比移动元素快,但查找位置的成本主导了总时间,因此平均来看两者复杂度相同。但在实践中,对于靠近两端的操作,LinkedList 通常更快;对于靠近中间的操作,ArrayList 可能更快(因为移动元素是内存块拷贝,而链表遍历涉及多次指针跳转) |
| 迭代遍历 (Iterator) | 较快 | 较慢 | ArrayList 的元素在内存中连续,CPU 缓存命中率高(空间局部性好)。LinkedList 的节点分散,缓存命中率低 |
3. 内存占用对比
ArrayList
- 内存开销较小:主要存储元素数据本身。虽然数组可能有未使用的预留空间(为了减少扩容频率),但总体开销相对紧凑。
- 内存布局连续:有利于缓存性能。
LinkedList
- 内存开销较大:每个节点除了存储元素数据,还需要额外存储两个引用(prev 和 next),这在存储大量小对象时,内存开销会显著增加。
- 内存布局分散:不利于缓存性能。
4. 特有功能
LinkedList:
除了实现 List 接口,还实现了 Deque (Double-ended Queue) 接口,因此,它提供了一系列针对头尾操作的高效方法,使其可以方便地用作栈(Stack)、队列(Queue)或双端队列(Deque)。
- addFirst(E e) / addLast(E e)
- getFirst() / getLast()
- removeFirst() / removeLast()
- offerFirst(E e) / offerLast(E e) (JDK 1.6+,空集合时返回 null 而非异常)
- peekFirst() / peekLast()
- pollFirst() / pollLast()
ArrayList
没有针对头尾操作的特殊优化方法,所有位置的操作都通过通用的 add(index, E e) 和 remove(int index) 等方法实现。
2.2 HashMap 和HashTable
1.核心区别概览
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | ❌ 非线程安全。在多线程环境下使用可能导致数据不一致或死循环。 | ✅ 线程安全。所有公共方法都用 synchronized 关键字修饰,同一时间只允许一个线程访问。 |
| 性能 | ⚡ 性能高。因为没有同步开销,在单线程环境下速度远超 Hashtable。 | 🐢 性能低。由于每个方法都加锁,导致并发性能极差。 |
| Null 值支持 | ✅ 允许 null。允许一个 null 键和任意多个 null 值。 | ❌ 不允许 null。键或值为 null 时会抛出 NullPointerException。 |
| 出现时间 | JDK 1.2 引入。 | JDK 1.0 就已存在,是早期的遗留类。 |
| 继承关系 | 继承自 AbstractMap 类。 | 继承自已废弃的 Dictionary 类。 |
| 推荐替代方案 | 在需要线程安全时,推荐使用 ConcurrentHashMap。 | 无。官方不推荐在新代码中使用。 |
2.深入对比详解
1. 线程安全性与性能
Hashtable
- 实现方式:通过在每个公共方法(如
put(),get(),remove())上添加synchronized关键字来实现线程安全。 - 缺点:这是一种全局锁机制。当一个线程在访问 Hashtable 时,会锁住整个对象,其他所有线程都必须等待,导致并发性能极低。
- 适用场景:仅适用于对性能要求不高、且需要简单线程安全的极少数遗留场景。
HashMap
- 实现方式:没有任何同步机制,追求极致的单线程性能。
- 多线程风险:
- 数据不一致:多个线程同时写入可能导致数据丢失或覆盖。
- 死循环(JDK 1.7 及以前):在并发扩容时,链表可能形成环形结构,导致
get()操作陷入无限循环。 - 快速失败 (Fail-Fast):其迭代器是 fail-fast 的。在迭代过程中,如果其他线程修改了 HashMap 的结构(如添加、删除元素),会抛出
ConcurrentModificationException异常,这是一种安全预警机制。
- 解决方案:
- 外部同步:使用
Collections.synchronizedMap(new HashMap<...>())包装,但这同样是全局锁,性能与 Hashtable 类似。 - 推荐方案:直接使用
ConcurrentHashMap。它采用分段锁(JDK 7)或 CAS + synchronized(JDK 8+)等更精细的并发控制策略,在保证线程安全的同时,大幅提升了并发性能。
- 外部同步:使用
2. 对 Null 键和 Null 值的支持
HashMap
- 设计上允许
null作为键或值。 null键会被特殊处理,存储在哈希表数组的第 0 个位置(table[0])。- 允许存在多个
null值。 - 这为编程提供了更大的灵活性。
Hashtable
- 严格禁止
null键或null值。 - 在
put或get方法内部会进行null检查,一旦发现null,立即抛出NullPointerException。 - 这种设计源于其早期的实现和对“键必须有效”的严格要求。
3. 继承关系与历史
Hashtable
- 继承自
Dictionary类。Dictionary是一个非常古老的抽象类,在现代 Java 中已被标记为@Deprecated(不推荐使用)。 - 是 Java 集合框架出现之前的产物。
HashMap
- 继承自
AbstractMap类,这是 Java 集合框架(JCF)中为 Map 实现提供的标准抽象基类。 - 是作为 Hashtable 的现代、高性能替代品被引入的。
4. 迭代器 (Iterator)
HashMap
- 使用
Iterator进行遍历。 Iterator是 fail-fast 的。如上所述,在迭代过程中检测到结构修改会立即失败并抛出异常,有助于快速发现问题。
Hashtable
- 除了支持
Iterator,还保留了旧的Enumeration接口(通过elements()和keys()方法获取)。 - 其
Iterator不是 fail-fast 的,在迭代过程中即使结构被修改,也可能不会抛出异常,导致结果不可预测。
5. 初始容量与扩容机制
| 特性 | HashMap | Hashtable |
|---|---|---|
| 默认初始容量 | 16 | 11 |
| 容量要求 | 必须是 2 的幂。这使得在计算元素存储位置时,可以用位运算 (&) 替代取模运算 (%),效率更高。 | 无特殊要求,可以是任意整数。 |
| 扩容方式 | 容量变为原来的 2 倍。 | 容量变为原来的 2 倍 + 1(即 old * 2 + 1)。这种设计旨在使容量尽量为奇数或质数,以期在使用取模运算时减少哈希冲突。 |
6. 哈希值计算
Hashtable
- 直接使用对象的
hashCode()方法返回的值。 - 计算存储位置:
index = (hashCode & 0x7FFFFFFF) % capacity。
HashMap
- 对
hashCode()的结果进行了一次扰动函数(hash function)处理,目的是让哈希值的高位也参与到低位的计算中,使哈希值分布更均匀,减少冲突。 - 计算存储位置:
index = hash & (capacity - 1)(得益于容量是 2 的幂,位运算效率极高)。
7. 底层数据结构演进 (JDK 8+)
HashMap
- 在 JDK 8 中进行了重大优化。当链表长度超过阈值(默认 8)且数组长度大于 64 时,链表会转换为红黑树。这使得在极端哈希冲突情况下,查询时间复杂度从 O(n) 降低到 O(log n),性能更稳定。
Hashtable
- 底层始终是数组 + 链表结构,没有引入红黑树优化。在发生严重哈希冲突时,性能会急剧下降。
2.3 String,StringBuilder,StringBuffer
String,StringBuilder以及StringBuffer这三个类之间有什么区别呢,自己从网上搜索了一些资料,有所了解了之后在这里整理一下,便于大家观看,也便于加深自己学习过程中对这些知识点的记忆,如果哪里有误,恳请指正。这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。
1.运行速度
StringBuilder > StringBuffer > String
- String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。
// 以下面一段代码为例:
String str="abc";
System.out.println(str);
str=str+"de";
System.out.println(str);
- 如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了。
- 因此,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
- .StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。另外,有时候我们会这样对字符串进行赋值
String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());
- 这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和 String str=“abcde”;是完全一样的,所以会很快,而如果写成下面这种形式那么JVM就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。
String str1="abc";
String str2="de";
String str=str1+str2;
2.线程安全
StringBuilder是线程不安全的,而StringBuffer是线程安全的
- 如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。
- 因此要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
3. 总结一下
- String:适用于少量的字符串操作的情况
- StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
- StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
3. 并发与多线程
3.1 线程
- Join() 线程加入,执行此方法的线程优先使用cpu
- Yeild() 线程释放资源使所有线程能有相等的机会使用cpu
- Sleep() 相当于让线程睡眠,交出CPU,让CPU去执行其他的任务(不会释放锁)。
- Wait()方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。wait是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以wait必须要写在synchronized代码块中,否则会报异常。
- notify方法,也需要写在synchronized代码块中,调用对象的这两个方法也需要先获得该对象的锁.notify,notifyAll,
唤醒等待该对象同步锁的线程,并放入该对象的锁池中.对象的锁池中线程可以去竞争得到对象锁,然后开始执行.如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来,并非随机唤醒;如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略;notify()或者notifyAll()调用时并不会真正释放对象锁,
必须等到synchronized方法或者语法块执行完才真正释放锁. - Interrupt() 处于阻塞状态(wait sleep)的线程调用时会抛出异常(InterruptedException )
- 守护线程 不会去实现系统的主要功能,主要用于监控、抓取系统资源明细和运行状态 等操作,如垃圾回收线程setDeamon(true) 在start方法前调用,守护线程中不要做
3.2 线程同步
- synchronized 是 Java 中最基础、最常用的同步机制,它通过获取和释放对象的“监视器锁”(MonitorLock)来实现线程间的互斥访问。
- volatile 是一个变量修饰符,它不提供任何互斥机制,而是通过内存屏障(MemoryBarrier)来保证 变量的可见性和禁止指令重排序。
| 特性 | synchronized | volatile |
|---|---|---|
| 作用对象 | 方法、代码块 | 变量 |
| 核心机制 | 互斥锁 (Monitor) | 内存屏障 (Memory Barrier) |
| 原子性 | 保证 (通过互斥实现) | 不保证 (仅保证单次读/写原子) |
| 可见性 | 保证 (进出同步块时刷新主内存) | 保证 (强制读写主内存) |
| 有序性 | 保证 (通过互斥和禁止重排序) | 保证 (通过内存屏障禁止重排序) |
| 线程阻塞 | 会阻塞 (未获取锁的线程进入阻塞状态) | 不会阻塞 (线程可以继续执行) |
| 性能开销 | 较大 (涉及操作系统,可能上下文切换) | 较小 (主要是内存屏障开销) |
| 适用场景 | 复杂的原子操作、临界区保护 | 简单的状态标志、一次性安全发布、DCL单例模式 |
共享变量:如果一个变量在多个线程中都使用到了,那么这个变量就是这几个线程的共享变量。
可见性:一个线程对共享变量的修改,能够及时地到主内存并且让其他的线程看到。
原子性:线程要么执行完整个代码块,要么完全不执行,不会被其他线程打断。
详见 Java中 synchronized 和 volatile 详解
3.2 线程池
在高并发的 Java 应用开发中,频繁地创建和销毁线程会带来巨大的系统开销(如内存分配、CPU 调度),并可能导致系统资源耗尽。线程池(Thread Pool)技术应运而生,它通过预先创建并管理一组可复用的线程,极大地优化了多线程任务的执行效率和资源利用率。
1.线程池的核心优势
- 降低资源消耗:通过复用已创建的线程,避免了频繁创建和销毁线程所带来的性能开销。
- 提高响应速度:任务到达时,无需等待线程创建,可立即由池中的空闲线程执行。
- 提高线程的可管理性:可以统一配置、监控和调优线程资源,避免无限制创建线程导致的系统不稳定(如 OOM)。
- 控制最大并发数:有效防止因过多线程并发执行而耗尽系统资源。
2.核心组件与工作原理
一个标准的 Java 线程池主要由以下几个部分组成:
核心组件
- ThreadPoolExecutor:线程池的核心实现类。
- 核心线程池 (corePoolSize):线程池中保持存活的最小线程数,即使它们处于空闲状态。
- 最大线程数 (maximumPoolSize):线程池中允许存在的最大线程数。
- 任务队列 (workQueue):用于存放待执行任务的阻塞队列。
- 线程工厂 (threadFactory):用于创建新线程的工厂。
- 拒绝策略 (handler):当线程池和任务队列都满时,用于处理新提交任务的策略。
工作流程
当一个新任务被提交到线程池时,其处理流程如下:
- 判断核心线程数:如果当前运行的线程数 小于
corePoolSize,则立即创建一个新线程来执行该任务,无论是否有空闲线程。 - 加入任务队列:如果当前运行的线程数 大于或等于
corePoolSize,则尝试将任务放入workQueue队列中。 - 判断最大线程数:如果队列已满,且当前运行的线程数 小于
maximumPoolSize,则创建一个非核心线程来执行该任务。 - 执行拒绝策略:如果队列已满,且当前运行的线程数 等于
maximumPoolSize,则触发RejectedExecutionHandler拒绝策略来处理该任务。
2.ThreadPoolExecutor 核心参数详解
ThreadPoolExecutor 提供了非常灵活的构造函数,允许开发者精确控制线程池的行为。
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 存活时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
3.线程池最佳实践
CPU 密集型任务
- 线程数 ≈ CPU 核心数 + 1。
- 过多的线程会导致频繁的上下文切换,反而降低效率。
I/O 密集型任务
- 线程数可以设置得更大,因为线程在等待 I/O 时会释放 CPU。
- 一个常见的公式是:线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均计算时间)。
- 也可以根据压测结果进行调整。
混合型任务
- 可以将任务拆分,分别用不同类型的线程池处理。
选择合适的任务队列
- 优先使用有界队列(如
ArrayBlockingQueue),并设置合理的容量,以防止资源耗尽。 - 避免在生产环境使用无界队列(
LinkedBlockingQueue),除非你能完全掌控任务的提交速率。
4. JVM 相关
Java 虚拟机(JVM,Java Virtual Machine)是 Java 平台的核心和基石,它使得 Java 语言能够实现“一次编写,到处运行”(Write Once, Run Anywhere)的跨平台特性。JVM 并非一个真实的物理机器,而是一个由软件模拟的、运行在操作系统之上的抽象计算机
4.1 JVM 的核心作用
1. 跨平台性
- 核心价值:JVM 最核心的作用是实现跨平台运行。
- 机制:开发者只需编写一次代码,编译成字节码后,任何安装了对应操作系统版本 JVM 的机器都能运行该程序。
- 角色:JVM 充当“翻译官”,将统一的字节码“翻译”成不同平台的本地机器码。
2. 字节码执行引擎
- 运行引擎:JVM 是 Java 程序的运行引擎。
- 执行方式:
- 通过解释器逐条执行字节码指令。
- 通过即时编译器(JIT Compiler)将频繁执行的“热点代码”编译成本地机器码,大幅提升执行效率。
3. 自动内存管理
- 机制:JVM 提供自动化的内存管理机制,最著名的是垃圾回收(Garbage Collection, GC)。
- 作用:自动追踪和回收堆中不再被引用的对象所占用的内存,减轻开发者手动管理内存的负担,有效避免内存泄漏(但不能完全杜绝)。
4. 安全性
- 机制:JVM 通过“沙箱”(Sandbox)机制对运行的代码进行安全检查。
- 作用:防止恶意代码破坏系统资源或进行未授权操作。
5. 多线程支持
- 原生支持:JVM 原生支持多线程并发执行。
- 管理职责:负责管理线程的生命周期、调度和同步,是构建高性能并发应用的基础。
6. 动态类加载
- 机制:JVM 的类加载器(Class Loader)可以在程序运行时动态地加载和链接所需的类。
- 作用:为程序的灵活扩展和热部署提供可能。
4.2 JVM 的内部结构(运行时数据区)
JVM 在执行 Java 程序时,会将其管理的内存划分为若干个不同的区域,这些区域各有各的用途。
1. 程序计数器 (Program Counter Register)
- 线程私有。
- 作用:记录当前线程正在执行的字节码指令的地址。当线程被切换回来时,能知道从哪里继续执行。
- 特点:是唯一一个在《Java虚拟机规范》中没有规定任何
OutOfMemoryError情况的区域。
2. Java 虚拟机栈 (Java Virtual Machine Stack)
- 线程私有,生命周期与线程相同。
- 作用:存储方法执行时的“栈帧”(Stack Frame)。每个方法被调用时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 异常:
- 线程请求的栈深度大于 JVM 允许的深度,会抛出
StackOverflowError。 - 如果栈可以动态扩展,但在扩展时无法申请到足够的内存,则会抛出
OutOfMemoryError。
- 线程请求的栈深度大于 JVM 允许的深度,会抛出
3. 本地方法栈 (Native Method Stack)
- 线程私有。
- 作用:与虚拟机栈类似,但它服务于 JVM 调用的 Native 方法(用 C/C++ 编写的本地方法)。
- 异常:与虚拟机栈相同,可能抛出
StackOverflowError或OutOfMemoryError。
4. Java 堆 (Java Heap)
- 线程共享,是 JVM 管理的内存中最大的一块。
- 作用:存放几乎所有对象实例和数组。垃圾回收的主要区域,因此也被称为“GC堆”。
- 结构:在主流的 JVM 实现(如 HotSpot)中,堆被细分为:
- 新生代 (Young Generation):新创建的对象首先分配在这里。
- Eden 区:对象的“出生地”。
- Survivor 区 (S0, S1):经过垃圾回收后存活的对象会从 Eden 区移动到这里。
- 老年代 (Old Generation):在新生代中经历了多次垃圾回收后仍然存活的对象会被晋升到这里。
- 新生代 (Young Generation):新创建的对象首先分配在这里。
- 异常:当堆中没有足够空间分配实例,并且堆也无法再扩展时,会抛出
OutOfMemoryError。
5. 方法区 (Method Area) / 元空间 (Metaspace)
- 线程共享。
- 作用:存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
- 演变:
- 在 JDK 8 之前,方法区的实现被称为“永久代”(Permanent Generation)。
- 从 JDK 8 开始,“永久代”被移除,取而代之的是“元空间”(Metaspace)。元空间使用的是本地内存(Native Memory),而不是 JVM 的堆内存,理论上只受本机物理内存限制。
- 异常:当方法区/元空间无法满足新的内存分配需求时,会抛出
OutOfMemoryError。
6. 运行时常量池 (Runtime Constant Pool)
- 方法区的一部分。
- 作用:存放编译期生成的各种字面量(如字符串
"Hello")和符号引用(如类和接口的全限定名、字段和方法的名称及描述符)。在 JDK 8 之后,字符串常量池被移到了堆中。 - 异常:当常量池无法再申请到内存时,会抛出
OutOfMemoryError。
7. 直接内存 (Direct Memory)
- 不属于 JVM 运行时数据区,但被频繁使用。
- 作用:通过
java.nio包中的DirectByteBuffer对象可以分配直接内存。这部分内存不受 JVM 堆大小限制,但受本机总物理内存限制。它在进行 I/O 操作(如 NIO)时能显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。 - 异常:分配失败时会抛出
OutOfMemoryError。
类加载机制和双亲委派模型。
5. 泛型与反射
5.1Java 泛型 (Generics)
泛型的核心思想是参数化类型,即在定义类、接口或方法时,不指定具体的类型,而是使用一个“类型参数”(如 , , <K, V>)来代替。在使用时,再传入具体的类型。
-
主要目的
类型安全 (Type Safety):编译器可以在编译时检查类型,避免在运行时出现 ClassCastException。例如,List 只能存放 String 类型的对象,如果尝试放入 Integer,编译器会直接报错。
消除强制类型转换:在使用泛型之前,从集合中取出对象时,通常需要进行强制类型转换(如 (String) list.get(0))。泛型消除了这种繁琐且易错的操作。
提高代码复用性:可以编写适用于多种数据类型的通用算法和数据结构。 -
核心机制:类型擦除 (Type Erasure)这是理解泛型与反射关系的关键。
定义:为了保持与早期 Java 版本的二进制兼容性,Java 编译器在编译泛型代码时,会将所有的泛型类型参数信息擦除,替换为其限定类型(通常是 Object)或第一个边界。
编译期 vs 运行期:
编译期:编译器利用泛型信息进行严格的类型检查。
运行期:JVM 看不到泛型信息。对于 JVM 而言,ArrayList 和 ArrayList 是同一个类 ArrayList。 -
类型擦除带来的限制
不能创建泛型类型的实例:new T() 是非法的。
不能创建泛型数组:new T[10] 是非法的。
不能使用 instanceof 检查具体的泛型类型:obj instanceof List 是非法的,只能写 obj instanceof List。
5.2 Java 反射 (Reflection)
反射是 Java 在运行时动态获取类、接口、字段、方法等信息,并能动态调用对象方法、修改字段值的能力。它打破了编译时的类型检查限制。
- 主要用途
框架开发:Spring, Hibernate 等框架大量使用反射来实现依赖注入、ORM 映射等功能。
动态代理:创建代理对象,在不修改源码的情况下增强方法功能。
调试与测试工具:访问私有成员进行单元测试。
通用工具类:编写能处理任意类型对象的通用序列化/反序列化、拷贝等工具。 - 核心 API
Class:代表一个类的运行时类型信息。
Field:代表类的成员变量。
Method:代表类的方法。
Constructor:代表类的构造器。 - 反射与类型擦除的“矛盾”
由于类型擦除,通过标准的反射 API 获取到的类型通常是原始类型(Raw Type),而不是具体的泛型类型。
5.3 特性对比总结
| 特性 | 泛型 (Generics) | 反射 (Reflection) |
|---|---|---|
| 主要作用时期 | 编译期 (Compile-time) | 运行期 (Runtime) |
| 核心目的 | 提供编译时类型安全,消除强制转换,提高代码复用性 | 在运行时动态获取和操作类、方法、字段等信息 |
| 关键机制 | 类型擦除 (Type Erasure) | Class, Field, Method, Constructor 等 API |
| 相互关系 | 类型擦除使得标准反射 API 无法直接获取泛型实例的具体类型 | 可通过 Type 接口(如 ParameterizedType)获取在声明处(字段、方法、父类)的泛型信息,从而部分“突破”类型擦除的限制 |
简单来说:
- 泛型是给编译器用的,用于在写代码时保证安全;
- 反射是给程序在运行时用的,用于动态地了解和操作对象。
- 虽然泛型信息在运行时大部分被擦除,但 Java 通过
java.lang.reflect.Type体系保留了关键的声明信息,使得反射在特定场景下依然能够与泛型协同工作,这为构建高度灵活的框架和工具提供了可能。
6. java 类双亲委托机制
双亲委派机制的核心思想是:当一个类加载器收到加载某个类的请求时,它不会立即尝试自己去加载,而是先将这个请求“委托”给它的父类加载器去处理。只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载
6.1类加载器的层级结构
在 Java 中,类加载器以树状的父子层级结构组织,主要包含以下类加载器:
1. 启动类加载器 (Bootstrap ClassLoader)
- 地位:位于最顶层,是 JVM 的一部分。
- 实现:由 C++ 语言实现,是 JVM 自身的一部分,非
java.lang.ClassLoader的子类。在 Java 代码中获取其引用时结果为null。 - 职责:加载 Java 最核心的类库(如
<JAVA_HOME>/jre/lib下的rt.jar、resources.jar、charsets.jar等)。例如,java.lang.Object、java.lang.String等类由其加载。
2. 扩展类加载器 (Extension ClassLoader)
- 地位:Bootstrap ClassLoader 的子加载器。
- 实现:由 Java 语言编写,是
java.lang.ClassLoader的子类。 - 职责:加载
<JAVA_HOME>/jre/lib/ext目录或系统变量java.ext.dirs指定路径下的 JAR 包。
3. 应用程序类加载器 (Application ClassLoader)
- 地位:Extension ClassLoader 的子加载器,也称为“系统类加载器”。
- 实现:由 Java 语言编写。
- 职责:加载用户类路径(ClassPath)指定的类库(如开发者编写的代码及第三方 JAR 包)。开发者可直接在代码中使用该加载器。
4. 自定义类加载器 (Custom ClassLoader)
- 地位:通常为 Application ClassLoader 的子加载器。
- 实现:通过继承
java.lang.ClassLoader类实现。 - 职责:满足特定需求,例如:
- 加载网络上的类。
- 实现热部署。
- 为不同应用模块提供隔离的类加载环境(如 Tomcat 为每个 Web 应用创建独立类加载器)。
6.2 工作流程
当类加载器(如应用程序类加载器)收到加载 com.example.MyClass 的请求时,流程如下:
- 向上委托:应用程序类加载器将请求委托给父加载器(扩展类加载器)。
- 继续向上:扩展类加载器将请求委托给父加载器(启动类加载器)。
- 顶层尝试:启动类加载器在
jre/lib核心类库中查找com.example.MyClass(失败,因用户类不存在于此)。 - 向下传递:请求返回扩展类加载器,在其负责的
jre/lib/ext目录下查找(失败)。 - 子加载器出手:请求回到应用程序类加载器,在 ClassPath 路径下查找并加载(成功则返回,失败则抛出
ClassNotFoundException)。
概括:自底向上检查,自顶向下加载。
6.3 为什么需要双亲委派机制?
双亲委派机制带来以下关键好处:
1. 保障核心类库的安全性 (Security)
- 问题:若无此机制,恶意开发者可在 ClassPath 下定义
java.lang.String并植入恶意代码。 - 解决:加载请求委托至启动类加载器,优先加载官方核心类,防止 API 被篡改。
2. 避免类的重复加载 (Avoid Duplicate Loading)
- 机制:父加载器已加载的类会直接返回,避免子加载器重复加载。
- 意义:保证 JVM 中类的唯一性,节省内存,避免类型转换异常。
3. 保证类加载的一致性 (Consistency)
- 机制:层级结构确保同一类由同一“权威”加载器加载。
- 意义:维护类定义的一致性,避免因不同加载器导致冲突。
二、 Android 核心组件与机制
1. 四大组件
1.1 Activity:
1. 核心作用
- 用户交互的入口,每个界面对应一个 Activity
- 管理界面生命周期(创建、暂停、销毁等)
- 通过 Intent 实现页面跳转和数据传递
2 activity架构层级
在 Android 的 Activity 中,Window、DecorView 和 Layout 是构成用户界面的三个核心层级,它们共同协作,将您在 XML 布局文件中定义的 UI 最终呈现到屏幕上
-
Window (窗口) 是一个抽象概念,代表了一个顶层窗口,它负责管理视图的显示区域、背景、标题栏(如果存在)、输入事件分发等。每个 Activity
都会关联一个 Window 对象(通常是 PhoneWindow 类的实例) -
DecorView (装饰视图): DecorView 是 Window 中最顶层的 ViewGroup,它是整个 Activity 视图树的根节点。您可以把它理解为一个“画框”,它包含了系统装饰部分(如状态栏、标题栏)和您自己的内容部分
-
Layout (布局) 这是开发者最熟悉的层级,指的就是您在 res/layout/ 目录下创建的 XML 布局文件(如 activity_main.xml),或者通过 Java/Kotlin 代码动态创建的 View 或 ViewGroup

3. 生命周期方法
- 生命周期方法是activity的重中之重,如下图所示:

- 触发场景如下表格:
| 方法 | 触发场景 |
|---|---|
| onCreate() | Activity 首次创建时调用 |
| onStart() | Activity 对用户可见时调用 |
| onResume() | Activity 获得焦点(可交互)时调用 |
| onPause() | Activity 失去焦点(如弹出对话框)时调用 |
| onStop() | Activity 不可见时调用 |
| onDestroy() | Activity 被销毁时调用 |
- activityA 中打开activityB
A onPause()—> B onCreate() ---->B onStart() —> B o’nResume() —> A onstop()
- 再点返回activityA的生命周期函数调用
B onPause() —>A onRestart() —> A onStart() —>A onResume() —>
B onStop —> B onDestroy()
- A 中打开B如果B是透明的activity,则不会执行a的onstop的restart以及start方法(同dialog)
A onPause()—> B onCreate() ---->B onStart() —> B o’nResume()
- 再点返回A的生命周期函数调用
B onPause() —>A onResume() —> B onStop —> B onDestroy()
Activity Result API(回传值)
步骤 1:在第一个 Activity 中注册回调使用 ActivityResultLauncher 启动第二个 Activity,并处理返回结果:
public class FirstActivity extends AppCompatActivity {
// 1. 定义 Activity Result Launcher
private ActivityResultLauncher<Intent> someActivityResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
if (data != null) {
String returnedValue = data.getStringExtra("key_return");
Toast.makeText(this, "回传值: " + returnedValue, Toast.LENGTH_SHORT).show();
}
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_first);
// 2. 启动第二个 Activity
findViewById(R.id.button).setOnClickListener(v -> {
Intent intent = new Intent(this, SecondActivity.class);
someActivityResultLauncher.launch(intent);
});
}
}
步骤 2:在第二个 Activity 中设置回传值
通过 setResult() 返回数据,并结束当前 Activity:
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
findViewById(R.id.button_return).setOnClickListener(v -> {
// 1. 准备回传数据
Intent resultIntent = new Intent();
resultIntent.putExtra("key_return", "这是从SecondActivity返回的数据");
// 2. 设置结果并结束当前Activity
setResult(Activity.RESULT_OK, resultIntent);
finish();
});
}
}
关键点
ActivityResultLauncher:替代 startActivityForResult(),更安全且避免内存泄漏。
RESULT_OK:表示操作成功,也可用 RESULT_CANCELED 表示取消。
setResult():必须在 finish() 前调用。
注意事项
- 内存泄漏:未在 onDestroy() 中解绑监听器或释放资源
- 配置变更:屏幕旋转会导致 Activity 重建,需通过 onSaveInstanceState() 保存数据
1.2 Service:
1. 核心作用
- 在后台执行长时间任务(如播放音乐、下载文件),不依赖用户界面
- 分为 启动型服务(Started Service) 和 绑定型服务(Bound Service)如图所示:

启动服务和绑定服务对比
| 特性 | startService | bindService |
|---|---|---|
| 启动目的 | 长期后台运行(如音乐播放、数据同步) | 与调用者交互(如 Activity 调用 Service 方法) |
| 生命周期控制 | 独立于调用者,需手动调用 stopService() 或 stopSelf() 停止 | 依赖调用者,调用者解绑或销毁时自动解绑,无绑定时销毁 |
| 回调方法 | onCreate() → onStartCommand() → onDestroy() | onCreate() → onBind() → onUnbind() → onDestroy() |
| 重复调用行为 | 多次调用 startService() 仅触发 onStartCommand(),不重复创建 Service | 多次调用 bindService() 仅首次触发 onCreate() 和 onBind() |
| 调用者与 Service 关系 | 无直接交互,通过 Intent 传递数据 | 通过 ServiceConnection 获取 IBinder,直接调用 Service 方法 |
2 IntentService
IntentService 的原理
IntentService 是 Android 提供的一种特殊 Service,其核心原理基于 HandlerThread + 工作队列,通过以下机制实现后台任务处理
- 内部维护 HandlerThread
IntentService 在 onCreate() 方法中初始化一个 HandlerThread(本质是带有消息循环的子线程),并为其创建 Looper 和 Handler(即 ServiceHandler)。所有任务通过 ServiceHandler 的 handleMessage() 方法处理,确保任务在子线程中执行,避免阻塞主线程。 - 任务队列与串行执行
客户端通过 startService(Intent) 发送任务请求,IntentService 将每个请求封装为 Message,按顺序放入 HandlerThread 的消息队列。任务按 先进先出(FIFO) 原则串行执行,前一个任务完成后才会处理下一个,避免并发问题。 - 自动停止服务
每个任务处理完成后,IntentService 会调用 stopSelf(msg.arg1)(传入最后一次处理的消息 ID),确保所有任务完成后服务自动销毁,无需手动调用 stopService()。
IntentService 的适用场景
-
后台串行耗时任务
适合需要 按顺序执行 的多个后台任务(如批量上传图片、下载多个文件),避免并发导致的资源竞争或数据混乱。 -
避免阻塞主线程
替代在普通 Service 中手动创建线程的繁琐操作,自动将任务切换到子线程,防止 ANR(Application Not Responding)
对比其他方案
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| IntentService | 串行后台任务、自动生命周期管理 | 简单易用,自动销毁 | 无法并行,效率较低 |
| HandlerThread | 自定义线程管理 | 灵活控制线程优先级 | 需手动处理任务队列和销毁 |
| WorkManager | 延迟或周期性任务 | 支持后台限制和电量优化 | 适合非实时任务 |
| Kotlin 协程 | 复杂异步逻辑 | 代码简洁,支持并发 | 学习成本较高 |
3. 前台 Service
前台 Service 是 Android 中一种优先级更高的后台服务,通过在通知栏显示持续存在的通知来告知用户其正在运行,从而避免被系统轻易回收(尤其在低内存或后台限制严格的情况下)
前台service的作用
-
提升后台存活能力
在 Android 8.0(Oreo)及以上版本,系统对后台服务有严格限制(如后台执行时间限制、进程容易被杀死),前台 Service 通过通知栏的“常驻通知”向系统声明其重要性,降低被回收的概率,适合执行需要长期运行的任务 -
满足用户感知需求
系统要求前台 Service 必须显示通知,确保用户明确知晓后台正在进行的操作(如“正在导航”“正在录音”),避免隐私或功耗问题。
创建前台service
-
继承 Service 类
创建自定义 Service 类,重写 onStartCommand() 和 onBind()(若需绑定)。 -
启动前台 Service
在 onStartCommand() 中调用 startForeground(),传入通知 ID和通知对象。 -
定义通知渠道(Android 8.0+ 必需)
使用 NotificationChannel 为通知分类,提升用户体验。 -
处理停止逻辑
在任务完成时调用 stopForeground(true) 移除通知,并停止 Service。
代码示例
(1)定义通知渠道(Android 8.0+)
// MainActivity 或 Application 类中初始化通知渠道
private static final String CHANNEL_ID = "foreground_service_channel";
private static final String CHANNEL_NAME = "Foreground Service";
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
(2)创建前台 Service 类
public class ForegroundService extends Service {
private static final int NOTIFICATION_ID = 1;
@Override
public void onCreate() {
super.onCreate();
// 初始化逻辑(如加载资源)
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 1. 创建通知
Notification notification = buildNotification();
// 2. 启动前台 Service(必须调用)
startForeground(NOTIFICATION_ID, notification);
// 3. 执行后台任务(示例:模拟耗时操作)
new Thread(() -> {
// 模拟任务
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 任务完成后停止 Service
stopSelf();
}).start();
return START_STICKY; // 服务被杀死后自动重启
}
@Override
public IBinder onBind(Intent intent) {
return null; // 若无需绑定,返回 null
}
// 构建通知
private Notification buildNotification() {
Intent stopIntent = new Intent(this, ForegroundService.class);
stopIntent.setAction("STOP_SERVICE");
PendingIntent stopPendingIntent = PendingIntent.getService(
this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("前台服务运行中")
.setContentText("点击停止服务")
.setSmallIcon(R.drawable.ic_notification) // 必须设置图标
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.addAction(R.drawable.ic_stop, "停止", stopPendingIntent); // 添加停止按钮
return builder.build();
}
@Override
public void onDestroy() {
super.onDestroy();
// 移除通知(可选)
stopForeground(true);
}
}
(3)启动和停止 Service
// 启动 Service
Intent serviceIntent = new Intent(this, ForegroundService.class);
startService(serviceIntent);
// 停止 Service(通过广播或绑定方式)
// 示例:通过 Action 停止
Intent stopIntent = new Intent(this, ForegroundService.class);
stopIntent.setAction("STOP_SERVICE");
startService(stopIntent); // 实际需在 Service 中处理 Action
注意事项
- 通知必须可见
前台 Service 的通知不能被隐藏,否则系统会抛出 IllegalStateException。
通知需包含图标和标题,且在 Android 10+ 上需适配深色模式。 - Android 8.0+ 兼容性
必须调用 createNotificationChannel() 创建通知渠道,否则通知无法显示。 - 权限声明
在 AndroidManifest.xml 中声明 Service 和通知权限:
<service android:name=".ForegroundService" />
<!-- 若需后台定位等权限,需额外声明 -->
- 省电模式优化
在 Android 9+ 上,前台 Service 仍可能受省电策略限制,建议结合 WorkManager 或 JobScheduler 使用。
4. JobScheduler 和 WorkManager
- JobScheduler 是系统原生的高性能调度工具,但兼容性受限。
- WorkManager 是跨版本的“全能型”解决方案,覆盖了 JobScheduler 的功能并扩展了持久化、任务链等特性。
- 推荐:新项目优先使用 WorkManager;仅针对 Android 5.0+ 且对性能敏感的任务可考虑 JobScheduler。
核心区别分析
| 维度 | JobScheduler | WorkManager |
|---|---|---|
| API 兼容性 | 仅支持 Android 5.0+(API 21+) | 支持 Android 4.0+(API 14+) |
| 底层实现 | 系统原生调度服务 | 根据设备 API 动态选择: - API 23+:JobScheduler - API 14-22:AlarmManager + BroadcastReceiver |
| 任务可靠性 | 依赖系统调度,可能因资源限制延迟执行 | 持久化存储 + 自动重试,确保任务最终执行 |
| 功能扩展性 | 基础条件约束(网络、充电、空闲) | 支持任务链、输入/输出数据传递、优先级设置 |
| 开发复杂度 | 需手动定义 JobInfo 和 JobService | 通过 Worker 类简化任务定义,API 更简洁 |
| 适用场景 | 高性能节能调度,对电池寿命敏感的任务 | 跨版本兼容、持久化任务、复杂任务流程 |
代码示例
- JobScheduler 实现
// 定义 JobInfo
ComponentName componentName = new ComponentName(this, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(JOB_ID, componentName)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 仅 Wi-Fi 下执行
.setPersisted(true) // 设备重启后保留任务
.build();
// 调度任务
JobScheduler jobScheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo);
// 实现 JobService
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// 执行耗时任务
new Thread(() -> {
// 任务逻辑
jobFinished(params, false); // 通知系统任务完成
}).start();
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
// 任务取消逻辑
return true;
}
}
- WorkManager 实现
// 定义 Worker
public class MyWorker extends Worker {
public MyWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
// 执行耗时任务
return Result.success(); // 返回任务结果
}
}
// 定义约束条件
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // 仅 Wi-Fi 下执行
.setRequiresCharging(true) // 充电时执行
.build();
// 调度任务
WorkManager.getInstance(this).enqueue(
new OneTimeWorkRequest.Builder(MyWorker.class)
.setConstraints(constraints)
.build()
);
1.3 BroadcastReceiver
BroadcastReceiver(广播接收器)是 Android 四大组件之一,用于接收和响应来自系统或其他应用程序发出的广播消息(Intent)。它是一种全局的监听器,可以在应用未运行时被系统唤醒以处理特定事件。
1. 典型应用场景
- 监听系统事件:如开机完成 (BOOT_COMPLETED)、电量变化、网络状态改变、屏幕开关等。
- 应用间通信:一个应用发送广播,另一个应用接收并处理。
- 应用内通信:在应用的不同组件(如 Activity、Service)之间传递消息。
2. 广播类型
根据发送和接收方式,广播主要分为两大类:
| 广播类型 | 特点 | 优点 | 缺点 | 发送方法 |
|---|---|---|---|---|
| 标准广播 (Normal Broadcast) | 完全异步。广播发出后,所有符合条件的接收器几乎同时收到,没有先后顺序。 | 效率高 | 接收器无法截断(abort)广播,也无法将处理结果传递给下一个接收器。 | Context.sendBroadcast(Intent) |
| 有序广播 (Ordered Broadcast) | 同步执行。广播按照接收器声明的优先级(android:priority,数值越大优先级越高)依次接收。前面的接收器可以截断广播(调用 abortBroadcast()),使其不再传递;也可以通过 setResultData() / getResultData() 将数据传递给下一个接收器。 | 可以控制广播的传递流程。 | 效率相对较低。 | Context.sendOrderedBroadcast(Intent, String receiverPermission) |
3.注册方式
广播接收器必须注册才能生效,有两种注册方式:
-
静态注册 (Static Registration)
方式:在 AndroidManifest.xml 文件中使用 标签声明。
特点:常驻型:即使应用进程未启动,也能接收广播(除非应用被用户手动“强制停止”)。生命周期独立于应用组件。
适用于需要长期监听的系统广播(如开机启动)。 -
动态注册 (Dynamic Registration)
方式:在代码中(通常在 Activity 或 Service 的 onCreate() 中)使用 Context.registerReceiver(BroadcastReceiver, IntentFilter) 方法注册。
特点:跟随型:接收器的生命周期与注册它的组件(如 Activity)绑定。组件销毁时(如 Activity.onDestroy()),必须调用 unregisterReceiver() 手动注销,否则会造成内存泄漏。
灵活性高,可以根据应用状态动态注册和注销。
适用于应用运行期间的临时监听。
4.LocalBroadcastManager
传统的 BroadcastReceiver 是跨进程的,存在安全风险(其他应用可能监听或伪造你的广播)和性能开销(涉及 Binder
通信)因此,官方推出 LocalBroadcastManager。
特点:
- 仅限应用内部:广播的发送和接收都限制在当前应用进程内,其他应用无法收到。
- 更高效:基于 Handler 实现,不涉及跨进程通信。
- 更安全:避免了数据泄露和外部恶意广播的干扰。
用法:
// 发送本地广播
Intent intent = new Intent("com.example.MY_LOCAL_ACTION");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
// 注册本地广播接收器
IntentFilter localFilter = new IntentFilter("com.example.MY_LOCAL_ACTION");
LocalBroadcastManager.getInstance(this).registerReceiver(mLocalReceiver, localFilter);
// 注销本地广播接收器
LocalBroadcastManager.getInstance(this).unregisterReceiver(mLocalReceiver);
本地广播总结
| 要点 | 说明 |
|---|---|
| 优先使用本地广播 | 应用内通信首选 LocalBroadcastManager,安全高效。 |
| 谨慎使用静态注册 | 仅在需要应用未启动时接收广播的场景下使用(如开机启动),并注意 Android 8.0+ 的后台限制。 |
| 动态注册必须注销 | 在 Activity/Service 的 onDestroy() 中调用 unregisterReceiver(),防止内存泄漏。 |
| onReceive() 禁止耗时 | 10秒内必须完成,否则 ANR。耗时任务交给 Service。 |
| 明确指定接收者 | 发送自定义广播时,使用 setPackage() 或 setComponent(),避免广播被滥用。 |
| 注意权限 | 某些系统广播需要在 AndroidManifest.xml 中声明相应权限才能接收。 |
5.系统广播
开发中不可避免的要去接收系统的事件以完成业务的开发
| Intent Action 常量 | 说明 |
|---|---|
android.intent.action.BATTERY_CHANGED | 持久的广播,包含电池的充电状态、级别和其他信息 |
android.intent.action.BATTERY_LOW | 标识设备的低电量条件 |
android.intent.action.BATTERY_OKAY | 标识电池在电量低之后,现在已经恢复正常 |
android.intent.action.BOOT_COMPLETED | 在系统完成启动后广播一次 |
android.intent.action.BUG_REPORT | 显示报告bug的活动 |
android.intent.action.CALL | 执行呼叫数据指定的某人 |
android.intent.action.CALL_BUTTON | 用户点击"呼叫"按钮打开拨号器或者其他拨号的合适界面 |
android.intent.action.DATE_CHANGED | 日期发生改变 |
android.intent.action.REBOOT | 设备重启 |
1.4 ContentProvider:
1. 核心作用
-
跨应用数据共享:这是 ContentProvider 最核心的价值。它允许一个应用(数据提供方)将其私有数据(如 SQLite数据库、文件等)以受控的方式暴露给其他应用(数据使用方)。
-
统一数据访问接口:无论底层数据是存储在数据库、文件还是网络上,ContentProvider 都对外提供一套标准的 CRUD(Create, Read, Update, Delete)操作接口,即 insert(), query(), update(), delete() 方法。这使得数据使用者无需关心数据的具体存储位置和格式。
-
访问权限控制:通过在 AndroidManifest.xml 中设置权限(android:permission, android:readPermission, android:writePermission),可以精确控制哪些应用可以读取或写入数据,保障数据安全。
-
访问系统数据:Android 系统本身也通过 ContentProvider 向所有应用提供对系统数据的访问,例如联系人 (ContactsContract)、媒体库(图片、视频、音频)、日历、短信等。
2. 工作原理
- ContentProvider 基于 Client-Server 模型和 Binder IPC 机制工作。
- Server端:数据拥有方实现并注册一个 ContentProvider 子类,负责处理来自其他应用的请求。
- Client端:数据使用方应用通过 ContentResolver 对象向系统发起请求。
- Binder:系统底层通过 Binder 机制在不同进程间传递数据和调用,实现跨进程通信(IPC)。

3. URI (Uniform Resource Identifier)
URI 是 ContentProvider 的“地址”,用于唯一标识要访问的数据资源。其标准格式为:
content://<_authority>/<data_path>[/<_id>]
- content://:固定前缀,表明这是一个内容 URI。
- <_authority>:授权者,通常是提供该 ContentProvider 的应用的包名加上类名(如 com.example.myapp.provider),用于在系统中唯一标识是哪个 ContentProvider。
- <data_path>:数据路径,指明要访问的数据表或数据集(如 users, contacts)。
- [/<_id>]:可选,指定要访问的单条数据的唯一 ID。
示例:
content://com.example.myapp.provider/users:访问 users 表中的所有用户数据。
content://com.example.myapp.provider/users/5:访问 ID 为 5 的单个用户数据。
content://media/external/images/media:访问系统媒体库中的所有外部存储图片。
4. ContentResolver
这是客户端用来与 ContentProvider 交互的统一接口。你的应用不需要直接实例化 ContentProvider,而是通过 Context.getContentResolver() 方法获取 ContentResolver 对象,然后调用其 insert(), query(), update(), delete() 方法,并传入对应的 URI 和其他参数来操作数据。
// 获取 ContentResolver
ContentResolver resolver = getContentResolver();
// 查询数据
Cursor cursor = resolver.query(
Uri.parse("content://com.example.myapp.provider/users"),
new String[]{"_id", "name", "email"}, // 要查询的列
null, // WHERE 子句
null, // WHERE 子句的参数
null // 排序
);
// 插入数据
Uri newUri = resolver.insert(
Uri.parse("content://com.example.myapp.provider/users"),
contentValues // 包含新数据的 ContentValues 对象
);
5. MIME 类型
ContentProvider 需要实现 getType(Uri uri) 方法,用于返回对应 URI 所代表数据的 MIME 类型。这对于系统(如 Intent 选择器)识别数据类型非常重要。
单条记录:通常以 vnd.android.cursor.item/ 开头。
多条记录:通常以 vnd.android.cursor.dir/ 开头。
6.与其他数据存储对比
| 特性 | ContentProvider | SharedPreferences | SQLite Database | File Storage |
|---|---|---|---|---|
| 主要用途 | 跨应用数据共享 | 存储简单键值对配置 | 存储结构化关系数据 | 存储文件(图片、音频、缓存等) |
| 访问范围 | 应用间 | 应用内 | 应用内 (或通过 CP 共享) | 应用内 (或通过文件权限共享) |
| 数据结构 | 任意 (通过 URI 和列定义) | 键值对 (String, int, boolean等) | 表格 (行和列) | 二进制或文本流 |
| 复杂度 | 高 (需实现接口、注册、权限) | 低 | 中 | 低到中 |
| 性能 | 中 (涉及 IPC) | 高 | 高 | 高 |
2. View 体系与事件分发
View 体系是 Android 界面编程的基础,它是一个树形结构,所有 UI 元素都基于 View 或 ViewGroup。
1. 核心概念
View: Android 中所有 UI 控件的基类,代表屏幕上的一个矩形区域,负责绘制和事件处理。例如:Button, TextView, ImageView。
ViewGroup: View 的子类,也是容器控件的基类。它可以包含并管理多个子 View 或子 ViewGroup,形成一个树状结构(View Tree)。例如:LinearLayout, RelativeLayout, FrameLayout, RecyclerView。
层级关系:Activity -> Window -> DecorView (根 ViewGroup) -> ViewGroup -> View。
2. 坐标系
理解坐标是处理触摸事件和滑动的基础。Android 中主要有两种坐标系:
Android 坐标系 (屏幕坐标系):
原点:屏幕左上角。
X 轴:向右为正。
Y 轴:向下为正。
获取方法:MotionEvent.getRawX(), MotionEvent.getRawY()。
视图坐标系 (View 坐标系):
原点:当前 View 的左上角(相对于其直接父容器)。
X 轴:向右为正。
Y 轴:向下为正。
获取方法:MotionEvent.getX(), MotionEvent.getY()。
3. 事件类型
ACTION_DOWN: 手指初次接触屏幕。
ACTION_MOVE: 手指在屏幕上移动(会触发多次)。
ACTION_UP: 手指离开屏幕。
ACTION_CANCEL: 事件被上层拦截,子 View 不再接收后续事件。
一个完整的触摸操作(如点击)通常由 ACTION_DOWN -> ACTION_MOVE (0次或多次) -> ACTION_UP 组成,称为一个事件序列。
4. 事件分发流程
- 事件分发遵循 “自顶向下分发,自底向上处理” 的原则。主要涉及三个核心方法
| 方法名 | 作用 | 返回值说明 | 存在位置 |
|---|---|---|---|
| dispatchTouchEvent(MotionEvent event) | 事件分发的入口,决定事件是自己处理、拦截,还是分发给子View | true:事件被消费,流程结束;false:事件未被消费,向上传递 | Activity, ViewGroup, View |
| onInterceptTouchEvent(MotionEvent event) | 仅ViewGroup拥有,在事件传递给子View前决定是否拦截 | true:拦截事件,交由自身onTouchEvent处理;false:不拦截,事件继续向下分发 | ViewGroup(默认返回false) |
| onTouchEvent(MotionEvent event) | 处理触摸事件的核心方法,编写点击/滑动逻辑 | true:事件被消费,流程结束;false:事件未被消费,向上传递 | Activity, View(ViewGroup继承自View) |
- 一旦某个 View 在 ACTION_DOWN 事件中返回 true,那么该事件序列(后续的 ACTION_MOVE,
ACTION_UP)都会交给它处理,父容器的 onInterceptTouchEvent 将不再被调用(除非子 View 调用了
requestDisallowInterceptTouchEvent(true)) - ACTION_DOWN 是关键:它决定了后续事件序列的归属。
- onInterceptTouchEvent 只在 ViewGroup 中有效:View 本身没有拦截能力。
- 优先级:如果给 View 设置了 OnTouchListener,其 onTouch() 方法会优先于 onTouchEvent() 被调用。如果 onTouch() 返回 true,则 onTouchEvent() 不会被执行。

思考
事件分发机制: dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent() 三个方法的作用、返回值含义及调用顺序。如何解决滑动冲突?
5.实战应用:解决滑动冲突
事件分发机制最常见的应用就是解决复杂的滑动冲突,例如:ScrollView 嵌套 RecyclerView,或 ViewPager 嵌套 ScrollView。
- 外部拦截法 (推荐)
在父容器的 onInterceptTouchEvent() 方法中,根据业务逻辑判断是否需要拦截事件。
public class MyParentLayout extends ViewGroup {
private int mLastXIntercept, mLastYIntercept;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false; // ACTION_DOWN 一定不能拦截,否则子 View 无法收到事件
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
// 根据滑动方向判断,例如:竖直滑动交给父容器,水平滑动交给子 View
if (Math.abs(deltaY) > Math.abs(deltaX)) {
intercepted = true; // 父容器拦截竖直滑动
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false; // UP 事件一般不拦截
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
}
- 内部拦截法
在子 View 中,通过调用 getParent().requestDisallowInterceptTouchEvent(true) 来通知父容器不要拦截事件。
public class MyChildView extends View {
private int mLastX, mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true); // 请求父容器不要拦截
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaY) > Math.abs(deltaX)) {
// 如果是竖直滑动,让父容器来处理
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
3.自定义 View
1.三种主要实现方式
根据需求和复杂度,通常有以下三种途径:
| 方式 | 继承 View 或 ViewGroup(完全自绘) | 继承现有的 View(功能扩展) | 继承 ViewGroup(组合控件) |
|---|---|---|---|
| 适用场景 | 需要从零开始绘制一个全新的、系统没有提供的控件,例如一个仪表盘、一个签名板、一个游戏视图或一个复杂的动画效果。 | 在现有控件的基础上增加新功能或修改其行为。例如,创建一个带清空按钮的 EditText,或者一个可以显示进度的 Button。 | 将多个现有的 View 组合在一起,形成一个具有特定功能的复合控件。这是最常用、最推荐的方式,因为它符合“组合优于继承”的设计原则。例如,标题栏(包含返回按钮、标题文字、设置按钮)、商品卡片(包含图片、标题、价格、购买按钮)等。 |
| 核心工作 | 重写 onMeasure() 方法,精确测量控件的尺寸。 重写 onDraw() 方法,使用 Canvas 和 Paint 对象绘制所有内容。 重写 onTouchEvent() 方法,处理触摸事件以实现交互。 | 通常需要重写 onDraw() 方法,在原有绘制基础上添加新元素。 可能需要重写 onTouchEvent() 方法来处理新的交互逻辑。 有时也需要重写 onMeasure() 以适应新增内容的尺寸。 | 重写 onMeasure() 方法,测量自身及其所有子 View 的尺寸。 重写 onLayout() 方法,确定每个子 View 在容器中的具体位置。 可能需要重写 onInterceptTouchEvent() 和 onTouchEvent() 来处理内部的事件分发和冲突。 |
| 优点 | 灵活性最高,可以实现任何视觉效果。 | 复用性强,开发效率高,可以利用现有控件的成熟逻辑。 | 结构清晰,易于维护和扩展,代码复用性极高,能有效减少布局嵌套层级。 |
| 缺点 | 工作量最大,需要自己处理所有细节,包括 padding、wrap_content 等。 | 受限于父类控件的结构和行为。 | 需要管理子 View 的测量和布局,逻辑相对复杂。 |
2. measure layout draw
1. Measure (测量) - onMeasure(int widthMeasureSpec, int heightMeasureSpec)
-
目的:确定 View 自身的宽高。
-
关键参数:
MeasureSpec 是一个 32 位的 int 值,高 2 位是测量模式,低 30 位是测量大小。
EXACTLY: 父容器已经确定了子View 的精确大小。对应 match_parent 或具体的数值(如 100dp)。
AT_MOST: 子 View 的大小不能超过父容器指定的值。对应 wrap_content。
UNSPECIFIED: 子 View 想要多大就多大,父容器不限制。通常在 ScrollView 等容器中出现。
- 如何实现:
必须调用 setMeasuredDimension(int measuredWidth, int measuredHeight) 来设置最终测量结果。
处理 wrap_content:这是新手常犯的错误。如果父容器给的是 AT_MOST 模式,你需要根据自己的内容计算一个合适的尺寸,而不是直接使用父容器给的大小。
处理 padding:在计算尺寸时,必须将 getPaddingLeft(), getPaddingRight(), getPaddingTop(), getPaddingBottom() 考虑进去。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int myWidth, myHeight;
// 处理宽度
if (widthMode == MeasureSpec.EXACTLY) {
myWidth = widthSize;
} else {
// 计算内容所需宽度 + padding
myWidth = calculateContentWidth() + getPaddingLeft() + getPaddingRight();
if (widthMode == MeasureSpec.AT_MOST) {
myWidth = Math.min(myWidth, widthSize);
}
}
// 处理高度 (逻辑同上)
if (heightMode == MeasureSpec.EXACTLY) {
myHeight = heightSize;
} else {
myHeight = calculateContentHeight() + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
myHeight = Math.min(myHeight, heightSize);
}
}
setMeasuredDimension(myWidth, myHeight);
}
- Layout (布局) - onLayout(boolean changed, int left, int top, int right, int bottom)
- 目的:确定 View 自身及其子 View 在屏幕上的最终位置。
- 触发时机:在 measure 过程完成后调用。
- 关键点:
View: 通常不需要重写 onLayout,因为单个 View 没有子 View。
ViewGroup: 必须重写。需要遍历所有子 View,并调用 child.layout(l, t, r, b) 方法为它们指定在父容器中的坐标。
// 在自定义 ViewGroup 中
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// 根据你的布局逻辑计算每个子 View 的位置
int childLeft = ...;
int childTop = ...;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
}
}
}
- Draw (绘制) - onDraw(Canvas canvas)
- 目的:将 View 的内容绘制到屏幕上。
- 核心工具:
Canvas: 画布,所有绘图操作都在其上进行,如 drawCircle(), drawRect(), drawText(), drawBitmap() 等。
Paint: 画笔,用于设置绘图的样式,如颜色、字体、笔触宽度、抗锯齿等。 - 绘制顺序(简化版):
绘制背景 (background.draw(canvas))
保存画布图层 (如果需要)
绘制内容 (onDraw(canvas))
绘制子 View (dispatchDraw(canvas))
绘制装饰 (如滚动条)
- 关键实践:
处理 padding:在 onDraw 中,所有绘制内容都应该在 getPaddingLeft(), getPaddingTop() 等指定的区域内进行,避免内容被裁剪。
避免在 onDraw 中创建对象:因为 onDraw 会被频繁调用,创建对象会导致 GC,影响性能。应在初始化时创建好 Paint 等对象。
private Paint mPaint;
private int mRadius = 50;
public MyCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 在构造函数中初始化,避免在 onDraw 中创建
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 考虑 padding,将圆绘制在 View 的中心
int centerX = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int centerY = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
canvas.drawCircle(centerX, centerY, mRadius, mPaint);
}
4.常用控件 RecyclerView:
RecyclerView 是 Android 开发中用于高效显示大量数据列表的核心组件,它取代了旧的 ListView 和 GridView,提供了更高的灵活性和性能。以下是 RecyclerView 的核心要点解析:
1. 核心组件 (Core Components)
RecyclerView 的设计遵循了“分离关注点”的原则,其功能被拆分为几个独立的组件:
RecyclerView 本身 (The Container)
- 一个
ViewGroup,负责管理子视图(Item Views)的布局和回收。 - 它不直接处理布局逻辑或数据绑定,而是依赖于其他组件。
LayoutManager (布局管理器)
- 作用: 负责决定子视图如何在屏幕上排列(线性、网格、瀑布流等),并负责在用户滚动时回收和重用视图。
- 内置实现:
LinearLayoutManager: 线性布局(垂直或水平)。GridLayoutManager: 网格布局。StaggeredGridLayoutManager: 瀑布流布局(交错网格)。
- 关键点: 必须为
RecyclerView设置一个LayoutManager,否则它不会显示任何内容。
Adapter (适配器)
- 作用: 作为数据源和
RecyclerView之间的桥梁。它负责创建 Item View (onCreateViewHolder) 和将数据绑定到这些视图上 (onBindViewHolder)。 - 继承: 通常继承自
RecyclerView.Adapter<YourViewHolder>。 - 关键方法:
onCreateViewHolder(ViewGroup parent, int viewType): 创建一个新的ViewHolder实例。通常在这里通过LayoutInflater从布局文件 (item_layout.xml) 填充视图。onBindViewHolder(YourViewHolder holder, int position): 将数据绑定到指定位置的ViewHolder上。这是性能优化的关键点,应避免在此方法中进行耗时操作。getItemCount(): 返回数据集中项目的总数。
ViewHolder (视图持有者)
- 作用: 持有 Item View 的引用,避免在
onBindViewHolder中重复调用findViewById,从而显著提升滚动性能。 - 实现: 通常作为
Adapter的内部类,继承自RecyclerView.ViewHolder。 - 关键点: 在
onCreateViewHolder中创建ViewHolder时,会将整个 Item View 传递给父类构造函数。
2. ☆☆ 四级缓存
RecyclerView 的四级缓存机制是其性能优化的核心,通过分级管理 ViewHolder 的复用与回收,显著减少了重复创建和绑定数据的开销。
| 缓存级别 | 名称 | 组成 | 作用 | 核心特点 |
|---|---|---|---|---|
| 一级缓存 | Scrap 缓存 | mAttachedScrap(未移除/更新的无效 ViewHolder)mChangedScrap(数据变更的无效 ViewHolder) | 临时保存屏幕内因布局变化(如动画、数据更新)分离的 ViewHolder,不参与滚动回收 | 复用优先级最高,直接匹配 position 或 id 返回,无需重新绑定数据 |
| 二级缓存 | mCachedViews | 默认容量为 2 的 ArrayList<ViewHolder> | 保存最新移出屏幕的 ViewHolder(如快速滑动时滑出的视图) | 精准复用(匹配 position/id),容量可调(setItemViewCacheSize()),FIFO 淘汰策略 |
| 三级缓存 | ViewCacheExtension | 开发者通过 RecyclerView.setViewCacheExtension() 自定义实现 | 提供业务逻辑相关的缓存(如固定位置的特殊视图) | 灵活性高,但使用率低(多数场景无需自定义) |
| 四级缓存 | RecycledViewPool | 按 viewType 分组的 SparseArray<ArrayList<ViewHolder>>(默认每个 viewType 缓存 5 个) | 存放被废弃的 ViewHolder(其他缓存均未命中时使用) | 数据需重新绑定,容量可调(setMaxRecycledViews()),支持跨 RecyclerView 共享 |
示意图:

- 复用流程:
Scrap → mCachedViews → ViewCacheExtension → RecycledViewPool,优先级依次降低。 - 性能优化关键:
减少 onCreateViewHolder 和 onBindViewHolder 调用次数。
优先命中高级缓存(如 mCachedViews),避免数据重置开销。
3. 核心工作流程 (Workflow)
- 初始化: 在 Activity/Fragment 中找到
RecyclerView实例。 - 设置 LayoutManager: 调用
recyclerView.setLayoutManager(new LinearLayoutManager(this))。 - 创建 Adapter: 实现一个继承自
RecyclerView.Adapter的类。 - 设置 Adapter: 调用
recyclerView.setAdapter(new YourAdapter(dataList))。 - 数据加载:
RecyclerView通过Adapter的getItemCount()获取数据总量。 - 视图创建与绑定:
LayoutManager请求创建一个不可见的 Item View。Adapter的onCreateViewHolder被调用,创建一个新的ViewHolder(包含新创建的视图)。LayoutManager将此ViewHolder的视图添加到屏幕上。Adapter的onBindViewHolder被调用,将数据集中对应位置的数据填充到ViewHolder的视图组件中。
- 回收与重用:
- 当用户滚动时,移出屏幕的 Item View 及其
ViewHolder不会被销毁。 RecyclerView将它们放入缓存(Scrap Heap 和 Recycled Pool)。- 当需要显示新出现的 Item 时,
LayoutManager会优先从缓存中获取一个ViewHolder。 onBindViewHolder被调用,用新数据刷新这个重用的ViewHolder的视图内容。
- 当用户滚动时,移出屏幕的 Item View 及其
4. 关键优势 (Key Advantages)
- 卓越的性能: 通过 ViewHolder 模式和强大的回收机制,确保了在滚动大量数据时的流畅性。
- 高度可定制: 可以轻松自定义
LayoutManager、ItemDecoration和ItemAnimator。 - 职责分离: 组件清晰分离,代码更易于维护和测试。
- 内置动画: 支持项目添加、删除、移动时的默认动画。
- 扩展性强: 易于添加拖拽、滑动删除等功能。
5. 高级特性 (Advanced Features)
- ItemDecoration: 用于在 Item 之间绘制分隔线或添加装饰。需要继承
RecyclerView.ItemDecoration并重写onDraw和getItemOffsets方法。 - ItemAnimator: 控制 Item 添加、删除、移动时的动画效果。默认提供
DefaultItemAnimator,也可以自定义。 - DiffUtil: 一个实用工具类,用于计算两个数据集之间的差异,并能高效地触发
RecyclerView的局部更新(如notifyItemChanged,notifyItemInserted等),避免notifyDataSetChanged()导致的全局刷新和性能损耗。强烈推荐在数据集变化较大时使用。 - ViewPool:
RecyclerView内部维护的ViewHolder缓存池,NestedRecyclerView可以共享此池以优化性能。 - 多种 ViewType: 通过重写
getItemViewType(int position)方法,可以在同一个RecyclerView中显示不同类型的 Item(例如,聊天消息中的用户消息和系统消息)。
6. 使用建议 (Best Practices)
- Always Use ViewHolder: 这是性能的基础。
- 避免在
onBindViewHolder中做耗时操作: 如数据库查询、网络请求。应在绑定前准备好数据。 - 使用
DiffUtil进行数据更新: 提升更新效率和用户体验。 - 合理设置
LayoutManager: 根据需求选择合适的布局管理器。 - 考虑使用
ListAdapter: 它是RecyclerView.Adapter的子类,内置了DiffUtil的支持,简化了列表更新的代码。 - 注意内存泄漏: 如果 Item View 中包含异步操作(如加载图片),在
onViewRecycled或onViewDetachedFromWindow中取消操作或清理资源。
5.常用控件 ViewPager2 与 ViewPager
ViewPager 和 ViewPager2 是 Android 中用于实现页面滑动(左右或上下)功能的核心组件,常用于引导页、图片轮播、标签页切换等场景。ViewPager2 是 ViewPager 的现代化替代品,解决了其诸多限制并提供了更强大的功能。
1. ViewPager (旧版)
ViewPager 是早期 Android 支持库(androidx.viewpager:1.x)中提供的组件。
核心特性
- 方向: 仅支持水平滑动。
- 适配器: 必须使用
PagerAdapter。 - 数据集: 通常与
Fragment配合使用,通过FragmentPagerAdapter或FragmentStatePagerAdapter管理Fragment列表。 - API 简单: 基本用法简单直接。
主要缺点与限制
- 仅支持水平滑动: 无法实现垂直滑动。
- 无法从右向左 (RTL) 布局: 不支持阿拉伯语等 RTL 语言的布局。
- 修改数据集困难: 当数据集发生变化时(如添加/删除页面),调用
notifyDataSetChanged()可能导致不可预测的行为或崩溃,因为PagerAdapter的getItemPosition()方法处理逻辑复杂。 - 与 RecyclerView 集成差: 不能直接使用
RecyclerView.Adapter。 - 性能: 在管理大量
Fragment时,FragmentStatePagerAdapter虽能销毁不可见的Fragment,但仍有优化空间。
2. ViewPager2 (新版)
ViewPager2 是在 ViewPager 基础上重构的现代化组件(androidx.viewpager2:1.x),基于 RecyclerView 构建,解决了 ViewPager 的大部分痛点。
核心特性
- 双向滑动:
- 支持 水平 和 垂直 滑动。
- 通过
setOrientation()方法设置:ViewPager2.ORIENTATION_HORIZONTAL或ViewPager2.ORIENTATION_VERTICAL。
- 支持 RTL 布局:
- 在启用 RTL 的语言环境中,页面会从右向左滑动,符合本地化需求。
- 基于 RecyclerView:
- 内部使用
RecyclerView实现,继承了其优秀的性能、回收机制和扩展性。 - 可以直接使用
RecyclerView.Adapter作为适配器。
- 内部使用
- 更可靠的
notifyDataSetChanged():- 由于基于
RecyclerView,数据集更新更加稳定和可预测。
- 由于基于
- 内置
PageTransformer:- 支持为页面添加动画效果(如淡入淡出、深度效果)。
- 支持
Fragment状态保存:- 通过
FragmentStateAdapter管理Fragment,能更好地保存和恢复Fragment状态。
- 通过
- 更好的
OffscreenPageLimit控制:- 可以设置预加载页面的数量,优化性能和内存。
核心组件
ViewPager2: 主容器。RecyclerView.Adapter: 适配器,取代了PagerAdapter。FragmentStateAdapter: 专为管理Fragment列表设计的适配器,是RecyclerView.Adapter的子类。它会自动处理Fragment的创建、销毁和状态保存,是FragmentStatePagerAdapter的现代化替代。PageTransformer: 用于自定义页面滑动时的动画效果。
基本用法示例
// Kotlin 示例
val viewPager2 = findViewById<ViewPager2>(R.id.viewPager2)
// 设置垂直滑动
viewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL
// 使用 FragmentStateAdapter
class MyFragmentStateAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int = 3 // 页面数量
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> FirstFragment()
1 -> SecondFragment()
2 -> ThirdFragment()
else -> throw IllegalArgumentException("Invalid position")
}
}
}
viewPager2.adapter = MyFragmentStateAdapter(this)
// Java 示例
ViewPager2 viewPager2 = findViewById(R.id.viewPager2);
viewPager2.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
viewPager2.setAdapter(new FragmentStateAdapter(this) {
@Override
public int getItemCount() {
return 3;
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0: return new FirstFragment();
case 1: return new SecondFragment();
case 2: return new ThirdFragment();
default: throw new IllegalArgumentException("Invalid position");
}
}
});
2. 特性对比
| 特性 | ViewPager | ViewPager2 |
|---|---|---|
| 方向 | 仅水平 | 水平 & 垂直 |
| RTL 支持 | ❌ 不支持 | ✅ 支持 |
| 基础架构 | 自定义 | 基于 RecyclerView |
| 适配器 | PagerAdapter | RecyclerView.Adapter / FragmentStateAdapter |
| notifyDataSetChanged() | 不稳定 | 稳定可靠 |
| 与 RecyclerView 集成 | 差 | 原生支持 |
| Fragment 管理 | FragmentPagerAdapter / FragmentStatePagerAdapter | FragmentStateAdapter |
| 性能 | 一般 | 更优(得益于 RecyclerView) |
| API 现代化 | 较旧 | 更现代、更简洁 |
| 依赖库 | androidx.viewpager:1.x | androidx.viewpager2:1.x |
ViewPager2 是 ViewPager 的全面升级版,解决了其历史遗留问题,并提供了更强大、更灵活的功能。对于新开发的 Android 应用,应优先选择 ViewPager2。
6. handler机制
Handler 是 Android 中用于实现线程间通信和消息调度的核心机制。它主要用于将任务(Runnable)或消息(Message)从一个线程发送到另一个线程的消息队列中执行,最常见的是在子线程中处理耗时操作后,通过 Handler 将结果发送回主线程(UI线程)以更新界面。
Handler 机制是 Android 消息循环系统的基础,其核心由四个关键组件构成:Handler, Looper, MessageQueue, 和 Message。
1. 核心组件 (Core Components)
1.1 Message (消息)
- 作用: 承载需要传递的数据或指令。
- 关键字段:
what: 消息类型标识符,通常用于Handler区分不同消息。arg1,arg2: 可传递简单的整型数据。obj: 可传递任意对象(注意序列化和内存泄漏风险)。target: 指向处理该消息的Handler(由Looper在分发时自动设置)。when: 消息的执行时间戳(毫秒)。
- 创建方式:
Message.obtain(): 推荐,从消息池中获取,避免频繁创建对象。new Message(): 直接创建,不推荐,可能导致内存抖动。
1.2 MessageQueue (消息队列)
- 作用: 存储待处理的
Message的单链表结构队列。 - 特点:
- 按
Message.when(执行时间)进行排序,时间越早的消息越靠前。 - 由
Looper负责从队列中取出消息 (next()方法)。 - 是
Looper和Handler之间的数据桥梁。
- 按
- 线程安全: 内部有同步机制,确保多线程操作安全。
1.3 Looper (消息循环器)
- 作用: 为线程提供消息循环能力。它会无限循环地从
MessageQueue中取出消息,并将消息分发给对应的Handler去处理。 - 关键方法:
prepare(): 为当前线程准备一个Looper。一个线程只能调用一次prepare()。loop(): 启动消息循环。这是一个死循环,会持续调用MessageQueue.next()获取消息,并调用msg.target.dispatchMessage(msg)将消息分发给Handler。
- 线程关联: 每个线程最多只能有一个
Looper。 - 主线程 Looper: Android 主线程(UI线程)在启动时会自动调用
Looper.prepareMainLooper()和Looper.loop(),因此主线程天生就具备消息循环能力。
1.4 Handler (处理器)
- 作用: 消息的发送者和处理者。
- 发送消息:
post(Runnable): 将Runnable作为消息发送。sendMessage(Message): 发送一个Message对象。postDelayed(Runnable, delay): 延迟发送。sendEmptyMessage(what): 发送一个只有what字段的空消息。
- 处理消息:
- 重写
handleMessage(Message msg)方法来处理Message。 Runnable会被封装在Message的callback字段中,Handler会直接执行它。
- 重写
- 创建时机: 必须在关联了
Looper的线程中创建。通常在主线程创建的Handler用于更新 UI。
2. 工作流程 (Workflow)
-
线程准备:
- 主线程:系统自动完成
Looper.prepareMainLooper()和Looper.loop()。 - 子线程:若需使用
Handler,必须手动调用Looper.prepare()创建Looper,然后调用Looper.loop()启动循环。
- 主线程:系统自动完成
-
创建 Handler:
- 在需要发送/处理消息的线程中创建
Handler实例。 - 创建时,
Handler会自动绑定到当前线程的Looper和MessageQueue。
- 在需要发送/处理消息的线程中创建
-
发送消息:
- 调用
handler.sendMessage(msg)或handler.post(runnable)。 Handler将Message插入到其绑定的MessageQueue的适当位置(按时间排序)。
- 调用
-
消息循环:
Looper的loop()方法在死循环中调用MessageQueue.next()。next()方法会阻塞,直到有消息到达其执行时间或队列非空。
-
分发与处理:
Looper从MessageQueue取出一个Message。- 调用
msg.target.dispatchMessage(msg)(target是发送该消息的Handler)。 Handler的dispatchMessage方法会:- 如果
msg.callback不为空(即post的Runnable),则执行Runnable.run()。 - 否则,如果设置了
Callback(通过构造函数或setCallback),则调用mCallback.handleMessage(msg)。 - 否则,调用
handleMessage(msg)方法(通常由子类重写)。
- 如果
graph TD
A[发送消息 handler.sendMessage(msg)] --> B[Message 加入 MessageQueue]
B --> C[Looper.loop() 循环]
C --> D[Looper 从 MessageQueue 取出 msg]
D --> E[Looper 调用 msg.target.dispatchMessage(msg)]
E --> F[Handler.dispatchMessage(msg)]
F --> G{msg.callback 是否为空?}
G -->|是| H[执行 Callback 或 handleMessage]
G -->|否| I[执行 Runnable.run()]
3.关键机制解析
ThreadLocal 的作用
原理:为每个线程提供独立的变量副本,确保 Looper 实例的线程隔离性。
在 Handler 中的应用:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed)); // 存储 Looper 到当前线程
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get(); // 获取当前线程的 Looper
}
同步屏障(Sync Barrier)
作用:支持优先级调度,确保关键任务(如 UI 渲染)优先执行。
实现:通过插入特殊消息(Message.setAsynchronous(true))拦截后续同步消息,处理完高优先级任务后再恢复。
4. 常见问题
1. Android 为什么不能在子线程更新 UI?
核心原因:线程不安全与UI框架设计
Android 的 UI 框架采用单线程模型,所有界面操作(如视图绘制、布局计算、事件分发)必须在主线程(UI 线程)中执行。原因如下:
- 线程安全:UI 组件(如 View、Window)的内部状态(如布局参数、可见性)非线程安全。若子线程直接修改,可能导致状态- 不一致(如视图错位、闪烁或崩溃)。
- 性能优化:主线程通过消息队列(MessageQueue)顺序处理 UI 更新,避免多线程竞争导致的性能损耗。
- 异常机制:Android 系统强制检查 UI 操作线程,若子线程尝试更新 UI,会抛出 CalledFromWrongThreadException,防止潜在问题。
解决方案:通过 Handler、runOnUiThread() 或 View.post() 将 UI 更新操作切换到主线程。例如:
new Thread(() -> {
String result = doBackgroundWork(); // 子线程耗时操作
runOnUiThread(() -> textView.setText(result)); // 切换到主线程更新UI
}).start();
2. 主线程的 Looper 何时创建?为什么不会阻塞主线程?
创建时机:
主线程的 Looper 在 ActivityThread.main() 方法中初始化,流程如下:
-
Looper.prepareMainLooper():创建主线程的 Looper 实例,并关联 MessageQueue。
-
ActivityThread 初始化:加载应用组件(如 Activity、Service)。
-
Looper.loop():启动消息循环,开始处理消息。
不会阻塞的原因: -
异步消息处理:Looper.loop() 内部通过 MessageQueue.next() 阻塞等待消息。若无消息,主线程进入休眠状态(通过 epoll 机制实现),不占用 CPU 资源。
-事件驱动模型:主线程仅在有消息(如 UI 事件、生命周期回调)时被唤醒,处理完毕后立即休眠,避免忙等待。
消息队列分离:耗时操作(如网络请求)在子线程执行,结果通过 Handler 发送到主线程,确保主线程流畅性。
3. 子线程如何创建 Looper?
步骤:
- 调用 Looper.prepare():初始化当前线程的 Looper 和 MessageQueue(每个线程只能有一个 Looper)。
- 创建 Handler:绑定当前线程的 Looper,用于发送和处理消息。
- 启动 Looper.loop():开始消息循环。
示例代码:
new Thread(() -> {
Looper.prepare(); // 初始化Looper
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d("SubThread", "Received message: " + msg.what);
}
};
handler.sendEmptyMessage(1); // 发送消息
Looper.loop(); // 启动消息循环
}).start();
注意事项:
- 生命周期管理:子线程的 Looper 需手动退出(调用 Looper.quit() 或 Looper.quitSafely()),避免内存泄漏。
- 适用场景:子线程 Looper 适用于需要长期运行的任务(如后台监听),但多数情况下推荐使用 HandlerThread 或线程池。
4. Handler 导致内存泄漏的原因及解决方案(静态内部类 + 弱引用)
内存泄漏原因:
- 匿名内部类隐式引用:若 Handler 定义为匿名内部类,会持有外部类(如 Activity)的强引用。
- 消息队列滞留:若 Handler 发送的消息未及时处理(如 Activity 已销毁但消息仍在队列中),外部类无法被垃圾回收。
示例代码(内存泄漏):
public class MainActivity extends Activity {
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
textView.setText("Updated"); // 持有Activity引用
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.sendEmptyMessageDelayed(1, 1000); // 延迟消息可能导致泄漏
}
}
解决方案: 静态内部类 + 弱引用:
- 将 Handler 定义为静态内部类,避免隐式持有外部类引用。
- 通过 WeakReference 持有外部类实例,允许垃圾回收。
示例代码(修复后):
public class MainActivity extends Activity {
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> activityRef;
public MyHandler(MainActivity activity) {
activityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = activityRef.get();
if (activity != null) {
activity.textView.setText("Updated"); // 安全访问UI
}
}
}
private final MyHandler handler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler.sendEmptyMessageDelayed(1, 1000);
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null); // 清理消息队列
}
}
其他优化:
- 及时清理消息队列:在 Activity 的 onDestroy() 中调用 handler.removeCallbacksAndMessages(null)。
- 使用 Lifecycle-aware 组件:如 LiveData 或 Jetpack 的生命周期库,自动管理 Handler 生命周期。
7. SharedPreferences 和MMKV
SharedPreferences (SP) 是 Android 原生的轻量级数据存储方案,而 MMKV 是由腾讯开源的基于 mmap (内存映射文件) 的高性能、跨平台键值对存储组件。两者都用于持久化简单配置数据,但底层实现和性能表现差异巨大。
1. SharedPreferences 要点解析
> (基于 Android 原生 API)
1. 核心原理
- 存储方式: 以 XML 文件 形式存储在应用私有目录 (
/data/data/<package>/shared_prefs/)。 - 读写机制:
- 写入: 调用
apply()或commit()时,将整个键值对集合序列化为 XML 字符串,然后覆盖写入磁盘文件。 - 读取: 首次访问时,加载并解析整个 XML 文件到内存中,后续读取在内存中进行查找。
- 写入: 调用
- 线程安全: 读操作线程安全;
Editor非线程安全,但框架内部有同步控制。
2. 关键特性与 API
// 获取实例
SharedPreferences sp = context.getSharedPreferences("config", Context.MODE_PRIVATE);
// 写入数据
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean("key_bool", true);
editor.putString("key_str", "value");
editor.apply(); // 推荐:异步提交
// editor.commit(); // 同步提交,阻塞线程
// 读取数据
boolean bool = sp.getBoolean("key_bool", false);
String str = sp.getString("key_str", "default");
// 监听变化
SharedPreferences.OnSharedPreferenceChangeListener listener = ...;
sp.registerOnSharedPreferenceChangeListener(listener);
// 别忘了 unregister!</code></pre>
3. 优点
- 原生支持: 无需引入第三方库,API 简单易用。
- 调试方便: XML 文件为明文,可直接查看内容。
- 适合小数据: 对于少量配置(如开关、用户名),性能可接受。
4. 缺点与局限
- 性能瓶颈:
- 写入慢:
apply()异步但仍有 I/O 开销;commit()同步阻塞。每次写入都重写整个文件。 - 读取慢: 首次读取需解析整个大文件,数据量大时耗时严重(可能导致 ANR)。
- 写入慢:
- 多进程不安全: 默认不支持多进程。
MODE_MULTI_PROCESS已废弃且不可靠,极易导致数据损坏。 - 无事务: 无法保证多个
put操作的原子性。 - 无加密: 数据以明文存储,敏感信息有泄露风险。
- 不适合大数据: 存储大量数据时,I/O 和解析成为性能瓶颈。
2. MMKV 要点解析
(腾讯开源高性能 KV 存储)
1. 核心原理
- 存储方式: 基于 mmap (内存映射文件)。
- 将文件直接映射到进程的虚拟内存。
- 对内存的读写等同于对文件的读写,由操作系统内核高效管理。
- 序列化: 使用 Protobuf 格式,二进制存储,紧凑高效。
- 核心优势: 增量更新。只修改变化的数据块,无需重写整个文件。
2. 关键特性与 API
1. 初始化 (Application.onCreate)
MMKV.initialize(this)
// 2. 获取实例
val kv = MMKV.defaultMMKV() // 默认实例
// val kv = MMKV.mmkvWithID("UserSettings", MMKV.MULTI_PROCESS_MODE) // 指定名称和模式
// 3. 写入数据 (所有操作均为内存级速度)
kv.encode("bool", true)
kv.encode("string", "Hello")
kv.encode("int", 123)
kv.encode("bytes", byteArrayOf(1, 2, 3))
// 4. 读取数据
val bool = kv.decodeBool("bool", false)
val string = kv.decodeString("string", "default")
val int = kv.decodeInt("int", -1)
// 5. 删除/清空
kv.removeValueForKey("key")
kv.clearAll()
// 6. 多进程模式 (安全可靠)
val multiKV = MMKV.mmkvWithID("MultiProcess", MMKV.MULTI_PROCESS_MODE)</code></pre>
3. 核心优势
- 极致性能:
- 写入: 毫秒级完成,
set()即“写入”。 - 读取: 内存访问速度,无需解析。
- 写入: 毫秒级完成,
- 真正的多进程支持:
- 通过 文件锁 (file lock) 保证多进程并发读写的安全性和一致性。
- 高可靠性:
- CRC64 校验: 确保数据完整性。
- 崩溃安全:
mmap机制保证写入过程崩溃也不会损坏原文件。
- 数据加密:
- 支持 AES-128-CBC 全文件加密,保护敏感数据。
- 跨平台:
- 支持 Android, iOS, Windows, macOS, Linux,数据格式统一。
- 内存友好: 只加载实际访问的数据块。
3. MMKV 与 SharedPreferences 全面对比
1.特性对比
| 特性 | SharedPreferences | MMKV |
|---|---|---|
| 底层原理 | XML 文件 I/O | ✅ mmap 内存映射 |
| 序列化格式 | 明文 XML | ✅ 二进制 Protobuf |
| 写入性能 | 慢 (I/O 瓶颈,全量写) | ✅✅✅ 极快 (内存操作,增量更新) |
| 读取性能 | 慢 (首次解析全文件) | ✅✅✅ 极快 (内存访问) |
| 多进程支持 | ❌ 不支持 / 不可靠 | ✅✅✅ 原生支持,安全可靠 |
| 数据加密 | ❌ 无 | ✅✅✅ 支持 AES-128-CBC |
| 数据完整性 | ❌ 无校验 | ✅✅✅ CRC64 校验 |
| 事务支持 | ❌ 无 | ⚠️ 有限支持 (begin/commitTransaction) |
| 跨平台 | ❌ Android 专用 | ✅✅✅ 全平台支持 |
| 数据大小限制 | 无硬限,大文件性能差 | 默认 1MB,可配置 |
| 调试友好性 | ✅ 明文,易读 | ❌ 二进制,需工具解析 |
| API 复杂度 | ✅ 简单,原生 | ⚠️ 需引入库,API 类似 |
| 库大小 | 0 (系统) | ~50-100KB |
| 适用场景 | 简单配置、小量数据 | ✅ 高性能、多进程、加密、大数据 |
2. 如何选择?
选择 SharedPreferences :
- 项目简单,数据量极小(< 50KB)。
- 无多进程需求。
- 对性能不敏感。
- 需要明文调试存储内容。
- 希望避免第三方依赖。
选择 MMKV :
- 追求高性能,对 I/O 敏感。
- 必须支持多进程(如主进程与推送服务)。
- 存储敏感信息,需要加密。
- 配置数据量较大(> 100KB)。
- 项目有跨平台需求。
- 需要更高的数据可靠性。
- 新项目: 强烈推荐使用 MMKV,尤其是涉及多进程或性能要求高的场景。
- 现有项目:
- 若 SP 运行良好,可维持。
- 若遇到性能问题或多进程需求,应迁移至 MMKV。
- 无缝迁移:
- MMKV 提供
migrateFromSharedPreferences()API,可一键迁移旧 SP 数据。
- MMKV 提供
8. 数据库
1.SQLite 数据库的
SQLite 是 Android 内置的轻量级、嵌入式关系型数据库。它无需独立的服务器进程,数据以文件形式存储在应用的私有目录中,非常适合在移动设备上存储结构化数据。Android 提供了 SQLiteOpenHelper 和 SQLiteDatabase 等 API 来方便地操作 SQLite 数据库。
1. 核心组件
1.1 SQLiteOpenHelper
- 作用: 抽象辅助类,用于管理数据库的创建和版本升级。
- 关键方法:
onCreate(SQLiteDatabase db): 在数据库首次创建时调用。通常在这里执行CREATE TABLE等 DDL 语句。onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion): 当数据库版本号增加时调用。用于执行数据表结构的修改(如ALTER TABLE)和数据迁移。onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion): 当版本号降低时调用(默认抛出异常,需重写)。onOpen(SQLiteDatabase db): 每次数据库打开时调用。
- 创建实例: 通常作为单例或在
Application中初始化。
1.2 SQLiteDatabase
- 作用: 代表一个数据库实例,提供执行 SQL 语句和事务管理的方法。
- 获取方式:
getWritableDatabase(): 获取可读写的数据库实例。如果磁盘满,此方法可能抛出异常。getReadableDatabase(): 优先尝试获取可读写实例;如果失败(如磁盘满),则返回只读实例。推荐使用此方法。
2. 基本用法
2.1 创建数据库帮助类
public class MyDatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "app.db";
private static final int DATABASE_VERSION = 1;
// 表名和列名建议定义为常量
public static final String TABLE_USERS = "users";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_EMAIL = "email";
// 创建表的 SQL 语句
private static final String CREATE_TABLE_USERS =
"CREATE TABLE " + TABLE_USERS + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COLUMN_NAME + " TEXT NOT NULL, " +
COLUMN_EMAIL + " TEXT UNIQUE" +
");";
public MyDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_USERS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 版本升级时的处理逻辑
switch (oldVersion) {
case 1:
// 从版本 1 升级到 2
// db.execSQL("ALTER TABLE ...");
// break;
// case 2:
// ...
}
// 简单粗暴方式:删除旧表,创建新表(会丢失数据!)
// db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);
// onCreate(db);
}
}</code></pre>
2.2 增 (Insert)
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 方法一:使用 ContentValues (推荐)
ContentValues values = new ContentValues();
values.put(MyDatabaseHelper.COLUMN_NAME, "Alice");
values.put(MyDatabaseHelper.COLUMN_EMAIL, "alice@example.com");
// insert 返回新记录的行 ID,失败返回 -1
long newRowId = db.insert(MyDatabaseHelper.TABLE_USERS, null, values);
// 方法二:使用 rawQuery 执行 INSERT SQL
// String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
// db.execSQL(sql, new Object[]{"Bob", "bob@example.com"});</code></pre>
2.3 删 (Delete)
SQLiteDatabase db = dbHelper.getWritableDatabase();
// 删除条件
String selection = MyDatabaseHelper.COLUMN_NAME + " = ?";
String[] selectionArgs = {"Alice"};
// delete 返回删除的行数
int deletedRows = db.delete(MyDatabaseHelper.TABLE_USERS, selection, selectionArgs);
// 删除所有数据
// db.delete(TABLE_USERS, null, null);</code></pre>
2.4 改 (Update)
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(MyDatabaseHelper.COLUMN_EMAIL, "alice_new@example.com");
String selection = MyDatabaseHelper.COLUMN_NAME + " = ?";
String[] selectionArgs = {"Alice"};
// update 返回更新的行数
int updatedRows = db.update(MyDatabaseHelper.TABLE_USERS, values, selection, selectionArgs);</code></pre>
2.5 查 (Query)
SQLiteDatabase db = dbHelper.getReadableDatabase();
// 查询条件
String[] projection = { // 要查询的列
MyDatabaseHelper.COLUMN_ID,
MyDatabaseHelper.COLUMN_NAME,
MyDatabaseHelper.COLUMN_EMAIL
};
String selection = MyDatabaseHelper.COLUMN_NAME + " LIKE ?";
String[] selectionArgs = {"%li%"};
String sortOrder = MyDatabaseHelper.COLUMN_NAME + " DESC";
// query 返回 Cursor
Cursor cursor = db.query(
MyDatabaseHelper.TABLE_USERS, // 表名
projection, // 列
selection, // 条件
selectionArgs, // 条件参数
null, // groupBy
null, // having
sortOrder // 排序
);
// 遍历结果
if (cursor != null && cursor.moveToFirst()) {
do {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MyDatabaseHelper.COLUMN_ID));
String name = cursor.getString(cursor.getColumnIndexOrThrow(MyDatabaseHelper.COLUMN_NAME));
String email = cursor.getString(cursor.getColumnIndexOrThrow(MyDatabaseHelper.COLUMN_EMAIL));
// 处理数据...
} while (cursor.moveToNext());
cursor.close(); // 必须关闭!
}
3. 事务管理
对于需要保证原子性的操作(如转账),必须使用事务。
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction(); // 开始事务
try {
// 执行多个数据库操作
db.insert(...);
db.update(...);
// db.delete(...);
db.setTransactionSuccessful(); // 标记事务成功
} catch (Exception e) {
e.printStackTrace();
// 异常时,事务会自动回滚
} finally {
db.endTransaction(); // 结束事务
}</code></pre>
4. 最佳实践与注意事项
- 使用
ContentValues: 避免直接拼接 SQL 字符串,防止 SQL 注入。 - 参数化查询: 使用
?占位符和参数数组,是防止 SQL 注入的标准做法。 - 及时关闭 Cursor:
Cursor使用后必须调用close(),否则会导致内存泄漏。推荐使用try-with-resources(API 16+) 或在finally块中关闭。 - 数据库版本管理: 合理设计
onUpgrade()逻辑,避免简单粗暴地DROP TABLE导致用户数据丢失。 - 避免主线程操作: 数据库的读写,尤其是复杂查询,可能耗时。务必在子线程中执行,避免 ANR。可使用
AsyncTask、HandlerThread、ExecutorService或现代方案Room+Coroutine/LiveData。 - 数据库文件位置: 默认在
/data/data/<package_name>/databases/目录下。 - 单例模式:
SQLiteOpenHelper通常设计为单例,避免频繁创建和打开数据库连接。 - 索引优化: 对频繁查询的列(如
WHERE条件中的列)创建索引,可大幅提升查询速度。
2.Room 和 GreenDao
GreenDAO 和 Room 都是 Android 平台上流行的 ORM (对象关系映射) 框架,旨在简化 SQLite 数据库的操作。它们将 Java/Kotlin 对象映射到数据库表,避免了编写大量重复的 SQL 语句。尽管目标相似,但两者在设计理念、性能、易用性和生态上存在显著差异。
1. 核心要点解析
1. GreenDAO 要点
- 开发者: 由第三方团队开发和维护。
- 设计哲学: 极致性能。通过代码生成在编译期生成高效的 DAO 类和访问逻辑。
- 性能: 在批量操作(如十万条数据的插入、查询)方面表现极为出色,是性能要求极高的场景的首选。
- 学习曲线: 相对陡峭,需要理解其生成的 DAO 类、
DaoSession、DaoMaster等概念。 - 缓存机制: 提供
DaoSession缓存层。同一个DaoSession中查询过的实体会被缓存,再次查询时直接从内存返回,显著提升查询效率。 - 加密: 原生支持数据库加密(需集成 SQLCipher)。
- 数据迁移: 提供灵活的迁移机制,支持通过注解和版本管理进行数据库升级。
- 关联关系: 完全支持一对一、一对多、多对多等复杂关联映射。
- Kotlin 支持: 支持,但非原生优先。
2. Room 要点
- 开发者: 由 Google 官方推出,属于 Jetpack 组件的一部分。
- 设计哲学: 类型安全 和 开发效率。在 SQLite 之上提供一个抽象层,强调编译时检查和简洁的 API。
- 性能: 单条数据操作性能良好,但在大规模批量操作上,性能通常略逊于 GreenDAO。
- 学习曲线: 相对平缓,API 设计简洁,与 Android 开发者工具链集成度高。
- 编译时验证: 核心优势。SQL 查询语句在编译时进行检查,如果表名、列名或 SQL 语法有误,会直接报错,避免运行时崩溃。
- 抽象层: 只需定义
Entity(实体)、DAO(数据访问对象) 和Database(数据库) 三个组件。 - 集成性: 与 LiveData、Kotlin 协程 (Coroutines)、RxJava 无缝集成,方便实现响应式编程和异步操作。
- 测试支持: 易于进行单元测试和仪器化测试。
- Kotlin 支持: 原生优先,对 Kotlin 友好。
- 加密: 需要额外集成 SQLCipher 或使用
EncryptedRoomDatabase。
2. GreenDAO 与 Room 全面对比
| 特性 | GreenDAO | Room |
|---|---|---|
| 开发者 | 第三方 | ✅ Google 官方 (Jetpack) |
| 核心优势 | ✅✅✅ 极致性能 (尤其批量操作) | ✅✅ 类型安全、编译时检查 |
| 学习曲线 | ⚠️ 较陡峭 | ✅ 较平缓 |
| API 简洁性 | ⚠️ 生成类多,概念稍复杂 | ✅ 简洁,组件清晰 |
| 性能 (批量操作) | ✅✅✅ 最优 | ✅ 良好,但通常慢于 GreenDAO |
| 性能 (单条操作) | ✅ 优秀 | ✅ 优秀 |
| 缓存机制 | ✅✅ 有 DaoSession 缓存层 | ❌ 无内置实体缓存 |
| 编译时检查 | ⚠️ 有限 | ✅✅✅ SQL 语句编译时验证 |
| 与 LiveData 集成 | ⚠️ 需手动实现 | ✅✅✅ 无缝集成,自动通知 |
| 与 Kotlin 协程集成 | ⚠️ 需手动实现 | ✅✅✅ 原生支持 suspend 函数 |
| 与 RxJava 集成 | ⚠️ 需额外库 | ✅✅✅ 内置支持 |
| 测试支持 | ⚠️ 一般 | ✅✅ 良好,易于测试 |
| 数据库加密 | ✅ 原生支持 (集成) | ⚠️ 需额外集成 |
| 数据迁移 | ✅ 灵活 | ✅ 支持,需手动实现 |
| Kotlin 友好度 | ⚠️ 支持 | ✅✅✅ 原生优先 |
| 生态与支持 | ⚠️ 第三方,社区支持 | ✅✅✅ 官方支持,长期维护,生态完善 |
| 适用场景 | 高性能数据采集、大量数据处理 | 中大型商业应用、注重开发效率和类型安全 |
3.如何选择?
选择 GreenDAO :
- 应用对数据库性能要求极高,尤其是涉及大规模数据的批量插入、更新、查询。
- 团队对 GreenDAO 有经验,或愿意投入时间学习。
- 需要利用其
DaoSession的一级缓存来优化频繁查询的场景。 - 项目早期选型已使用 GreenDAO,且性能表现满足要求。
选择 Room :
- 追求开发效率和代码安全性。
- 项目使用 Kotlin 和 Jetpack 组件 (如 ViewModel, LiveData)。
- 需要与 LiveData 或 Kotlin 协程 集成,实现数据的自动观察和异步处理。
- 希望获得 Google 官方的长期支持和维护。
- 团队倾向于使用官方推荐的技术栈。
- 数据量和操作复杂度在中等水平,性能要求不是极端苛刻。
| 框架 | 定位 |
|---|---|
| GreenDAO | 性能怪兽:为速度而生,适合对性能有极致要求的场景。 |
| Room | 官方亲儿子:平衡了性能、安全性和开发效率,是现代 Android 开发的推荐首选。 |
9.文件存储(内部存储、外部存储)的路径和权限。
Android 提供了多种文件存储方式,用于保存应用数据。选择合适的存储方式对于应用的性能、安全性和用户体验至关重要。主要分为 内部存储 (Internal Storage) 和 外部存储 (External Storage) 两大类。
1.内部存储 (Internal Storage)
- 特点:
- 私有性: 每个应用在内部存储中都有自己的私有目录 (
/data/data/<package_name>/files/)。 - 安全性: 其他应用无法直接访问(除非设备已 root)。
- 可靠性: 数据随应用安装而存在,卸载应用时自动删除。
- 空间有限: 空间大小受设备限制,通常较小。
- 私有性: 每个应用在内部存储中都有自己的私有目录 (
- 适用场景: 存储敏感、私有的应用数据,如配置文件、缓存、数据库文件等。
1. 使用 openFileOutput() 和 openFileInput()
这是最基础的文件读写方式。
// 写入文件
try {
String filename = "config.txt";
String content = "Hello, Internal Storage!";
// MODE_PRIVATE: 文件仅本应用可读写
FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
// 读取文件
try {
String filename = "config.txt";
FileInputStream fis = openFileInput(filename);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader bufferedReader = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
fis.close();
String content = sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
2. 使用 getFilesDir() 和 getCacheDir()
getFilesDir(): 返回应用私有文件目录的File对象 (/data/data/<package_name>/files/)。适合存储重要、不应被系统清理的文件。getCacheDir(): 返回应用私有缓存目录的File对象 (/data/data/<package_name>/cache/)。适合存储可再生的临时文件。当系统存储空间不足时,可能被系统自动清理。
File filesDir = getFilesDir(); // /data/data/com.example.myapp/files
File cacheDir = getCacheDir(); // /data/data/com.example.myapp/cache
// 在 filesDir 下创建文件
File configFile = new File(filesDir, "settings.conf");
// ... 使用 FileOutputStream/FileInputStream 操作
2. 外部存储 (External Storage)
- 特点:
- 共享性: 存储在公共目录(如
Download,Pictures,Music)或应用专属的外部目录中。 - 空间大: 通常指设备的共享存储空间或 SD 卡,空间较大。
- 可移除: 可能是可移动的 SD 卡。
- 可被其他应用/用户访问: 存在安全风险。
- 状态检查: 必须在访问前检查外部存储的可用状态。
- 共享性: 存储在公共目录(如
- 适用场景: 存储用户生成的内容、媒体文件、下载的大文件等需要与其他应用共享或用户可直接访问的数据。
1. 应用专属的外部存储目录
- 特点: 路径为
/storage/emulated/0/Android/data/<package_name>/files/或/storage/emulated/0/Android/data/<package_name>/cache/。 - 权限: 无需申请
WRITE_EXTERNAL_STORAGE权限 (API 29+)。 - 私有性: 虽然在“外部”,但其他应用无法直接访问(除非 root)。卸载应用时自动删除。
- 方法:
getExternalFilesDir(String type): 获取指定类型(如Environment.DIRECTORY_PICTURES,Environment.DIRECTORY_MUSIC)的应用专属目录。传null获取根目录。getExternalCacheDir(): 获取应用专属的外部缓存目录。系统存储不足时可能被清理。
File externalPicturesDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File photoFile = new File(externalPicturesDir, "photo.jpg");
// 获取外部缓存目录
File externalCacheDir = getExternalCacheDir();</code></pre>
2. 共享的公共外部存储目录
- 特点: 存储在公共目录,如
Downloads,DCIM/Camera,Music等。用户和其他应用可以访问。 - 权限:
- API 28 及以下: 需要申请
READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。 - API 29 (Android 10) 及以上: 引入 Scoped Storage (分区存储)。
- 推荐使用 MediaStore API 或 Storage Access Framework (SAF)。
- 直接使用
FileAPI 访问公共目录受限。若必须使用,需在AndroidManifest.xml中声明android:requestLegacyExternalStorage="true"(临时方案,非长久之计)。
- API 28 及以下: 需要申请
- 方法 (推荐 - MediaStore):
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "my_image.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (OutputStream os = getContentResolver().openOutputStream(uri)) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
3. 检查外部存储状态
// 检查外部存储是否可读写
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
// 可读写
} else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
// 只读
} else {
// 不可用
}
3、最佳实践与注意事项
- 优先使用内部存储: 对于应用私有数据,优先选择内部存储,更安全。
- 合理使用缓存目录: 将可再生的临时文件放在
cacheDir,避免占用过多空间。 - Scoped Storage (API 29+):
- 核心: 应用对文件系统的访问被“分区”,减少对全局文件的随意访问。
- 策略:
- 使用 MediaStore 管理共享的媒体、下载等文件。
- 使用 Storage Access Framework (SAF) 让用户选择文件或目录。
- 使用 应用专属目录 存储私有文件。
- 权限处理: 动态申请运行时权限(针对 API 23+ 的危险权限)。
- 异步操作: 文件 I/O 可能耗时,务必在子线程中执行,避免阻塞主线程导致 ANR。
- 资源管理: 使用
try-with-resources语句确保InputStream、OutputStream、Cursor等资源被正确关闭。 - 路径兼容性: 避免硬编码文件路径,使用系统 API 获取目录。
- 数据备份: 重要数据可考虑使用
Auto Backup或Key/Value Backup服务。
4. 总结
| 存储位置 | 访问方式 | 权限要求 (API 29+) | 卸载时是否删除 | 适用场景 |
|---|---|---|---|---|
| 内部存储 | openFileOutput, getFilesDir | 无 | ✅ 是 | 私有、敏感数据 |
| 内部缓存 | getCacheDir | 无 | ✅ 是 | 临时、可再生文件 |
| 外部专属 | getExternalFilesDir | 无 | ✅ 是 | 较大私有文件 (如离线地图) |
| 外部专属缓存 | getExternalCacheDir | 无 | ✅ 是 | 大型临时文件 |
| 公共目录 (旧) | File API | WRITE_EXTERNAL_STORAGE | ❌ 否 | (不推荐) |
| 公共目录 (新) | MediaStore, SAF | 通常无 | ❌ 否 | 用户共享内容、媒体文件 |
核心原则: 最小权限、数据隔离、遵守 Scoped Storage。根据数据的私密性、生命周期和共享需求,选择最合适的存储方案。
10. 网络编程
HTTP 协议基础(GET/POST 区别、状态码、Header)。
OkHttp 的核心架构(拦截器链 Interceptor)和使用。
Retrofit 的原理(动态代理、注解解析)和使用。
JSON 解析库(Gson, Moshi)的使用。
三、性能优化
1. 内存优化
如何检测和分析内存泄漏?(工具:Android Profiler, LeakCanary)
如何避免内存抖动(频繁创建和回收对象)?
对象池 (Pools) 的使用场景。
Bitmap 的高效加载和内存管理(采样率压缩、复用、及时回收)。
2. 布局优化
如何减少布局层级?(使用 ConstraintLayout, , , )
什么是过度绘制 (Overdraw)?如何检测和避免?
onDraw() 中避免创建对象和执行耗时操作。
3. 卡顿优化
卡顿的根本原因是什么?(主线程执行耗时任务)
如何监控应用的帧率 (FPS)?
工具:Systrace, TraceView (已废弃,推荐使用 CPU Profiler)。
4. 启动优化
应用启动流程(Application, Activity)。
优化方案:异步初始化、延迟初始化、启动预加载、闪屏页优化。
5. APK 瘦身
代码混淆 (ProGuard/R8) 的作用。
资源压缩(移除无用资源、使用 WebP 格式、资源混淆)。
So 库的优化(按 ABI 打包)。
Lint 工具的使用。
四、架构与设计模式
1. 常用设计模式
1. 单例模式(Singleton)
- 定义:确保一个类只有一个实例,并提供全局访问点。
- 应用场景:
- 全局状态管理(如用户信息、配置信息)。
- 资源密集型对象(如网络请求客户端、数据库连接)。
- Android中的
Application类本质是单例。
- 示例代码:
class NetworkManager private constructor() { companion object { val instance: NetworkManager by lazy { NetworkManager() } } // 网络请求相关方法 }
2. 工厂模式(Factory)
- 定义:定义一个创建对象的接口,由子类决定实例化哪个类。
- 应用场景:
BitmapFactory用于创建Bitmap对象。- 自定义组件时,通过工厂创建不同类型的View。
- 示例代码:
interface ViewFactory { fun createView(): View } class TextViewFactory : ViewFactory { override fun createView(): View = TextView(context) }
3. 建造者模式(Builder)
- 定义:分步骤构建复杂对象,隐藏创建细节。
- 应用场景:
- Android中的
AlertDialog.Builder。 - 自定义复杂对象的构建过程(如
Request对象)。
- Android中的
- 示例代码:
4. 适配器模式(Adapter)class AlertDialogBuilder { private var title: String = "" private var message: String = "" fun setTitle(title: String): AlertDialogBuilder { this.title = title return this } fun setMessage(message: String): AlertDialogBuilder { this.message = message return this } fun build(): AlertDialog { return AlertDialog(title, message) } } - 定义:将一个接口转换为另一个接口,使不兼容的类可以协作。
- 应用场景:
RecyclerView.Adapter将数据转换为UI组件。- 适配第三方库与Android原生API的差异。
- 示例代码:
class MyAdapter(private val dataList: List<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // 创建ViewHolder } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(dataList[position]) } override fun getItemCount(): Int = dataList.size }
5. 代理模式(Proxy)
- 定义:为其他对象提供一种代理以控制对这个对象的访问。
- 应用场景:
- Android中的
Binder机制(AIDL实现跨进程通信)。 - 权限检查代理(如网络请求前检查权限)。
- Android中的
- 示例代码:
class ImageLoaderProxy(private val realImageLoader: ImageLoader) : ImageLoader { override fun loadImage(url: String) { if (hasPermission()) { realImageLoader.loadImage(url) } else { requestPermission() } } }
6. 组合模式(Composite)
- 定义:将对象组合成树形结构,表示“部分-整体”的层次结构。
- 应用场景:
- Android的
View和ViewGroup层级关系。
- Android的
- 示例代码:
abstract class View { abstract fun render() } class TextView(context: Context) : View() { override fun render() { /* 渲染文本 */ } } class LinearLayout(context: Context) : View() { private val children = mutableListOf<View>() fun addView(view: View) { children.add(view) } override fun render() { for (child in children) { child.render() } } }
7. 观察者模式(Observer)
- 定义:定义对象间的一对多依赖关系,当一个对象状态变化时,所有依赖者自动更新。
- 应用场景:
LiveData与ViewModel的协同工作。- 广播接收器(
BroadcastReceiver)。
- 示例代码:
class NewsSubject { private val observers = mutableListOf<NewsObserver>() fun addObserver(observer: NewsObserver) { observers.add(observer) } fun removeObserver(observer: NewsObserver) { observers.remove(observer) } fun notifyObservers() { for (observer in observers) { observer.update() } } } interface NewsObserver { fun update() }
8. 策略模式(Strategy)
- 定义:定义一系列算法,将每个算法封装起来并使其可互换。
- 应用场景:
- 支付方式切换(如支付宝、微信支付)。
- 排序算法动态选择。
- 示例代码:
interface PaymentStrategy { fun pay(amount: Double) } class AlipayStrategy : PaymentStrategy { override fun pay(amount: Double) { /* 支付宝支付逻辑 */ } } class WeChatPayStrategy : PaymentStrategy { override fun pay(amount: Double) { /* 微信支付逻辑 */ } } class PaymentContext(private var strategy: PaymentStrategy) { fun executePayment(amount: Double) { strategy.pay(amount) } }
9. 责任链模式(Chain of Responsibility)
- 定义:将请求的发送者和接收者解耦,使多个对象都有机会处理请求。
- 应用场景:
- Android事件分发机制(
onTouchEvent)。 - 日志过滤链。
- Android事件分发机制(
- 示例代码:
abstract class Handler { private var successor: Handler? = null fun setSuccessor(successor: Handler) { this.successor = successor } abstract fun handleRequest(request: String) } class ConcreteHandler1 : Handler() { override fun handleRequest(request: String) { if (/* 条件满足 */) { // 处理请求 } else { successor?.handleRequest(request) } } }
2. 应用架构
1. MVC(Model-View-Controller)
- 定义:
- Model:数据和业务逻辑(如数据库操作、网络请求)。
- View:用户界面(XML布局)。
- Controller:协调Model和View(Activity/Fragment)。
- 问题:传统MVC中Activity职责过重,容易成为“上帝类”。
2. MVP(Model-View-Presenter)
- 定义:
- Presenter:负责业务逻辑,独立于View。
- View:仅负责UI展示,通过接口与Presenter通信。
- Model:数据层,与MVC相同。
- 优势:解耦View和业务逻辑,便于单元测试。
3. MVVM(Model-View-ViewModel)
- 定义:
- ViewModel:暴露数据和命令,通过双向绑定(如
DataBindingUtil)与View通信。 - 优势:减少View的代码量,支持数据自动更新。
- ViewModel:暴露数据和命令,通过双向绑定(如
3. Jetpack 组件
Lifecycle 组件的原理和使用。
Navigation 组件的作用和优势。
WorkManager 的使用场景。
以下是根据你提供的提纲,针对 中高级至资深 Android 开发者岗位 的面试准备内容进行全面补充和深化。内容涵盖 技术原理、代码示例、系统级机制分析、设计思想与可读性优化,旨在帮助你构建一篇既具备技术深度又易于理解的高质量文档。
五、Framework 与原理(中高级/资深岗)
1. Binder 机制
1.为什么 Android 要使用 Binder 进行进程间通信 (IPC)?
传统 IPC(如管道、Socket、共享内存)在 Android 上存在以下问题:
- 性能差:如 Socket 基于网络协议栈,开销大。
- 安全性弱:传统 IPC 没有内核级身份校验机制。
- 复杂度高:共享内存需手动同步,易出错。
而 Binder 的优势:
| 特性 | 说明 |
|---|---|
| 高效 | 一次数据拷贝(通过 mmap 映射),传统 IPC 至少两次 |
| 安全 | 内核维护 UID/PID 权限验证,支持实名调用 |
| 面向对象 | 支持跨进程调用远程对象方法(类似 RPC) |
| 统一模型 | 所有系统服务(AMS、WMS、PMS)均基于 Binder |
关键点:Binder 是 Android IPC 的“基石”,它结合了 内核驱动 + 用户态代理 的设计,在效率、安全、抽象层次上达到了最佳平衡。
2.Binder 通信的大致流程
整个流程涉及四个核心角色:
- Client:发起请求的应用
- Server:提供服务的进程(如 AMS)
- ServiceManager:服务注册与查询中心
- Binder Driver:Linux 内核中的 Binder 驱动,负责数据传输与线程调度
1. 通信流程图解:
[Client] [ServiceManager] [Server] [Binder Driver]
| | | |
|---- get(service) --->| | |
|<--- return handle ----| | |
| | |
|-------- transact() -------------------> (IPC) |
| |---- copy data ->|
| |<- wake up thread
| |--- onTransact() |
|<-------- reply() <----------------------- |
2. 详细步骤:
-
Server 启动时注册服务
// 示例:ActivityManagerService 注册 ServiceManager.addService(Context.ACTIVITY_SERVICE, activityManager);- Server 将服务名和 IBinder 引用传递给
ServiceManager - Binder 驱动创建
binder_node并加入全局哈希表
- Server 将服务名和 IBinder 引用传递给
-
Client 查询服务
IBinder binder = ServiceManager.getService(Context.ACTIVITY_SERVICE); IActivityManager am = IActivityManager.Stub.asInterface(binder); -
Client 发起调用
am.startActivity(...); // 实际是 Proxy 调用 transact() -
Binder 驱动处理
- 将数据从 Client 用户空间拷贝到内核缓冲区
- 找到目标 Server 的
binder_node,唤醒其等待线程 - 数据再从内核拷贝到 Server 用户空间
-
Server 处理请求
onTransact()被调用,反序列化参数,执行逻辑- 返回结果通过
reply.write()写回
注意:Binder 使用 内存映射 (mmap) 实现零拷贝优化,仅需一次数据拷贝(从发送方用户空间 → 内核空间 → 接收方用户空间)。
3.AIDL 的使用和原理
1. 使用示例
定义 IUserManager.aidl:
// IUserManager.aidl
package com.example.aidl;
interface IUserManager {
User getUser(int id);
void addUser(in User user);
}
生成的 Java 文件结构(由 AIDL 编译器生成):
public interface IUserManager extends android.os.IInterface {
public User getUser(int id) throws RemoteException;
public void addUser(User user) throws RemoteException;
public static abstract class Stub extends android.os.Binder implements IUserManager {
private static final java.lang.String DESCRIPTOR = "com.example.aidl.IUserManager";
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
switch (code) {
case TRANSACTION_getUser: {
data.enforceInterface(DESCRIPTOR);
int _id = data.readInt();
User _result = this.getUser(_id);
reply.writeNoException();
reply.writeTypedObject(_result, 0);
return true;
}
case TRANSACTION_addUser: {
data.enforceInterface(DESCRIPTOR);
User _arg0;
if (data.readInt() != 0) {
_arg0 = User.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addUser(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
static class Proxy implements IUserManager {
private IBinder mRemote;
public Proxy(IBinder remote) {
mRemote = remote;
}
@Override
public User getUser(int id) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
User _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(id);
mRemote.transact(Stub.TRANSACTION_getUser, _data, _reply, 0);
_reply.readException();
_result = _reply.readTypedObject(User.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
}
}
2. 核心原理
- 动态生成代码:AIDL 编译器生成 Stub(本地)和 Proxy(远程)
- Stub 是服务端的 Binder 实体,继承 Binder 并实现接口
- Proxy 是客户端代理,持有远端 Binder 引用,调用
transact()发起 IPC - 数据必须 Parcelable:保证跨进程序列化能力
深入理解:AIDL 本质是 代理模式 + Binder 驱动 + 序列化 的组合拳,屏蔽了底层 IPC 细节。
2. Handler 消息机制深入
1.MessageQueue 是如何实现“等待”和“唤醒”的?(epoll 机制)
Android 的 MessageQueue 底层依赖 Linux 的 epoll 多路复用机制实现阻塞等待。
1. 实现机制:
// frameworks/base/core/jni/android_os_MessageQueue.cpp
static int epoll_fd = -1;
static int wake_fd = -1;
void NativeMessageQueue::pollOnce(int timeoutMillis) {
// 调用 epoll_wait 等待事件
struct epoll_event event;
int result = epoll_wait(epoll_fd, &event, 1, timeoutMillis);
if (result > 0 && event.data.fd == wake_fd) {
// 被唤醒,说明有新消息插入
awoken();
}
}
2. 工作流程:
- Looper 调用
queue.next()获取下一条消息 - 如果队列为空或下一条消息未到时间,则进入
nativePollOnce() epoll_wait()阻塞等待,直到:- 超时(定时唤醒)
- 其他线程调用
enqueueMessage()插入消息 → 触发wake()→ 写入wake_pipe→epoll返回
3. 关键点:
- pipe + epoll:通过匿名管道(pipe)实现跨线程通知
- 当
MessageQueue.enqueueMessage()插入紧急消息(msg.target != null)且当前是等待状态时,会调用nativeWake()向 pipe 写一个字节,触发 epoll 返回 - 这种机制非常高效,避免了轮询
类比:就像一个快递站,没人来取件就打盹;一旦有新包裹送达,立刻响铃叫醒。
2.Looper.loop() 为什么是死循环却不会导致 ANR?
public static void loop() {
final Looper me = myLooper();
for (;;) { // 死循环
Message msg = queue.next(); // 可能阻塞
if (msg == null) return; // 退出循环
msg.target.dispatchMessage(msg); // 分发给 Handler
msg.recycle();
}
}
1. 为什么不是卡死?
- 并非真正“死循环占用 CPU”:当没有消息时,
queue.next()会调用nativePollOnce(timeout)进入 休眠状态(底层 epoll_wait),不消耗 CPU - 事件驱动模型:只有当有消息到来或超时才会被唤醒处理
- 主线程本身就是事件循环:Activity 生命周期、点击事件、绘制等都是通过 Handler 投递的消息来驱动的
2. 那什么时候会 ANR?
当主线程正在处理某个消息(比如 onClick() 中执行耗时操作)超过 5 秒未返回,则系统认为“无响应”。
结论:
loop()的死循环 ≠ 卡顿。它是 Android UI 线程的“心脏”,驱动整个应用的消息流转。
3.View 绘制深入
1.Choreographer 的作用(协调输入、动画、绘制)
Choreographer 是 Android 4.1 引入的核心类,用于同步屏幕刷新周期,实现 60fps 流畅渲染。
1.主要职责:
- 接收 VSYNC 信号
- 统一调度:输入 → 动画 → 布局 → 绘制
- 减少丢帧(jank)
2.调用链:
Choreographer.postFrameCallback(new FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// 在下一个 VSYNC 信号到来时执行
// 可用于性能监控、自定义动画同步
}
});
3.默认三大回调队列:
CALLBACK_INPUT:处理输入事件CALLBACK_ANIMATION:运行属性动画CALLBACK_TRAVERSAL:执行 measure/layout/draw
⚙️ 所有 View 的
requestLayout()最终都会触发scheduleTraversals()→postCallback(CHOREOGRAPHER_CALLBACK_TRAVERSAL)
2.VSYNC 信号的作用
- 垂直同步信号(Vertical Sync):每 16.6ms(60Hz)由硬件发出一次
- 通知系统“可以开始下一帧的绘制”
- 避免撕裂(tearing):确保一帧完整绘制后再刷新屏幕
- Choreographer 利用 VSYNC 来对齐所有 UI 操作,实现“一帧一画”
理想情况:每个 VSYNC 周期内完成一次完整的
measure → layout → draw,否则就会 掉帧。
3. 应用启动流程
从点击桌面图标到 Activity.onCreate() 被调用,经历了以下关键阶段:
[Launcher]
↓ 启动 Intent
[AMS]
↓ startProcessLocked()
[Zygote] fork()
↓ ActivityThread.main()
[ActivityThread] attach() → handleBindApplication() → onCreate()
1. 详细流程:
-
Launcher 发送启动请求
Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); startActivity(intent); -
AMS 处理启动请求
- 检查权限、任务栈
- 若目标进程未启动,则通知 Zygote 创建新进程
-
Zygote 进程 fork 子进程
- Zygote 是所有应用进程的父进程
- 预加载了大量类(
ZygoteInit.preload()),加快启动速度 - 调用
forkAndSpecialize()创建新进程
-
新进程执行 ActivityThread.main()
public static void main(String[] args) { Looper.prepareMainLooper(); // 创建主线程 Looper ActivityThread thread = new ActivityThread(); thread.attach(false); // 向 AMS 注册 Looper.loop(); // 开启消息循环 } -
AMS 回调 applicationOnCreate
- Application 创建:
makeApplication()→attachBaseContext()→onCreate() - Activity 创建:
performLaunchActivity()→new Activity()→onCreate(savedInstanceState)
- Application 创建:
冷启动关键路径:
PackageManager→Launcher→AMS→Zygote→Binder→ActivityThread→Application.onCreate()→Activity.onCreate()
4. ANR (Application Not Responding)
1.触发条件
| 类型 | 超时时间 | 触发场景 |
|---|---|---|
| KeyDispatchTimeout | 5s | 主线程未处理完输入事件(如 onClick) |
| BroadcastTimeout | 10s(前台)/ 60s(后台) | BroadcastReceiver 执行过长 |
| ServiceTimeout | 20s | Service.onStartCommand() 未返回 |
注意:ANR 判断基于 主线程是否在规定时间内响应特定事件,与是否“卡顿”不完全等价。
2.如何分析和定位 ANR 问题?
分析步骤:
-
获取 traces.txt
adb shell cat /data/anr/traces.txt > anr.log -
查看主线程堆栈
"main" prio=5 tid=1 Native | group="main" sCount=1 dsCount=0 obj=0x74f8e000 self=0xb4f00500 | sysTid=28884 nice=0 cgrp=default sched=0/0 handle=0xb6f76534 | state=S schedstat=( 0 0 0 ) utm=4 stm=2 core=0 HZ=100 | stack=0xbe77d000-0xbe77f000 stackSize=8MB | held mutexes= at java.lang.Thread.sleep(Native method) at com.example.MyActivity.onCreate(MyActivity.java:30) at android.app.Activity.performCreate(Activity.java:7136)发现主线程在
onCreate中 sleep,导致 ANR。 -
检查 CPU 使用率
- 同时生成
dumpsys cpuinfo对比时间戳 - 判断是 CPU 密集型还是 I/O 阻塞
- 同时生成
-
使用工具辅助
- StrictMode:检测主线程磁盘/网络操作
- Systrace:可视化分析主线程执行耗时
- BlockCanary:第三方 ANR 监控库
最佳实践:避免在主线程做任何耗时操作,使用
HandlerThread、IntentService或协程进行异步处理。
六、开源框架与新技术
1. 常用框架原理
1.Retrofit:如何将接口方法转换为网络请求?
1.使用示例
public interface ApiService {
@GET("users/{id}")
Call<User> getUser(@Path("id") int id);
}
// 使用
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiService service = retrofit.create(ApiService.class);
Call<User> call = service.getUser(1);
call.enqueue(...);
2. 核心原理
-
动态代理:
retrofit.create(ApiService.class)返回的是一个代理对象return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) { // 将方法调用封装为 OkHttp Call ServiceMethod<Object, Object> serviceMethod = loadServiceMethod(method); OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); -
注解解析:
ServiceMethod解析@GET、@Path、@Query等注解,构建Request对象 -
适配器模式:支持
Call、Observable(RxJava)、LiveData等返回类型 -
转换器模式:
Converter<F, T>实现 JSON ↔ Object 转换(如 Gson)
一句话总结:Retrofit 是一个 声明式 REST 客户端,通过 动态代理 + 注解处理器 + 拦截链 将 Java 接口映射为 HTTP 请求。
2. OkHttp 拦截器链工作原理
OkHttp 使用 责任链模式(Interceptor Chain) 处理请求。
1. 拦截器顺序:
[Application Interceptors]
↓
[RetryAndFollowUpInterceptor] // 重试、重定向
↓
[BridgeInterceptor] // 添加 Host、Cookie、gzip 支持
↓
[CacheInterceptor] // 读取/写入缓存
↓
[ConnectInterceptor] // 建立 TCP 连接(RealConnection)
↓
[CallServerInterceptor] // 向服务器写请求、读响应
↓
[Response]
2. 自定义拦截器示例:
class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = System.nanoTime();
Log.d("OkHttp", "Sending request: " + request.url());
Response response = chain.proceed(request); // 继续执行后续拦截器
long endTime = System.nanoTime();
Log.d("OkHttp", "Received response in " + (endTime - startTime) / 1e6 + "ms");
return response;
}
}
优势:灵活、可插拔、便于调试和扩展。
3. Glide 图片加载流程与缓存机制
1.加载流程:
Glide.with(context).load(url).into(imageView);
- 构建请求:解析
url、transformations、placeholder等 - 内存缓存查找:先查
ActiveResources(活跃资源)和MemoryCache - 磁盘缓存查找:查
DiskLruCache(基于 LRU) - 数据源获取:从网络(OkHttp)或本地文件加载
- 解码 → 转换 → 缓存 → 显示
2.缓存层级:
| 层级 | 作用 | 实现 |
|---|---|---|
| Memory Cache | 快速访问已解码的 Bitmap | LruResourceCache |
| Active Resources | 正在使用的图片(防止被回收) | 弱引用 HashMap |
| Disk Cache | 持久化缓存原始数据或转换后结果 | DiskLruCache |
关键优化:
- 使用
BitmapPool复用 Bitmap 内存 - 支持
.thumbnail()实现渐进式加载 - 生命周期感知(绑定 Activity/Fragment)
4. EventBus 工作原理
1.使用方式
// 注册
EventBus.getDefault().register(this);
// 发布
EventBus.getDefault().post(new MessageEvent("Hello"));
// 接收
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
textView.setText(event.message);
}
2. 核心机制
-
注解处理:编译期扫描
@Subscribe方法,生成索引类(提高查找效率) -
订阅者注册:
Map<Object, List<SubscriberMethod>> subscriptions;key 是订阅者对象,value 是方法列表
-
事件分发:
post(event)→ 查找所有订阅了该 event 类型的方法- 根据
threadMode决定执行线程(POSTING、MAIN、IO、ASYNC)
-
线程切换:使用 Handler 或线程池实现
缺点:过度使用会导致“隐式调用”难以追踪,建议局部使用。
2. Kotlin
1. lateinit vs by lazy
| 特性 | lateinit | by lazy |
|---|---|---|
| 初始化时机 | 运行时手动赋值 | 第一次访问时初始化 |
| 类型限制 | 只能用于 var,非空引用类型 | 任意类型(val) |
| 是否可变 | 可变(var) | 不可变(val) |
| 线程安全 | 否 | LazyThreadSafetyMode.SYNCHRONIZED 默认安全 |
| 适用场景 | DI 注入、View Binding | 单例、耗时初始化 |
1. 示例对比:
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding // 在 onCreate 中初始化
val userManager: UserManager by lazy {
UserManager.getInstance(applicationContext) // 第一次用时创建
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
建议:优先使用
by lazy,除非必须用var且延迟初始化。
2.data class 的作用和自动生成方法
data class User(val name: String, val age: Int)
编译器自动添加:
equals()/hashCode()toString()→"User(name=John, age=25)"copy()→user.copy(age = 30)componentN()→ 解构语法val (name, age) = user
用于 数据载体类,减少模板代码。
3. 扩展函数与属性
// 扩展函数
fun String.isValidEmail(): Boolean {
return Patterns.EMAIL_ADDRESS.matcher(this).matches()
}
// 扩展属性
val Context.appName: String
get() = getString(R.string.app_name)
提高代码可读性和复用性,类似“工具类”的优雅写法。
4.密封类 sealed class vs 枚举
| 特性 | enum | sealed class |
|---|---|---|
| 实例数量 | 固定 | 子类固定,但实例可变 |
| 数据携带 | 不能携带不同数据 | 每个子类可携带不同参数 |
| 继承范围 | 同文件 | 同模块(Kotlin 1.1+) |
1.示例:
// 枚举:状态有限且无数据
enum class Status { LOADING, SUCCESS, ERROR }
// 密封类:更灵活的数据表达
sealed class Result<out T> {
object Loading : Result<Nothing>()
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
// 使用
when (result) {
is Result.Loading -> showLoading()
is Result.Success -> updateUI(result.data)
is Result.Error -> showError(result.exception)
}
推荐:网络请求结果、UI 状态管理使用
sealed class。
5.协程 Coroutine 与 suspend 原理**
1. 基本使用
lifecycleScope.launch {
val user = fetchUser() // suspend 函数
textView.text = user.name
}
suspend fun fetchUser(): User {
return withContext(Dispatchers.IO) {
api.getUser() // 耗时操作
}
}
2. 挂起函数原理
suspend函数不是线程阻塞,而是 协作式挂起- 编译器将其转换为状态机(State Machine)
- 通过
Continuation保存执行上下文 - 当 I/O 完成后,回调
resume()恢复执行
// 伪代码:编译器生成的状态机
int label = 0;
Object result = null;
while (true) {
switch (label) {
case 0:
label = 1;
return suspendFun(continuation); // 挂起点
case 1:
result = (User) result; // 恢复后继续执行
return result.name;
}
}
优势:用同步写法实现异步逻辑,避免回调地狱。
3. Flutter / Compose
1.Flutter:Widget、State、Context
- Widget:UI 的不可变描述(类似 React)
- State:有状态组件的数据持有者(StatefulWidget)
- Context:Widget 在树中的位置引用,用于查找父级或主题
class CounterWidget extends StatefulWidget {
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int count = 0;
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Count: $count'),
);
}
}
声明式 UI:每次
setState()都会重建 Widget 树,由框架决定最小更新范围。
2.Jetpack Compose:声明式 UI vs 命令式 UI
| 对比项 | 传统 View 系统(命令式) | Jetpack Compose(声明式) |
|---|---|---|
| 更新方式 | findViewById().setText() | 重新调用 @Composable 函数 |
| 状态管理 | 手动更新视图 | 状态驱动 UI 重建 |
| 性能优化 | 手动 diff | 智能重组(Recomposition) |
| 代码结构 | XML + Java/Kotlin | 纯 Kotlin |
1.示例对比:
// 命令式
button.setOnClickListener { textView.text = "Clicked!" }
// 声明式
@Composable
fun ClickCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}
优势:更简洁、更易测试、更好支持动态 UI。
七、场景题与开放性问题
1. 项目相关
1.请介绍一下你做过的最有挑战性的项目
回答模板(STAR 法则):
- Situation:项目背景(用户量、业务复杂度)
- Task:你的职责(主导/参与)
- Action:关键技术方案(如组件化、性能优化、架构设计)
- Result:量化成果(启动速度↓30%、崩溃率↓50%)
1.示例:
“我主导了一个电商 App 的重构项目。原项目耦合严重,启动耗时达 3.2s。我们采用 模块化 + 启动器优化 + 冷启动预加载 方案,最终将冷启动时间降至 1.1s,用户留存提升 18%。”
2.你是如何进行性能优化的?
- 启动优化:异步初始化、延迟加载、启动器编排
- 内存优化:LeakCanary 检测泄漏、Bitmap 复用、ArrayMap 替代 HashMap
- 卡顿优化:Systrace 分析主线程耗时、使用协程异步处理
- 包大小优化:资源压缩、WebP、动态下发、ProGuard/R8
- 电量优化:JobScheduler 替代 AlarmManager、减少 WakeLock
3.如何保证代码质量和可维护性?
- 编码规范:Kotlin 风格指南、命名统一
- 静态检查:Lint、Detekt、ktlint
- 单元测试 & UI 测试:JUnit、MockK、Espresso
- CI/CD:GitLab CI 自动打包、SonarQube 扫描
- 文档:Wiki 记录架构设计、接口文档
2. 设计题
1.如何设计一个图片加载框架?
1.核心模块:
- 调用层 API:
ImageLoader.with(context).load(url).into(iv) - 请求管理:Request、ImagePipeline
- 缓存层:
- 内存缓存:LruCache<Key, Bitmap>
- 磁盘缓存:DiskLruCache
- 数据源:网络(OkHttp)、本地、资源
- 解码器:BitmapFactory + WebP 支持
- 变换器:圆角、模糊、裁剪
- 生命周期感知:绑定 Activity/Fragment 自动取消请求
参考 Glide 架构,但简化依赖。
2.如何设计一个网络请求框架?
1.模块划分:
- Request/Response 模型
- 拦截器链(参考 OkHttp)
- 序列化插件(Gson、Moshi)
- 缓存策略(HTTP Cache、本地持久化)
- 线程调度(主线程回调)
- 错误重试机制
- HTTPS 配置扩展
支持同步/异步、支持 Kotlin 协程挂起函数。
3.如何实现全局用户登录状态管理?
1.方案一:单例 + LiveData
object UserManager {
private val _isLoggedIn = MutableLiveData<Boolean>()
val isLoggedIn: LiveData<Boolean> = _isLoggedIn
fun login(token: String) {
// 保存 token
SharedPreferences.save("token", token)
_isLoggedIn.value = true
}
fun logout() {
SharedPreferences.clear()
_isLoggedIn.value = false
}
}
2.方案二:使用 MVI 或 Redux 模式(如 MVIKotlin)
推荐结合 Jetpack 架构组件,实现跨页面状态同步。
3. 开放性问题
1.你平时是如何学习新技术的?
示例回答:
“我主要通过官方文档 + GitHub 源码 + 技术博客(如 Medium、掘金)学习。每周会安排固定时间阅读 Android Developers Blog 和 Kotlin Weekly。对于重点技术(如 Compose),我会动手写 Demo 并分享到博客。”
2.你如何看待 Android 的未来发展?
示例回答:
“我认为 Android 正在向 声明式 UI(Compose)、跨平台(Kotlin Multiplatform)、模块化与动态化 演进。随着 AI 和大模型的发展,本地智能推理、个性化推荐也会成为新方向。同时,隐私安全和后台限制将持续加强。”
3.你对我们公司/团队有什么想了解的?
建议提问:
- 团队当前的技术栈和主要产品方向?
- 是否有技术分享或成长激励机制?
- 未来半年最重要的技术挑战是什么?
708

被折叠的 条评论
为什么被折叠?



