Skip to content

面经

Java

Java基础

类加载机制

双亲委派模式

双亲委派是一种任务委派模式,是java中通过加载工具加载类文件的一种具体方式。具体表现为:

  1. 如果一个类加载器收到了类加载请求,他并不会自己先加载,而是把这请求委派给父亲的类加载器去执行。
  2. 如果父类加载器还存在父类加载器,则进一步向上委托,依次递归,请求最终将到达最顶层的类加载器。
  3. 若父类加载器成功完成,则不会继续传递,成功返回;如果父类加载器无法完成任务,子加载器才会尝试进行加载。
  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,就会加载此类,如传递到系统加载器也无法加载此类就会报错
双亲委派模型的优缺点

优点:

  • 避免类的重复加载:当父类加载过某一个类后,子类就不会再重新加载这个类。
  • 保证安全性

缺点:

在双亲委派模式中,子类加载器可以使用父类加载器已经加载过的类,但是父类加载器无法使用子类加载器加载过的类。(类似于继承关系)

类加载五个阶段
  1. 加载:读取字节码,生成Class对象
  2. 验证:校验字节码格式、语义
  3. 准备:为静态变量分配内存并设零值(static int a = 10此时a=0)
  4. 解析:符号引用->直接引用
  5. 初始化:执行<clinit>(静态变量赋值+静态代码块)
创建对象过程
  1. 类加载检查:虚拟机遇到一条new指令时,会首先去检查这个指令的参数是否能在常量池中定位到一个类的,并且检查这个符号引用代表的类是否已经被加载解析和初始化过,如果没有,那必须先执行相应的类加载过程
  2. 分配内存:在类加载检查通过后,接下来虚拟机将会为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。
  3. 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在JAVA代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 进行必要设置,比如对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  5. 执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,单从java程序的视角看,对象创建才刚刚开始构造函数,即class文件中的方法还没有执行,所有的字段都还为0,对象需要的其他资源和状态信息还没有按照预定的意图构建好。所以一般情况来说,执行new指令之后接着会执行方法,把对象按照程序员的意愿进行初始化
对象生命周期
  • 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配
  • 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
  • 销毁:当对象不再被引用,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的校验过程。

设计模式

代理模式(proxy)

为目标对象提供一个代理对象,由代理控制对目标对象的访问,可以在访问前后添加额外逻辑。

java
/**
 * 代理模式(Proxy)
 */
interface Api {
    void request();
}

class RealApi implements Api {

    @Override
    public void request() {
        System.out.println("hello");
    }
}

class ApiProxy implements Api {
    private RealApi realApi = new RealApi();
    @Override
    public void request() {
        realApi.request();
    }
}

分为静态代理(手写代理类)、动态代理(Proxy.newProxyInstance运行时生成)。

  • 静态代理:在编译期就产生代理类,性能好;但是每一个接口都会对应一个类会有大量的类产生,结构冗余,难维护。
  • 动态代理:运行时产生代理,涉及到反射,性能开销更大,结构简单,可维护性强,面对接口更改情况下改动小,不会修改源代码。

应用:

  • Retrofit:通过动态代理拦截接口方法调用,解析注解生成网络请求。
装饰器模式(Decorator)

在不修改原类,不使用继承的前提下,动态的给对象添加新功能。装饰器与被装饰者实现同一接口,并持有被装饰者的引用。

java
InputStream in = new BufferedInputStream(  // 装饰:加缓冲
                    new FileInputStream(file)); // 被装饰者

应用:

  • Java IO流
  • Android的ContextWrapper

符合开闭原则,可以多层嵌套灵活组合功能。

单例模式

保证一个类在全局只有一个实例,并提供统一访问点。

实现:

懒汉方式

java

public class Singleton {
    private static Singleton instance;
    private Singleton() { }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点:只有使用时才会被初始化,节约资源。

缺点:每次调用需要实例化,浪费时间,每次调用getInstance都同步,造成不必要的开销。

Double Check Lock方式

java
/**
 * Double Check Lock(双重检查锁模式)
 */
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() { }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

优点:资源利用率高,第一次getInstance时菜实例化,效率高

缺点:第一次加载稍慢

两次判空的原因

第一次盼空用于提高性能,如果实例已经存在了,就不需要再次加锁减少系统开销;第二次判空是当多个线程同时进入getInstance时,如果instance==null,不会创建多个instance对象。

volatile关键字防止指令重排。

静态内部类单例模式

java
/**
 * 静态内部类单例模式
 */
public class Singleton {
    private Singleton() { }
    public static Singleton getInstance() {
        return SinglertonHolder.instance;
    }
    private static class SinglertonHolder {
        private static final Singleton instance = new Singleton();
    }
}

优点与双重检查锁一样,并且不会出现无法初始化情况。

建造者模式(Builder)

将复杂对象的构建过程与表示分离,通过链式调用逐步设置参数,最后统一构建。

比如Okhttp中:

java
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .addInterceptor(logInterceptor)
    .build();

优点:构造参数过多(尤其是可选参数多)时,避免重叠构造器(多个构造函数重载)的混乱,让复杂对象的构建过程变得清晰、可控、可扩展,提高可读性。

工厂模式

创建型设计模式

定义:定义一个用于创建对象的接口,让子类决定实例化哪个类

组成:需要抽象产品,抽象工厂以及对应的实现类。

优点:

  1. 客户端不直接使用new, 不依赖具体类,而是依赖某个接口。
  2. 想要添加新的产品,只需要增加新的工厂类。
  3. 逻辑都在工厂类中,维护等工作不会影响其他代码。

缺点:每多一个产品就要多创建一个工厂类

责任链模式

行为型设计模式

将多个处理者连城一条链,请求沿着链传递,每个处理者决定自己处理还是传给下一个。

java
package designpattern.chain;

public class ChainResponsibility {
    public static void main(String[] args) {
        Approver teamLeader = new TeamLeader();
        teamLeader.setNext(new Manager())
            .setNext(new Director());
        teamLeader.handleRequest(7);
    }
}

abstract class Approver {
    protected Approver next; // 下一个处理者
    public Approver setNext(Approver next) {
        this.next = next;
        return next;
    }
    public abstract void handleRequest(int leaveDays);
}

class TeamLeader extends Approver {
    @Override
    public void handleRequest(int leaveDays) {
        if (leaveDays <= 2) {
            System.out.println("TeamLeader 批准" + leaveDays + "天");
        } else if (next != null) {
            next.handleRequest(leaveDays);
        }
    }
}

class Manager extends Approver {

    @Override
    public void handleRequest(int leaveDays) {
        if (leaveDays <= 5) {
            System.out.println("Manager 批准" + leaveDays + "天");
        } else if (next != null) {
            next.handleRequest(leaveDays);
        }
    }
    
}

class Director extends Approver {
    public void handleRequest(int leaveDays) {
        if (leaveDays <= 10) {
            System.out.println("Director 批准 " + leaveDays + " 天");
        } else {
            System.out.println("假期太长,没人能批");
        }
    }
}

优点:

  1. 请求发送者不知道最终处理的是谁,降低耦合。
  2. 灵活组合,调整责任链顺序。
  3. 易于拓展新增加的处理者只需要继承处理器并插入链表中即可。

缺点:

  1. 所有的请求不一定都被处理,可能导致请求丢失
  2. 请求处理路径不明确,调试困难。
观察者模式

定义:对象间一种一对多的依赖关系,每当一个对象改变状态,则所有依赖他的对象都会得到通知并被自动更新

一个被观察者可以同时拥有多个观察者,但是一个观察者不能观察多个对象。

java
package designpattern.observe;

import java.util.ArrayList;
import java.util.List;

public interface Observer {
    void update(String weather);
}

class PhoneDisplay implements Observer {

    @Override
    public void update(String weather) {
        System.out.println("Phone:" + weather);
    }
    
}

class TVDisplay implements Observer {
    public void update(String weather) {
        System.out.println("电视播报:" + weather);
    }
}

class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private String weather;

    public void addObserver(Observer o) {
        observers.add(o);
    }

    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    public void setWeather(String weather) {
        this.weather = weather;
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer o : observers) {
            o.update(weather);
        }
    }
}

优点:

  1. 被观察者无需知道观察者具体实现,只负责通知。
  2. 动态响应,观察者可以在运行时自由的添加或者移除
  3. 一次状态改变,自动通知所有观察者

缺点:

  1. 如果观察者多,频繁通知可能影响性能。
  2. 通知无序,通知顺序无法保证,可能引发并发问题。
  3. 如果不及时移除观察者,可能造成内存泄露。

深拷贝与浅拷贝

  • 浅拷贝是指只服之对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。即浅拷贝只是创建一个新的对象,然后把原对象的字段值复制到新对象中,但如果原对象内部有引用类型的字段,只是把引用复制到新对象中,两个对象指向的是同一个引用对象。
  • 深拷贝是在复制对象的同时,将对象内部所有引用类型字段的内容也复制一份,而不是共享引用。即深拷贝会递归复制对象内部所有引用类型的字段,生成一个全新的对象以及其内部的所有对象。

JVM

运行时数据区
Plaintext
┌─────────────────────────────────────────┐
│              JVM 运行时数据区              │
├──────────────┬──────────────────────────┤
│  线程私有     │  线程共享                  │
├──────────────┼──────────────────────────┤
│ 程序计数器    │  堆(Heap)               │
│ 虚拟机栈     │   - 新生代(Eden+S0+S1)    │
│ 本地方法栈   │   - 老年代                 │
│              │  方法区(元空间)           │
│              │   - 类信息、常量池、静态变量 │
└──────────────┴──────────────────────────┘
  • 方法区:存储类信息,常量,静态变量,所有线程共享区。
  • 堆区:内存最大的区域,所有线程创建的对象都会在这个区里面分配内存,而在虚拟机栈中分配的实际上是指向堆的一个引用,GC主要是对这部分区域进行处理,也是内存泄漏的主要发生区域,所有线程共享区。
  • 虚拟机栈:存储当前的局部变量表,操作数栈等数据,线程独有区,也就是每个线程在运行时都有一个独立的虚拟机栈。

当每个java方法执行时,java虚拟机会同步创建一个栈帧(stack frame),用于存储该方法的局部变量,操作数栈(存储方法执行中的临时数据),动态链接(保存对常量池的引用),方法出口(调用完成后的返回位置)。当一个方法被调用完毕后就会被弹出并返回给调用者。

内存溢出Java.lang.stackOverflowError 栈内存溢出就是因为虚拟机栈中栈帧过多或每个栈帧占用内存过大。

  • 本地方法栈:与虚拟机栈发挥作用类似,只不过这个是用于native层。
  • 程序计数器:存储当前线程执行目标方法到哪行,也是线程私有的。执行java方法时,计数器记录虚拟机字节码指令的地址。执行native方法时,计数器为空。
四种引用类型
  1. 强引用(StrongReference)。强引用是Java中最常用的引用类型,平常默认创建的也是强引用。当一个对象为强引用类型时,永远不会被垃圾回收,只有在程序结束或者对象被赋值为null时才会释放强引用。
  2. 软引用(SoftReference)。当JVM内存充足时,不会回收软引用对象。而如果内存不足时,可能会被回收(GC并不一定会回收软引用对象)。
java
SoftReference<Object> softRef = new SoftReference<>(new Object());
  1. 弱引用(WeakReference)。GC只要发现弱引用对象,无论内存是否充足,都会立即回收。
java
WeakReference<Object> weakRef = new WeakReference<>(new Object());
  1. 虚引用(PhantomReference)。不能通过get()获取对象,只能用于跟踪即将对被引用对象进行的收集,相当于对象被回收前的“通知”机制,必须与ReferenceQueue类联合使用。
LeakCanary原理

LeakCanary的一个简单实现思路就是将弱引用和引用队列ReferenceQueue进行关联,如果弱引用的引用对象被回收,JVM就会把这个弱引用加入到相关联的引用队列中。LeakCanary会延迟一段时间,然后检查这个RefrenceQueue,如果该组件在队列中,就证明没有发生内存泄漏。这样就能检测一个对象是否被GC回收。

堆的分类
  • 新生代:分为Eden区和Survior区,大多数新生成的对象会被存放到此位置。eden区比较小,当eden区满了以后会发生一次GC,存活下来的会被放到Survivor区,survivor区有01两个区域,0中内存满了后会进行一次GC,存活下来的会放到1区,两者交替当作对象的存放处
  • 老年代:存放一次或者多次GC后活下来的对象,老年代中生命周期普遍较长,发生GC频率相对较低。
  • 元空间:从java8开始,永生代被元空间所取代,用于存储类的元数据信息,如类的结构信息(字段,方法信息等)。
Java 堆
├── 新生代(Young Generation)
│   ├── Eden 区
│   ├── Survivor 0 区
│   └── Survivor 1 区
└── 老年代(Old Generation)

元空间和永久代的区别:

**永久代:**出现在jdk1.8之前,存储在堆区中,容量固定(大量动态生成类(比如动态代理的时候)容易出现oom)

**元空间:**使用本地内存,不再受限于堆大小限制。并且类元信息可以更灵活回收,不易出现oom

GC垃圾回收

判断哪些是垃圾的方法:
  1. 引用计数法

为每一个对象添加一个引用计数器,当这个对象的引用被创建时,计数器+1;当这个引用不在指向这个对象时,计数器-1。如果一个对象的引用计数器为0了,就说明不再被任何其他对象引用,GC就可以进行回收了。

虽然这样会占用一些其他内存进行计数但原理简单,也比较有效率,但是却无法解决循环引用的问题,比如A引用了B,B引用了A,这时A和B永远也不会被回收,容易造成内存泄漏。

  1. 可达性分析算法

基本思路是通过“GC Roots”的根对象作为起始节点集,根据引用关系向下搜索,走过的路径被称作“引用链”。如果一个对象没有被任何引用链相连,则说明这个对象不可达,则证明对象是垃圾,GC可以进行回收。

GC Roots主要有虚拟机栈中的局部变量,活动的线程,静态变量,常量池等。

Java虚拟机内存回收算法

标记-清除

先标记所有可回收的对象,再统一回收掉被标记的对象。同时会产生不连续的内存碎片,这会导致后面如果需要分配较大对象时,找不到连续的内存,而不得不再次触发GC。

复制-清除

先将内存划分为两块,每次只使用其中的一块,这块用完时,就将存活的对象转移到另一半上面,然后将已使用的全部清理就好了,就不用考虑内存碎片的问题,非常简单。缺点就是需要两倍空间。

标记-整理

简单来说就是先用标记-清理算法,然后将内存从后往前移动,清除内存碎片,同时更新对象的指针。

分代回收策略

大概就是前面三种方法的实际应用,新生代,老年代,永久代,大部分虚拟机厂商使用这个方式进行GC。

新生代:朝生夕灭,存活时间短。eg:某一个方法的局部变量,循环内的临时变量等等。

老年代:生存时间长,但总会死亡。eg:缓存对象,数据库连接对象,单例对象等等。

永久代:几乎一直不灭。eg:String池中的对象,加载过的类信息。

1.Java泛型的类型擦除是什么?有什么影响?

Java泛型是编译期特性,编译后泛型信息被擦出——List<String>List<Integer>在运行时都是List,类型参数被替换为上界(无界则为Object)。

影响:

  1. 运行时无法获取泛型类型
  2. 不能创建泛型数组:比如new T[]
  3. 不能用基本类型作为类型参数,比如int
  4. 可以通过反射绕过泛型检查。

追问:PECS 原则是什么?

Producer Extends, Consumer Super:

  • 只读取数据(生产者)用 <? extends T>:可以安全地读出 T 类型(这里的类都是T的子类)
  • 只写入数据(消费者)用 <? super T>:可以安全地写入 T 类型(这里的类都是T的父类)
  • 既读又写不用通配符

典型例子:Collections.copy(List<? super T> dest, List<? extends T> src)

Kotlin 用 out(协变,对应 extends)和 in(逆变,对应 super)在声明处指定型变,比 Java 的使用处型变更简洁。reified 内联泛型可以在运行时获取类型信息。

2.父类与子类的加载顺序,包括静态变量,普通变量,构造方法

顺序:父类静态变量,子类静态变量,父类普通成员变量,父类构造方法,子类普通成员变量,子类构造方法。

Java集合

1.HashMap原理

JDK1.7之前,HashMap的数据结构是链表和数组,通过哈希算法将元素key映射到数组中的槽位上,如果多个键映射到同一个槽位,会以链表的形式储存在同一个槽位上,而链表查询时间为O(n),故冲突很严重,一个索引上的链表很长的话,效率就很低了。

在JDK1.8时,当某个桶的链表长度>=8时且哈希表数组长度>=64,会把链表转为红黑树,把桶的查找时间复杂度从O(n)降至O(logn);如果数组长度<64,则只会出发扩容而不会树化,同样在扩容(resize())中,若某个桶的节点数<=6,红黑树会被退化成链表。

基本工作流程

  • 插入数据时,先根据 keyhashCode() 计算哈希值。
  • 再通过扰动函数和数组长度计算出桶位置。((n - 1) & hash)
  • 如果桶为空,直接放入。
  • 如果桶不为空,说明发生哈希冲突。
  • 冲突时,会比较 hashequals(),决定是覆盖旧值还是追加新节点。
  • 如果同一个桶里的链表过长,会转换成红黑树,提高查找效率。

2.Hash冲突解决方法

  • 链接法:用链表或其他数据结构来存储冲突的键值对,将它们链接在同一个哈希桶中。
  • 开放寻址法:在哈希表中找到另一个可用的位置来存储冲突的键值对,而不是存储在链表中。常见的开放寻址方法包括线性探测、二次探测和双重散列。
  • 再哈希法:当发生冲突时,使用另一个哈希函数再次计算键的哈希值,直到找到一个空槽来存储键值对。
  • 哈希桶扩容:当哈希冲突过多时,可以动态地扩大哈希桶的数量,重新分配键值对,以减少哈希冲突的概率。

3.HashMap是线程安全的吗

HashMap不是线程安全的:

  • JDK 1.7时HashMap采用数组+链表的数据结构,在多线程场景下,数组扩容会存在Entry链死循环和数据丢失的问题。
  • JDK 1.8时新引入了红黑树的数据结构,优化了扩容方案,解决了Entry链死循环和数据丢失的问题。但是多线程场景下,put方法存在数据覆盖的问题。

如果要保证线程安全:

  • 多线程环境可以用Collections.synchronizedMap同步加锁的方式,还可以用Hashtable,但是同步方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。
  • ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7用Segment+HashEntry的分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表性能过长的问题。

HashMap 允许 null key(hash 为 0),而 ConcurrentHashMap 不允许 null key/value。

4.介绍一下HashMap的put的过程?为什么用红黑树?

HashMap在JDK 1.8中底层是数组+链表+红黑树。put流程如下:

  1. 对key调用hash()方法:(h = key.hashCode()) ^ (h >>> 16),高 16 位异或低 16 位做扰动,减少碰撞
  2. (n-1) & hash 定位桶索引
  3. 如果桶为空,直接创建新节点放入
  4. 如果桶不为空,判断第一个节点的 key 是否相同(hash 相等且 equals 为 true),相同则覆盖 value
  5. 如果是红黑树节点,调用 putTreeVal 插入
  6. 否则遍历链表,尾插法插入新节点。如果链表长度 ≥ 8 且数组长度 ≥ 64,将链表转为红黑树;数组长度 < 64 时优先扩容
  7. 插入后如果 size > threshold(容量 × 负载因子 0.75),触发 resize 扩容

用红黑树是因为链表过长时查找退化为 O(n),红黑树保证 O(log n)。

5.HashMap调用空指针一定安全吗

不一定:

  • 空指针异常。如果 HashMap 变量本身是 null(还没 new),那么调用它的任何方法都会抛 NPE。而初始化完成后null作为key调用都是合法的,不会抛NPE,因为HashMap明确支持null键。
  • 线程安全。HashMap 本身不是线程安全的。如果在多线程环境中,没有适当的同步措施,同时对 HashMap 进行读写操作可能会导致不可预测的行为。可以考虑使用ConcurrentHashMap

6.HashMap key 可以为null吗?

可以为null。

  • hashMap虽然支持key和value为null,但是null作为key只能有一个,null作为value可以有很多个。
  • 因为hashMap中,如果key值一样,那么会覆盖相同key值的value为最新,所以key为null只能有一个。

7.HashMap的扩容机制

hashMap默认的负载因子为0.75,如果hashmap中的元素个数超过了总容量的75%,则会触发扩容:

  1. 对哈希表长度的扩展(2倍)
  2. 将旧哈希表中的数据放到新的哈希表中。

那么,元素的位置要么在原位置,要么在原位置再移动2次幂的位置,那么在扩充时,就不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,0的话索引不变,1的话索引变成“原索引+oldCap”。

8.ConcurrentHashMap用了乐观锁还是悲观锁?

都有用到,添加元素时会先判断容器是否为空:

  • 如果为空则使用volatile加CAS(乐观锁)来初始化。
  • 如果容器不为空,则根据存储的元素计算该位置是否为空。
  • 如果根据存储的元素计算结果为空,则利用CAS(乐观锁)设置该节点;
  • 如果根据存储的元素计算结果不为空,则使用 synchronized(悲观锁) ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。

9.Hashtable底层实现原理是什么

Hashtable底层数据结构主要是数组加上链表,数组是主体,链表是解决hash冲突存在的。Hashtable是线程安全的,实现方法是给所有公共方法都加上synchronized关键字。

10.说一下HashMap和Hashtable、ConcurrentMap的区别

维度HashMapHashtableConcurrentHashMap
线程安全❌ 否✅ 是 (全表加锁)✅ 是 (细粒度锁)
性能效率最高 (无锁)最低 (串行阻塞)极高 (并发读写)
Null键/值允许 1个Null键,多个Null值均不允许均不允许 (多线程下有歧义)
底层结构数组 + 链表 + 红黑树 (1.8)数组 + 链表数组 + 链表 + 红黑树 (1.8)
默认容量/扩容16,扩容为 2 倍11,扩容为 2n+116,扩容为 2 倍
使用现状单线程首选⚠️ 基本淘汰多线程并发首选
  • 追问:ConcurrentHashMap的演进JDK 1.7:分段锁 (Segment Lock)

JDK 1.8:锁桶头 + CAS + 红黑树优化

  • 结构:把整个大表拆分成多个小表(Segment)。
  • 加锁机制:使用 ReentrantLock。每次写操作只锁住当前 Segment,不同 Segment 之间的写入互不干扰。
  • 缺陷:锁的粒度依然有些大(一段里有多个元素),且并发度受限于 Segment 数量。
  • 结构:废弃了 Segment,结构与 1.8 的 HashMap 保持一致(引入红黑树解决链表过长查询慢的问题)。
  • 加锁机制:使用 volatile + CAS + synchronized
    • 无冲突时:如果对应数组槽位是空的,直接用 CAS乐观锁 写入,无阻塞。
    • 有冲突时:发生哈希碰撞,只用 synchronized 锁定当前链表/红黑树的头节点
  • 优势:锁粒度从“段”极限缩小到了“单个桶”,大大降低了锁竞争,并发度拉满。

追问:为什么ConcurrentHashMap不允许存null?

如果你调用 get(key) 返回了 null,在多线程下你无法判断是因为没找到这个 key,还是说这个 key 存的值就是 null。虽然可以通过 containsKey(key) 去验证,但在并发环境中,验证的这一瞬间,数据可能已经被其他线程篡改了,所以直接在源码层面禁止存 null 来规避风险。

Java并发

volatile

可见性

volatile变量的写会立即刷新到主内存,读会从主内存重新加载:

java
volatile boolean running = true;

// 线程A
public void stop() {
    running = false; // 写入主内存
}

// 线程B
public void run() {
    while (running) { // 每次从主内存读取
        // do work
    }
}

没有volatile,线程B可能永远看不到running = false(因为工作内存缓存了旧值)。

有序性

volatile通过插入内存屏障禁止指令重排

屏障类型插入位置作用
StoreStorevolatile 写之前禁止上面的普通写与 volatile 写重排
StoreLoadvolatile 写之后禁止 volatile 写与下面的读重排
LoadLoadvolatile 读之后禁止 volatile 读与下面的普通读重排
LoadStorevolatile 读之后禁止 volatile 读与下面的普通写重排

不保证原子性

java
volatile int count = 0;

// ❌ 不安全!i++ 是三步操作:读-改-写
count++; // 1.读count 2.count+1 3.写回count
// 两个线程可能同时读到相同值,各自+1后写回,丢失一次自增

// ✅ 用 AtomicInteger
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS 保证原子性

DCL单例中为什么需要volatile

java
public class Singleton {
    // 必须加 volatile!
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {        // 第二次检查
                    instance = new Singleton(); // 非原子操作!
                }
            }
        }
        return instance;
    }
}

new Singleton()分为三步:

  1. 分配内存
  2. 调用构造方法初始化
  3. 将引用赋值给instance

没有volatile,步骤2和3可能重排序。线程A执行了1->3(还没执行2),B发现instance != null就直接返回未初始化的对象。

线程中的锁

  1. 悲观锁与乐观锁
    • 悲观锁:认为并发冲突容易发生,操作前先加锁,如 synchronizedReentrantLock
    • 乐观锁:先执行操作,更新时通过 CAS 检查是否冲突,如原子类。
  2. 互斥锁与共享锁
    • 互斥锁:同一时间只允许一个线程持有,如写锁、ReentrantLock
    • 共享锁:允许多个线程同时持有,如 ReentrantReadWriteLock 的读锁。
  3. 可重入锁与不可重入锁
    • 可重入锁:线程持有锁后,可以再次获得同一把锁,如 synchronizedReentrantLock
    • 不可重入锁:线程重复获取同一把锁会导致阻塞或死锁。
  4. 公平锁与非公平锁
    • 公平锁:按照线程等待顺序分配锁。
    • 非公平锁:新线程可以直接竞争锁,吞吐量通常更高,但可能导致部分线程长时间等待。
  5. 自旋锁与阻塞锁
    • 自旋锁:线程通过循环不断尝试获得锁,不立即挂起,适合锁持有时间短的情况。
    • 阻塞锁:获取失败后线程进入等待状态,释放 CPU,适合等待时间较长的情况。
  6. 读锁与写锁
    • 读锁:多个读线程可以同时持有。
    • 写锁:同一时间只能由一个线程持有,并且会排斥读锁。
    • Java 中常见实现是 ReentrantReadWriteLock
  7. 可中断锁与不可中断锁
    • 可中断锁:等待锁时可以响应中断,如 ReentrantLock.lockInterruptibly()
    • 不可中断锁:等待过程中不能通过普通中断立即退出,如进入 synchronized 的锁竞争。
  8. 轻量级锁与重量级锁
    • 轻量级锁:多个线程交替执行同步块(无竞争),存在锁的竞争,这个时候偏向锁会升级为轻量级锁,线程会通过自选的方式获得锁。
    • 重量级锁:多个线程并发访问,竞争时间过长时,轻量级锁会升级成为重量级锁,拿不到锁的线程就会进入阻塞状态

死锁

定义:两个或多个线程互相持有对方想要的锁,但同时又在等待对方释放锁,导致所有相关的线程都无法继续向下执行。

死锁产生的条件

  • 互斥条件:指运算单元对锁分配到的资源具有排他性,即一段时间内某个锁资源只能被一个线程所占用
  • 请求和保持条件:指线程已经已经保持至少一个资源,但又提出了新的资源请求,但是该资源已经被其他线程锁住,此时线程被锁住,但又无法释放自己的资源
  • 不可剥夺条件:线程已经获得的资源在未使用完之前不能被剥夺。
  • 环路等待条件:发生死锁时,必然存在线程和资源的环形链,即线程正在等待另一个线程的资源,但是对方线程在等待自身的资源

解决死锁的办法

在多线程代码中,强制规定所有线程获取锁的顺序。比如锁A和锁B,规定所有线程都必须先获取锁A,再获取锁B。只要顺序一致,永远不会发生死锁。

synchronized

主要用于解决多个线程之间访问资源的同步性,被关键字修饰的方法或者代码块,同一时间只有一个线程可访问。

synchronized是Java提供的内置锁,这种内置的并且使用者看不到的也叫监视器锁。

能做到两件事:

  1. 保证原子性:被 synchronized 包裹起来的代码块,在同一时刻只能被一个线程执行。不管这段代码有多长,别人都插不进来,这就解决了我们前面说的 i++ 不安全的问题。
  2. 保证可见性:当一个线程释放 synchronized 锁出洗手间时,它在里面做的所有修改,都会立刻刷新到主内存中,让下一个进来的线程清清楚楚地看到最新数据。

特性:

  1. 可重入性:方法A和B都加了同一把锁,当你进入方法A时,拿到了锁,此时在A里面调用B,就不需要再拿锁了。避免了自己把自己死锁的问题。
  2. 自动释放锁
实现

当加入synchronized代码后,java编译器会分下面两种情况进行编译:

修饰同步代码块

此时反编译后的字节码指令中,会看到两个核心指令:monitorentermonitorexit

  • monitorenter(进入监视器):线程执行到这里,会尝试获取对象的锁。
  • monitorexit(退出监视器):线程执行完或者发生异常时,释放锁。

反编译后通常会有 1个 monitorenter2个 monitorexit。为什么多出一个?因为 JVM 要保证哪怕你的代码抛出了异常,也会执行第二个 monitorexit 进行兜底解锁,这就是前面说的“自动释放锁”的底层原因。

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。

synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。

修饰同步方法

这时候,反编译后,我们会在方法的访问标志(flags)里,多了一个叫 ACC_SYNCHRONIZED 的标志位。

JVM调用方法时,看到这个标志,就知道这是一个同步方法。JVM会在方法开始前自动获取锁,方法结束后(或抛异常时)自动释放锁。

synchronized锁升级的过程
Plaintext
无锁 → 偏向锁 → 轻量级锁 → 重量级锁(单向升级,不可降级)

偏向锁(Biased Locking):

  • 场景:只有一个线程访问同步块
  • 实现:在 Mark Word 中记录线程 ID
  • 后续该线程进入同步块时,只需比较线程 ID,无需 CAS
  • 当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁

轻量级锁(Lightweight Lock):

  • 场景:多个线程交替执行同步块(无竞争)
  • 实现:在栈帧中创建 Lock Record,CAS 将 Mark Word 复制到 Lock Record
  • 退出时 CAS 恢复 Mark Word
  • CAS 失败(有竞争)→ 升级为重量级锁

重量级锁(Heavyweight Lock):

  • 场景:多个线程同时竞争
  • 实现:依赖操作系统 Mutex Lock
  • 未获取锁的线程阻塞(用户态→内核态切换,开销大)
  • 底层是 ObjectMonitor:
    • _EntryList:等待获取锁的线程队列
    • _WaitSet:调用 wait() 后等待的线程
    • _owner:当前持有锁的线程
锁优化

锁消除: JIT编译器检测到同步块中的对象不会逃逸到其他线程,自动去除锁。

锁粗化:连续对同一对象加锁解锁,合并为一次更大范围的锁。

synchronized和reentrantlock区别

都是Java中提供的可重入锁:

  • 用法不同:synchronized是关键字,可以直接用来修饰普通方法、静态方法和代码块;ReentrantLock通过显示调用lock()获取锁、unlock()释放锁来使用,通常配合try-finally确保锁一定能释放。
  • 获取锁和释放锁方式不同:synchronized会自动加锁和释放锁,当进入synchronized修饰的代码块之后会自动加锁,当离开synchronized的代码段之后会自动释放锁。而ReentrantLock需要手动加锁和释放锁。
  • 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

AQS

AQS全称为AbstractQueuedSynchronizer,是Java中的一个抽象类。AQS是一个用于构建锁、同步器、协作工具类的工具类(框架)。

AQS核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

CAS

CAS是一种乐观锁技术,Compare And Swap,先比较后交换,其包含三个操作数——内存位置v,预期原值A和新值B。在进行并发修改时,会先比较v和A取出的值是否相等,如果相等,则会把值替换为B,否则不做任何操作,在多个线程尝试用CAS去更新同一个变量时,只有一个线程可以更改,其他线程都失败。整个操作是原子的。

ABA问题

线程1读到值A,线程2将A→B→A,线程1 CAS 发现仍是 A 就成功了,但值已经被改过。

解决方法:加版本号,乐观锁在每次执行数据修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

循环时间过长问题

CAS操作如果长时间不成功,会不断进行重试,可能会导致线程长时间处于忙等状态,导致CPU长时间做无效操作。

ThreadLocal

原理

每个Thread持有一个ThreadLocalMap,key是ThreadLocal对象(弱引用),value是存储的值。

Android中的应用:

  • Looper.myLooper():每个线程的Looper存储在ThreadLocal中
  • Choregrapher:每个线程的编舞者实例

线程池ThreadPoolExecutor

七大参数
Java
public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数(即使空闲也不回收,除非设置 allowCoreThreadTimeOut)
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 非核心线程空闲存活时间
    TimeUnit unit,         // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂
    RejectedExecutionHandler handler    // 拒绝策略
)
execute流程
Plaintext
提交任务


workerCount < corePoolSize ?
  ├─ 是 → 创建核心线程执行任务

  ▼ 否
workQueue.offer(task) 成功?
  ├─ 是 → 任务入队等待

  ▼ 否(队列满)
workerCount < maximumPoolSize ?
  ├─ 是 → 创建非核心线程执行任务

  ▼ 否
执行拒绝策略
四种拒绝策略
策略行为
AbortPolicy(默认)抛出 RejectedExecutionException
CallerRunsPolicy由提交任务的线程直接执行
DiscardPolicy静默丢弃任务
DiscardOldestPolicy丢弃队列中最老的任务,重新提交当前任务

1.为什么volatile不是一种锁?

因为volatile不提供互斥性,多个线程可以同时访问变量;没有获取锁和释放锁的过程;不能保持复合操作的原子性(比如i++);

主要提供可见性有序性

2.synchronized和ReentrantLocl的区别

都是Java中提供的可重入锁:

  • 用法不同:synchronized是关键字,可以直接用来修饰普通方法、静态方法和代码块;ReentrantLock通过显示调用lock()获取锁、unlock()释放锁来使用,通常配合try-finally确保锁一定能释放。
  • 获取锁和释放锁方式不同:synchronized会自动加锁和释放锁,当进入synchronized修饰的代码块之后会自动加锁,当离开synchronized的代码段之后会自动释放锁。而ReentrantLock需要手动加锁和释放锁。
  • 锁类型不同:synchronized 属于非公平锁,而 ReentrantLock 既可以是公平锁也可以是非公平锁。
  • 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  • 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

追问:synchronized的锁升级过程

  1. 无锁->偏向锁:第一个线程进入,在Mark Word中记录线程ID,后续该线程进入只需比较ID
  2. 偏向锁->轻量级锁:第二个线程尝试获取锁,撤销偏向,两个线程CAS竞争
  3. 轻量级锁->重量级锁:CAS自旋

锁升级是单向的,不可降级。

3.volatile能保证线程安全吗?

volatile不保证原子性。i++是读-改-写三步操作,两个线程可能同时读到相同的值,各自+1后写回,丢失一次自增。

所以volatile不能完全保证线程安全,只适用于:

  • 状态标志位
  • DCL单例中防止指令重排
  • 一些多读的场景

DCL单例中为什么需要volatile?

new Singleton()分为三步:

  1. 分配内存
  2. 初始化
  3. 赋值引用。

没有volatile,2和3可能重排序,另一个线程看到非null的引用但对象还没初始化完成。volatile禁止这个重排序。

4.线程池的核心参数?execute的执行流程?

7 个核心参数:

  • corePoolSize:核心线程数,即使空闲也不回收
  • maximumPoolSize:最大线程数
  • keepAliveTime + unit:非核心线程空闲存活时间
  • workQueue:任务队列(LinkedBlockingQueue/ArrayBlockingQueue/SynchronousQueue)
  • threadFactory:线程工厂,可自定义线程名
  • handler:拒绝策略

execute流程:

  1. 当前线程数 < corePoolSize -> 创建核心线程执行任务
  2. 核心线程满 -> 任务放入 workQueue
  3. 队列满且线程数 < maximumPoolSize -> 创建非核心线程
  4. 队列满且线程数 = maximumPoolSize -> 执行拒绝策略

四种拒绝策略:

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:调用者线程执行任务(降级)
  • DiscardPolicy:静默丢弃
  • DiscardOldestPolicy:丢弃队列头部最老的任务

5.Java线程有哪些状态?状态之间怎么转换?

6种状态:

Plaintext
NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
  • New:创建但未start
  • RUNNABLE:调用start后,包括就绪和运行中
  • BLOCKED:等待获取synchronized锁
  • WAITING:调用wait(),join(),LockSupport.park(),无限等待
  • TIMED_WAITING:调用sleep(ms),wait(ms),join(ms),有超时的等待
  • TERMINATED:执行完毕或异常退出

追问:sleep 和 wait 的区别?

维度sleepwait
所属Thread 静态方法Object 实例方法
不释放锁释放锁
唤醒超时自动唤醒需要 notify/notifyAll
使用条件任何地方必须在 synchronized 中

6.Android为什么不能在子线程直接更新UI?

Android 的 View 体系不是线程安全的。UI 的测量、布局、绘制和事件分发都在主线程中按消息队列顺序执行,如果多个线程同时修改 View 状态,会产生竞态条件,导致显示错乱甚至崩溃。

追问:子线程一定不能创建handler吗

可以,但这个线程必须先调用Looper.prepare() 创建 Looper,再调用 Looper.loop() 开启消息循环。普通子线程默认没有 Looper。

7.线程和进程有什么区别?

  • 进程是系统分配资源的基本单位,有独立的内存空间。
  • 线程是CPU调度的基本单位,同一个进程内的线程共享进程资源。

Android中每个App通常运行在独立进程中,主线程也叫UI线程。网络请求、数据库读写、图片解码等耗时任务如果放在主线程,可能造成卡顿或 ANR。

8.什么情况下会出现线程安全问题?

线程安全问题通常出现在多个线程同时读写同一份可变数据时。例如多个线程同时执行 count++,它不是原子操作,实际包含读取、加一、写回三个步骤,可能导致结果丢失。

解决方式:

  • 使用 synchronizedLock 保护临界区。
  • 使用原子类,比如 AtomicInteger
  • 尽量使
  • 用不可变对象,减少共享可变状态。
  • 在 Android 中把 UI 状态收敛到主线程更新。

9.如何停止一个线程:

  • 异常法停止:线程调用interrupt()方法后,在线程的run方法中判断当前对象的interrupted()状态,如果是中断状态则抛出异常,达到中断线程的效果。
  • 在沉睡中停止:先将线程sleep,然后调用interrupt标记中断状态,interrupt会将阻塞状态的线程中断。会抛出中断异常,达到停止线程的效果
  • stop()暴力停止:线程调用stop()方法会被暴力停止,方法已弃用,该方法会有不好的后果:强制让线程停止有可能使一些请理性的工作得不到完成。
  • 使用return暴力停止线程:调用interrupt标记为中断状态后,在run方法中判断当前线程状态,如果为中断状态则return,能达到停止线程的效果。

10.为什么sleep不需要锁而wait需要锁

sleep()是Thread类的静态方法,只是单纯为了让当前线程暂停,不会释放锁;wait()是Object类的方法,用于线程间的协作通信,调用后会释放锁。

wait() 的核心动作是释放当前对象锁,如果调用前没有获得锁,逻辑上无法成立,会抛出异常。

11.blocking和waiting之间的区别

  • 触发条件:线程进入BLOCKED状态是因为试图获取一个对象的锁,但是该锁已经被另一个线程所获取。通常发生在尝试进入synchronized块或者方法时,如果锁已经被占用,将会阻塞直到锁可以使用。线程进入WAITING状态是因为它正在等待另一个线程执行另一个操作,例如Thread.join,Thread.wait或LockSupport.park()方法,这种状态下,线程将不会占用CPU资源,并且不会参与锁的竞争
  • 唤醒机制:当一个线程被阻塞等待锁时,一旦锁被释放,就会自动唤醒,并有机会获得锁,如果获得到了锁,那么就会从BLOCKED状态进入RUNNABLE状态。线程在WAITING状态时,必须要被显式唤醒。

12.线程间的通信方式

  • wait+notify:配合synchronized使用。
  • Lock+condition接口:比wait/notify更灵活(支持多个条件变量)。
  • volatile:此线程对变量的改变其他所有线程都能看见。
  • BlockingBueue:无需显式加锁,内部已经封装了wait/notify方法。

notify和notifyAll的区别

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。 区别在于:

  • notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断
  • notifyAIl:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

13.生产者-消费者模型

  • 模型定义
    • 生产者:生成数据/任务的线程,将结果放入共享缓冲区(如阻塞队列)。
    • 消费者:从缓冲区取出数据并处理的线程。
    • 缓冲区:平衡生产与消费速率的中间容器,解耦两者直接依赖。
    • 本质:通过缓冲区协调忙闲不均的资源调度,避免生产者空等或消费者饥饿。
  • 同步与互斥机制
    • 三种关系:
      • 生产者间互斥:防止同时写入导致数据冲突。
      • 消费者间互斥:防止同时读取同一数据。
      • 生产者与消费者间:同步(缓冲区空/满时等待通知)+ 互斥(共享缓冲区需加锁)。

如果没有缓冲区会怎么样

直接传递会导致强耦合:生产者需等待消费者就绪,造成线程阻塞;消费者需主动轮询数据,浪费CPU资源。缓冲区解耦两者,支持异步处理,避免系统卡顿。

Demo
java
import java.util.LinkedList;
import java.util.Queue;

/**
 * ProducerConsumerDemo
 */
public class ProducerConsumerDemo {

    public static void main(String[] args) {
        Buffer buffer = new Buffer(5);
        
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1;i <= 10;i++) {
                    buffer.put(i);
                    System.out.println("生产数据: " + i);
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1;i <= 10;i++) {
                    int data = buffer.take();
                    System.out.println("消费数据: " + i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}
/**
 * Buffer
 */ 
class Buffer {

    private final Queue<Integer> queue;
    private final int capacity;
    public Buffer(int capacity) {
        this.capacity = capacity;
        this.queue = new LinkedList<>();
    }
    
    // 生产者放入数据
    public synchronized void put(int data) throws InterruptedException {
        while (queue.size() == capacity) {
            System.out.println("缓冲区已满,等待中...");
            wait();
        }
        queue.offer(data);
        notifyAll();
    }

    // 消费者取出数据
    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {
            System.out.println("缓冲区为空,等待中...");
            wait();
        }
        int data = queue.poll();
        notifyAll();
        return data;
    }
    
}

14.多线程轮流打印奇偶数

java
package printoddeven;

/**
 * 多线程轮流打印奇偶数
 */
public class PrintOddEven {
    private static Object lock = new Object();
    private static int count = 1;
    private final static int MAX_COUNT = 100;

    public static void main(String[] args) {
        Runnable odd = () -> {
            synchronized(lock) {
                while (count <= MAX_COUNT) {
                    if (count % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + ": " + count);
                        count++;
                        lock.notifyAll();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        Runnable even = () -> {
            // 获取锁,同一时间奇偶线程只有一个线程能进入同步代码块执行
            synchronized(lock) {
                while (count <= MAX_COUNT) {
                    if (count % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + count);
                        count++;
                        // 唤醒所有在lock对象上等待的线程,但不会立即把锁交给偶数线程
                        lock.notifyAll();
                    } else {
                        try {
                            // 释放锁,进入等待
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        Thread oddThread = new Thread(odd, "oddThread");
        Thread evenThread = new Thread(even, "evenThread");
        oddThread.start();
        evenThread.start();
    }
}

Kotlin

1.kotlin的val和var的区别?val是不是线程安全的?

  • val:只读引用,赋值后不能重新赋值
  • var:可变引用,可以重新赋值

val不等于不可变:

Kotlin
val list = mutableListOf(1, 2, 3)
list.add(4) // ✅ 可以修改内容!val 只是引用不可变
// list = mutableListOf() // ❌ 不能重新赋值

val不是线程安全的:

  • 自定义 getter 每次可能返回不同值
  • 引用的对象内部状态可能被其他线程修改
  • 只有 val + 不可变对象(如 String、data class)才是线程安全的

2.apply,let,also,run的区别

函数对象引用返回值常见用途
applythis原对象初始化、配置对象
alsoit原对象日志、调试、附加操作
letitLambda 结果转换数据、空安全处理
runthisLambda 结果操作对象并计算结果

apply

使用this访问对象,返回对象本身,适合初始化或配置对象:

kotlin
val user = User().apply {
    name = "张三"
    age = 20
}

返回的是配置好后的User对象。

also

使用 it 访问对象,返回对象本身,适合执行日志、调试等附加操作:

kotlin
val user = User("张三", 20).also {
    println("创建用户:$it")
}

打印完成后,user 仍然是原来的 User 对象。

let

使用 it 访问对象,返回 Lambda 最后一行的结果,适合数据转换和非空处理:

kotlin
val length = "Kotlin".let {
    it.length
}

println(length) // 6

常用于安全调用:

kotlin
name?.let {
    println(it)
}

run

使用 this 访问对象,返回 Lambda 最后一行的结果,适合在对象上下文中完成一组操作并计算结果:

kotlin
val result = "Kotlin".run {
    println(length)
    uppercase()
}

println(result) // KOTLIN

3.Kotlin 的内联函数原理?什么时候用?

inline函数在编译时将函数体和lambda参数直接内联到调用处,不会创建function对象和额外的方法调用。

主要用途:

  1. 消除lambda开销:普通高阶函数每次调用会创建一个Function对象(匿名内部类),inline消除这个开销
  2. reified泛型:只有inline函数才能用reified,在运行时获取泛型类型
  3. 非局部返回:inline lambda中可以return直接从外层函数返回
Kotlin
inline fun <reified T> isType(value: Any): Boolean = value is T

// 调用处编译后直接展开为:value is String
isType<String>("hello")

noinline:标记不需要内联的 lambda(需要将 lambda 存储或传递给非内联函数时)。

crossinline:禁止 lambda 中的非局部返回(lambda 会在其他执行上下文中调用时)。

**使用场景:**函数体非常短,频繁调用时,调用开销要大于执行时间

4.kotlin vs java

kotlin和java有什么区别。

  • kotlin更加的简洁比如会自动识别变量类型,自动配置get/set函数等
  • 支持自动生成数据类
  • 有空安全

5.kotlin扩展函数怎么实现的?有什么限制?

扩展函数的语义是在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性。扩展函数编译后是一个静态方法,接收者对象作为第一个参数:

Kotlin
fun String.addStar() = "*$this*"
// 编译为:
public static String addStar(String $this) { return "*" + $this + "*"; }

限制:

  1. 静态分发:根据声明类型而非运行时类型调用,不支持多态
  2. 成员函数优先:如果类有同名同参数的成员函数,成员函数优先
  3. 不能访问 private/protected 成员:扩展函数本质是外部静态方法
  4. 可以被遮蔽:子类和父类定义同名扩展函数时,调用哪个取决于变量的声明类型

6.为什么很多高阶函数都使用inline函数

主要是为了减少Lambda带来的运行时开销(Lambda会被编译为匿名内部类对象)

减少Lambda对象创建

高阶函数会接收函数类型参数:

kotlin
fun calculate(block: () -> Int): Int {
    return block()
}

val result = calculate { 1 + 2 }

如果Lambda捕获了外部变量,还需要保存这些变量,所以如果高阶函数被频繁调用,还会带来额外的内存和垃圾回收开销。

减少函数调用开销

没有inline时,执行高阶函数可能包含两层调用:

  1. 调用高阶函数
  2. 通过函数对象调用lambda

而使用inline后,就会在调用处展开。

7.kotlin委托机制

可以理解为是将一个接口委托给一个类然后直接实现这个接口的方法,有点像是静态委托的感觉,代码更加的简洁,能够实现一些额外的操作,但是可读性较差。

by lazy三种模式

  • LazyThreadSafetyMode.SYNCHRONIZED(默认):双重检查锁,线程安全
  • LazyThreadSafetyMode.PUBLICATION:多线程可能同时初始化,但只有第一个完成的值被使用
  • LazyThreadSafetyMode.NONE:不加锁,单线程使用

委托原理:编译器生成一个 $$delegate 字段,属性的 get/set 转发给委托对象的 getValue/setValue

by lazy和lateinit的区别

都是延迟初始化的操作,by lazy是访问资源时看是否被初始化,已经初始化就直接使用,没有初始化就走一套委托流程开始初始化(初始化后不能重新赋值,只能作用于val);lateinit就是延迟初始化,在使用到时才会调用,如果没有就抛出异常(只能作用于var)

协程

挂起与恢复原理
CPS变换

编译器将suspend函数转换为带Continuation参数的普通函数:

  • 每个suspend函数编译为一个状态机
  • 每个挂起点对应一个label状态
  • Continuation保存了当前状态和局部变量
  • 返回COROUTINE_SUSPENDED表示真正挂起了
  • 恢复时调用continuation.resumeWith(result)
CoroutineContext

这是一个类似Map的结构,由多个Element组合:

Kotlin
val context = Job() + Dispatchers.IO + CoroutineName("myCoroutine")
// 等价于
val context = CoroutineContext(
    Job(),
    Dispatchers.IO,
    CoroutineName("myCoroutine")
)

常用的Element:

  • Job:控制协程生命周期(取消、等待完成)
  • CoroutineDispatcher:决定协程在哪个线程执行
  • CoroutineName:调试用的名称
  • CoroutineExceptionHandler:未捕获异常处理器
调度器
Kotlin
Dispatchers.Main      // Android 主线程(通过 Handler(Looper.getMainLooper()) 实现)
Dispatchers.IO        // IO 密集型,共享线程池,最大 max(64, CPU核心数) 个线程
Dispatchers.Default   // CPU 密集型,线程数 = CPU 核心数
Dispatchers.Unconfined // 不切换线程,在当前线程执行到第一个挂起点

IO和Default共享线程池:它们底层使用同一个线程池,但通过LimitingDispatcher限制并发数。IO允许更多并发(因为IO操作大部分时间在等待),Default限制为CPU核心数。

Job层级关系

Plaintext
父 Job
├── 子 Job A
│   ├── 孙 Job A1
│   └── 孙 Job A2
└── 子 Job B
  • 父Job取消->所有子Job递归取消
  • 子Job 失败 → 默认取消父 Job → 取消所有兄弟 Job
  • SupervisorJob:子 Job 失败不影响父和兄弟
异常传播

aunch vs async

Kotlin
// launch:异常立即向上传播
val job = scope.launch {
    throw RuntimeException("boom") // 异常传播到父协程
}

// async:异常在 await() 时抛出
val deferred = scope.async {
    throw RuntimeException("boom") // 暂时不传播
}
try {
    deferred.await() // 这里才抛出异常
} catch (e: Exception) {
    // 处理异常
}

CoroutineExceptionHandler

Kotlin
val handler = CoroutineExceptionHandler { _, exception ->
    println("捕获异常: ${exception.message}")
}

// 只在根协程(scope.launch)上设置才有效
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
    throw RuntimeException("boom") // 被 handler 捕获
}

// ❌ 在子协程上设置无效
scope.launch {
    launch(handler) { // handler 不会生效!
        throw RuntimeException("boom")
    }
}
Flow
冷流 vs 热流
Kotlin
// 冷流:每次 collect 都重新执行
val coldFlow = flow {
    println("开始发射")
    emit(1)
    emit(2)
    emit(3)
}
coldFlow.collect { println(it) } // 打印:开始发射 1 2 3
coldFlow.collect { println(it) } // 再次打印:开始发射 1 2 3

// SharedFlow(热流):多个收集者共享
val sharedFlow = MutableSharedFlow<Int>(
    replay = 1,           // 新收集者能收到最近 1 个值
    extraBufferCapacity = 64,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
)

// StateFlow(特殊的 SharedFlow):
// replay = 1,必须有初始值,自动 distinctUntilChanged
val stateFlow = MutableStateFlow(0)
stateFlow.value = 1 // 更新值
stateFlow.value = 1 // 相同值不会通知收集者
StateFlow vs LiveData
特性StateFlowLiveData
初始值必须有可以没有
空安全泛型约束可能为 null
生命周期感知需要 repeatOnLifecycle自动感知
相同值自动去重每次都通知
背压支持不支持
平台依赖纯 KotlinAndroid 专属
Kotlin
// 在 Activity/Fragment 中安全收集 StateFlow
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUI(state)
        }
    }
}
背压处理
Kotlin
flow {
    for (i in 1..100) {
        emit(i)
        delay(10) // 生产快
    }
}
.buffer(64)          // 缓冲区,生产者不等消费者
// .conflate()       // 只保留最新值,丢弃中间值
// .collectLatest {} // 新值来时取消上一次处理
.collect { value ->
    delay(100) // 消费慢
    println(value)
}
Channel
Kotlin
// Channel:协程间通信(CSP 模型)
val channel = Channel<Int>(capacity = Channel.BUFFERED)

// 生产者
launch {
    for (i in 1..5) {
        channel.send(i) // 缓冲区满时挂起
    }
    channel.close()
}

// 消费者
launch {
    for (value in channel) { // 自动在 close 后结束
        println(value)
    }
}

缓冲策略:

  • Channel.RENDEZVOUS(0):无缓冲,send 和 receive 必须同时就绪
  • Channel.BUFFERED(64):默认缓冲大小
  • Channel.CONFLATED:只保留最新值
  • Channel.UNLIMITED:无限缓冲(注意内存)

Channel vs Flow

  • Channel 是热的,创建即开始
  • Flow 是冷的,collect 才开始
  • Channel 适合一对一通信
  • Flow 适合一对多的数据流
1.Kotlin协程的挂起原理是什么?

Kotlin 协程的挂起本质是 CPS(Continuation Passing Style)变换 + 状态机。

编译器将 suspend 函数转换为带 Continuation 参数的普通函数,函数体被改写为一个 when(label) 状态机。每个挂起点对应一个 label 状态。

执行到挂起点时:

  1. 保存当前局部变量到 Continuation 对象
  2. 设置下一个 label
  3. 调用挂起函数,如果返回 COROUTINE_SUSPENDED 则函数返回(挂起)
  4. 异步操作完成后,调用 continuation.resumeWith(result) 恢复执行
  5. 恢复时从上次的 label 继续执行,从 Continuation 中恢复局部变量

关键点:协程挂起不会阻塞线程,线程可以去执行其他协程。恢复时由调度器决定在哪个线程继续执行。

追问:suspend 关键字的作用?

suspend 只是一个标记,告诉编译器这个函数可能挂起。编译器会为它添加 Continuation 参数并生成状态机代码。suspend 函数只能在协程或其他 suspend 函数中调用。

追问:协程比线程轻量在哪?

  • 协程是用户态调度,不需要内核态切换(线程切换需要系统调用,开销约 1-10μs)
  • 协程挂起时只保存少量状态(Continuation 对象),线程需要保存完整的栈帧(默认 1MB 栈空间)
  • 一个线程上可以运行成千上万个协程
2.launch和async的区别?异常处理有什么不同?

launch 返回 Job,用于不需要返回值的场景。async 返回 Deferred(继承 Job),可以通过 await() 获取返回值。

异常处理的关键区别:

  • launch:异常立即向上传播到父协程,如果没有 CoroutineExceptionHandler,会导致整个协程作用域取消
  • async:异常被封装在 Deferred 中,调用 await() 时才抛出
Kotlin
// launch 异常立即传播
val scope = CoroutineScope(Job())
scope.launch {
    throw RuntimeException("boom") // 立即传播,scope 被取消
}

// async 异常延迟到 await
val deferred = scope.async {
    throw RuntimeException("boom") // 暂时不传播
}
deferred.await() // 这里才抛出异常

追问:CoroutineExceptionHandler 在哪里生效?

只在根协程(顶层 launch)中生效,子协程的异常会向上传播到父协程,不会被子协程的 handler 捕获。

Kotlin
val handler = CoroutineExceptionHandler { _, e -> println("Caught: $e") }

// ✅ 生效:handler 在根协程
CoroutineScope(Job() + handler).launch {
    throw RuntimeException()
}

// ❌ 不生效:handler 在子协程
CoroutineScope(Job()).launch {
    launch(handler) { // handler 在子协程,不生效
        throw RuntimeException()
    }
}

追问:supervisorScope 和 coroutineScope 的区别?

  • coroutineScope:任何子协程失败,所有子协程都被取消
  • supervisorScope:子协程失败不影响兄弟协程

SupervisorJob 的原理是重写了 childCancelled() 返回 false,不将子协程的失败传播给父协程。

3.StateFlow和SharedFlow的区别?
  • StateFlow:必须有初始值,replay = 1,自动 distinctUntilChanged(相同值不重复发射)
  • SharedFlow:无需初始值,可配置 replay 和 buffer,不去重

StateFlow 适合表示状态(UI State),SharedFlow 适合表示事件(一次性事件如 Toast、导航)。

4.Kotlin协程取消是怎么工作的?

协程取消是协作式的,不是强制中断。调用 job.cancel() 后:

  1. Job 的状态变为 Cancelling
  2. 在下一个挂起点(suspend 函数调用处)检查取消状态
  3. 如果已取消,抛出 CancellationException
  4. CancellationException 不会被当作异常传播(正常取消流程)

Android

Handler

自己总结的Handler原理:Handler

1.Handler整个消息机制运行

  1. 首先系统调用ActivityThread中的main函数初始化主线程的Looper,并把Looper保存在ThreadLocal中(保证一个线程只有一个Looper)
  2. 然后调用Looper.loop()进入一个死循环,一直循环调用MessageQueue.next()获取下一个Message
  3. MessageQueue.next()中有一个死循环用于获取队列的下一条Message,在获取不到消息或者消息执行时间未到时会调用native层的NativeMessageQueue的休眠方法
  4. 之后会调用native层的Looper,使用Linux的epoll机制进行休眠(epoll机制会监听一个休眠的文件描述符)
  5. 如果有消息添加进来,就会唤醒native层的Looper,同时唤醒java层的MessageQueue
  6. 最后java层的MessageQueue的next就会读取到新加入的Message并返回给Looper
  7. Looper 拿到 Message 后会根据 Message 中保存的 target 变量(Handler)回调 Handler 的 dispatchMessage() 方法

2.Handler为什么会发生内存泄漏

  1. 在使用非静态内部类时就容易导致内存泄漏,原因在于非静态内部类默认持有外部类的引用
  2. 泄漏时机在于发送了一条延时Message,但宿主比如Activity已经回调了onDestroy(),此时却因为Message还没有执行发生内存泄漏
  3. 发送泄漏的引用链为:Looper -> MessageQueue -> Message -> Handler(非静态内部类) -> Activity

3.如何解决Handler内存泄漏

  1. 使用静态内部类+弱引用Activity解决。在Message被回调时判断当前Activity的弱引用是否为null,不为null时才执行
  2. 在Activity被摧毁时调用handler.removeCallbacksAndMessages(null)把相关联的Message清空

4.MessageQueue怎么保证线程安全

在读取Message队列是会用synchronized枷锁保证线程安全

5. MessageQueue为什么不会造成死锁

MessageQueue在读取下一条Message时,如果没有Message可以执行,就会退出synchronized,然后调用native层的方法进行休眠,等待Message入队唤醒

6.MessageQueue在没有消息时会休眠,那么是什么促使Message的产生

虽然主线程会进行休眠,但是可以通过其他线程对主线程的 Looper 发送 Message。比如:AMS 通过 Binder 线程向主线程发送作用于 Activity 启动的 Message,然后触发一系列的 Message

7. Looper死循环为什么不会导致应用卡死

  1. Android中一切皆是消息,包括触摸事件,视图的绘制、显示和刷新等等都是消息,正是因为Looper死循环一直读取消息才使应用一直在运行
  2. 而应用卡死一般是因为单个Message执行时间过长,导致后面的Message一直不被执行而产生的卡顿

8.Looper死循环会特别消耗CPU资源吗?

  1. 并不会消耗太多的资源。在 MessageQueue 当前没有消息要执行时会进行休眠,调用了 native 层 NativeMessageQueue,NativeMessageQueue 调用了 native 层的 Looper,该 Looper 会使用 Linux 的 epoll 机制进行休眠,休眠时会让出 CPU 调度
  2. epoll会监听了一个专门用于休眠操作的文件描述符,并向底层签写了一个当前文件描述符可读的回调,在 java 层 MessageQueue 入队时会对休眠的文件描述符进行写入,然后唤醒之前的休眠操作

9.同步屏障是什么?它的原理是怎么实现的?

  1. 同步屏障是一种特殊的消息,可以使 Handler 优先执行异步消息。在 ViewRootImpl.scheduleTraversals() 方法中发送了一个同步屏障,并紧接着发送了一个用于测量布局绘制的异步消息。
  2. 在 MessageQueue.next() 读取下一条消息时,会先判断队列头是否是同步屏障,如果是的话,就会跳过同步消息,只寻找异步消息,最后返回给 Looper
  3. 通过同步屏障和异步消息来保证了 View 的绘制会优先执行,避免了消息过多而出现掉帧的情况

10.Handler、Looper、MessageQueue、线程的关系

  • 一个线程只会有一个Looper对象,所以线程和Looper是一一对应的。
  • MessageQueue对象是在new Looper的时候创建的,所以Looper和MessageQueue是一一对应的。
  • Handler的作用只是将消息加到MessageQueue中,并后续取出消息后,根据消息的target字段分发给当初的那个handler,所以Handler对于Looper是可以多对一的,也就是多个Hanlder对象都可以用同一个线程、同一个Looper、同一个MessageQueue。但是一个Handler只能对应一个Looper/MessageQueue(创建时绑定)

总结:Looper、MessageQueue、线程是一一对应关系,而他们与Handler是可以一对多的。

11. 主线程为什么不用初始化Looper

在Android程序入口ActivityThread的main方法中初始化了主线程Looper:

java
// 初始化主线程Looper
 Looper.prepareMainLooper();
 ...
 // 新建一个ActivityThread对象
 ActivityThread thread = new ActivityThread();
 thread.attach(false, startSeq);
 // 获取ActivityThread的Handler,也是他的内部类H
 if (sMainThreadHandler == null) {
 sMainThreadHandler = thread.getHandler();
 }
 ...
 Looper.loop();
 // 如果loop方法结束则抛出异常,程序结束
 throw new RuntimeException("Main thread loop unexpectedly exited");
}

ActivityThread并不是一个线程,而是主线程操作的管理者

12.Handler是如何切换线程的

Handler在创建时会绑定一个Looper,每个Looper都有一个对应的线程,在子线程中把消息投放到Handler对应的MessageQueue,然后Looper负责取出并消费消息,调用dispatchMessage方法就运行在其所在线程了。

13.post(Runnable)和sendMessage有什么区别

post和sendMessage的区别就在于,post方法给Message设置了一个callback回调,然后在消息处理方法dispatchMessage中:

java
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
    message.callback.run();
}

所以post(Runnable) 与 sendMessage的区别就在于后续消息的处理方式,是交给msg.callback还是 Handler.Callback或者Handler.handleMessage。

14.IdleHandler是什么,有什么使用场景

IdleHandler 是 MessageQueue 的一个内部接口,可以用于在 Loop 线程处于空闲状态的时候执行一些优先级不高的操作,通过 MessageQueue 的 addIdleHandler 方法来提交要执行的操作。MessageQueue 在执行 next() 方法时,如果发现当前队列是空的或者队头消息需要延迟处理的话,那么就会去尝试遍历 mIdleHandlers来依次执行 IdleHandler。

常见的使用场景:启动优化,我们一般会把一些事件(比如界面view的绘制、赋值)放到onCreate方法或者onResume方法中。但是这两个方法其实都是在界面绘制之前调用的,也就是说一定程度上这两个方法的耗时会影响到启动时间。所以我们可以把一些操作放到IdleHandler中,也就是界面绘制完成之后才去调用,这样就能减少启动时间了。

以及:ActivityThread 就向主线程 MessageQueue 添加了一个 GcIdler,用于在主线程空闲时尝试去执行 GC 操作

15.HandlerThread是什么

java
public class HandlerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }

HandlerThread是一个封装了Looper的Thread类,为了让我们在子线程里面更方便的使用Handler。

Jetpack

1.ViewModel的作用以及使用ViewModel的主要优势

ViewModel的作用在于解决Android应用中活动和碎片(Fragment)的生命周期问题。它允许数据在屏幕旋转等配置更改时存活,并确保数据在不同组件之间共享而不丢失。主要优势包括:

  • 生命周期感知: ViewModel能够感知与UI相关的生命周期变化,确保数据存活时间比短暂的UI组件更长。
  • 数据共享: 通过ViewModel,可以在不同的UI组件之间共享和管理数据,避免重复加载或丢失数据。
  • 状态保存: ViewModel在配置变更时保持其状态,例如屏幕旋转,避免重新加载数据和执行耗时操作。

2.详细说明LiveData和ViewModel的工作原理,并讨论在实际项目中如何解决常见的生命周期问题。

LiveData是一种可观察的数据持有者,ViewModel用于存储和管理与用户界面相关的数据。深入理解包括:

  • LiveData的粘性事件: 了解postValuesetValue的区别,以及如何避免LiveData的粘性事件在特定场景中引发的问题。
  • ViewModel的存活周期: 使用ViewModel正确处理配置变化,保证数据在屏幕旋转等情况下不丢失。
  • LiveData和View绑定: 结合DataBinding,实现LiveData与View之间的绑定,确保数据的实时更新。

3.LiveData和RxJava有什么区别

  1. 设计目标:

LiveData专为 Android 设计,主要用于在 UI 层观察数据变化;与 Android 生命周期紧密集成,确保数据更新只在 UI 处于活跃状态时触发,避免内存泄漏;简单易用,适合处理 UI 相关的数据流。

RxJava是一个通用的响应式编程库,适用于任何 Java 项目。提供了强大的数据流操作符(如 mapfilterflatMap 等),适合处理复杂的异步任务和数据流。需要手动管理生命周期,否则可能导致内存泄漏。

  1. 生命周期感知LiveData自动感知生命周期,确保观察者只在STARTEDRESUMED状态下接收数据更新,无需手动处理生命周期RXJava不直接支持生命周期感知,需要借助 RxLifecycleAutoDispose 等第三方库来管理生命周期。如果不处理生命周期,可能导致内存泄漏。
  2. 数据流处理能力

LiveData功能简单,主要用于观察单一数据源的变化;不支持复杂的数据流操作(如线程切换、数据转换等)

RxJava提供了丰富的操作符(如 mapfilterflatMapzip 等),可以轻松处理复杂的数据流。支持线程切换(如 subscribeOnobserveOn),方便处理异步任务。

  1. 线程管理

LiveData默认在主线程中触发数据刷新,如果需要在后台线程更新数据,可以使用 postValue 方法。

RxJava提供了强大的线程调度功能,可以通过 subscribeOnobserveOn 灵活切换线程。

4.如何避免LiveData粘性事件

LiveData 的“粘性事件”是指当一个新的观察者开始观察 LiveData 时,它会立即接收到 LiveData 中最后一条数据,即使这条数据是在观察者注册之前更新的。这种行为在某些场景下可能会导致问题(例如重复处理数据或触发不必要的 UI 更新)。以下是避免 LiveData 粘性事件的几种方法:

  1. 使用SingleLiveEvent

SingleLiveEvent 是一个自定义的 LiveData 实现,确保每个事件只被观察一次,避免粘性事件。

java
// ViewModel
private SingleLiveEvent<String> singleLiveEvent = new SingleLiveEvent<>();

public SingleLiveEvent<String> getSingleLiveEvent() {
    return singleLiveEvent;
}

// 触发事件
singleLiveEvent.setValue("Hello, SingleLiveEvent!");

// 观察事件
viewModel.getSingleLiveEvent().observe(this, value -> {
    // 处理事件
});
  1. 使用Event包装类

通过将数据包装在一个 Event 类中,可以标记数据是否已经被处理,从而避免重复触发。

java
private MutableLiveData<Event<String>> liveData = new MutableLiveData<>();

public MutableLiveData<Event<String>> getLiveData() {
    return liveData;
}

// 触发事件
liveData.setValue(new Event<>("Hello, Event!"));

// 观察事件
viewModel.getLiveData().observe(this, event -> {
    String content = event.getContentIfNotHandled();
    if (content != null) {
        // 处理事件
    }
});
  1. 使用MediatorLiveData

MediatorLiveData 可以监听其他 LiveData 的变化,并在需要时过滤掉粘性事件。

java
private MutableLiveData<String> sourceLiveData = new MutableLiveData<>();
private MediatorLiveData<String> mediatorLiveData = new MediatorLiveData<>();

public MediatorLiveData<String> getMediatorLiveData() {
    return mediatorLiveData;
}

public void triggerEvent(String value) {
    sourceLiveData.setValue(value);
}

{
    mediatorLiveData.addSource(sourceLiveData, value -> {
        mediatorLiveData.setValue(value);
        mediatorLiveData.removeSource(sourceLiveData); // 移除源以避免粘性事件
    });
}

// 观察事件
viewModel.getMediatorLiveData().observe(this, value -> {
    // 处理事件
});
  1. SharedFlow(推荐)

SharedFlow天然支持非粘性事件:

kotlin
private val _events = MutableSharedFlow<String>()
val events: SharedFlow<String> = _events

fun triggerEvent(value: String) {
    viewModelScope.launch {
        _events.emit(value)
    }
}

// 观察事件
viewModel.events.collect { value ->
    // 处理事件
}

flow有热流和冷流,热流分为SharedFlowStateFlow,StateFlow和LiveData功能差不多

为什么LiveData要设计成粘性事件?

LiveData 的粘性事件,或者说数据倒灌,是正常现象,也是它的设计使然。

因为 LiveData 本质上是一个“状态持有者”,不是事件总线。

它会保存一份最新数据,当新的 Observer 注册时,如果当前有最新值,就会立刻回调给它。

这么设计的核心目的,是为了更好地配合 Android 的生命周期,尤其是 Activity/Fragment 因旋转屏幕、页面重建后,能够立即恢复最新 UI 状态。

比如页面重建后,UI 需要马上拿到当前的用户信息、列表数据、加载状态,这时候粘性就是合理且必要的。

但是如果把 LiveData 用来发送 一次性事件,比如 Toast、导航、弹窗,这种粘性就会带来问题,比如页面重建后事件被重复消费,这就是所谓的数据倒灌。

LiveData 适合做状态分发,不适合直接做一次性事件分发。

5.ViewModel如何穿透生命周期

ViewModel 是 Android 架构组件的一部分,它的设计目的是为了在配置更改(如屏幕旋转)时保留数据,从而避免重新加载数据或重新初始化 UI 状态。然而,ViewModel 的生命周期是与 ActivityFragment 绑定的,当 ActivityFragment 被销毁时(例如用户按返回键退出),ViewModel 也会被清除。

如果需要在 ActivityFragment 销毁后仍然保留数据,可以通过以下方法实现“穿透生命周期”的效果。

  1. 使用SavedStateHandle
  2. 使用持久化存储
  3. 使用单例模式(应用进程被杀死后数据丢失,且可能导致内存泄漏)

6.ViewModel与onSaveInstanceState有什么本质区别

  • 作用范围ViewModel适用于大数据对象(如图片列表),而Bundle适合存储轻量级状态(如页面滚动位置)
  • 生命周期ViewModel存活到组件完全销毁(如Activity调用finish()),而Bundle仅在临时重建时有效
  • 性能对比ViewModel通过内存缓存避免序列化开销,性能提升约30倍

7.如何实现自定义生命周期的ViewModel

  • 1). 继承ViewModel并重写onCleared()
  • 2). 通过ViewModelProvider.Factory注入自定义作用域
  • 3). 使用LifecycleObserver监听特定生命周期事件

8.ViewModel三大应用场景

  1. 跨屏幕旋转HolderFragment + ViewModelStore机制
  2. 跨组件通信ViewModelStoreOwner的多级作用域控制
  3. 跨进程恢复SavedStateHandleBundle的深度集成

9.对比LiveData和Observable,分析它们在Android应用中的应用场景,以及在何种情况下选择使用哪种。

LiveData和Observable都是用于实现响应式编程的工具,但有一些关键区别:

  • 生命周期感知: LiveData是生命周期感知的,它会在观察者(通常是UI组件)的生命周期内自动启动和停止。这使得在处理UI数据时更加安全,避免了潜在的内存泄漏。
  • 背压处理: Observable在RxJava中通常使用背压策略来处理数据流,而LiveData则通过生命周期感知来实现反应式响应,避免了背压问题。

根据实际需求,选择使用LiveData还是Observable取决于应用的具体场景。对于需要与UI组件绑定的数据,以及对生命周期敏感的场景,LiveData是更好的选择。而在需要更强大的操作符和背压处理的情况下,可以考虑使用Observable。

10.为什么ViewModel能在旋转屏幕时保持状态

ViewModel 之所以能在旋转屏幕时保持状态,是因为它不是保存在 Activity 实例里,而是保存在 Activity/Fragment 持有的 ViewModelStore 中。

当屏幕旋转发生配置变更时,旧的 Activity 虽然会销毁重建,但系统会保留 NonConfigurationInstance,新的 Activity 会拿回之前的 ViewModelStore,因此通过 ViewModelProvider 获取到的仍然是同一个 ViewModel 实例,所以里面的 UI 状态不会丢失。

Glide

1.Glide原理

Glide 是 Android 中一个非常成熟的图片加载框架,核心能力包括:图片异步加载、内存缓存、磁盘缓存、图片解码、复用、生命周期管理以及图片变换。

它整体采用了一个比较经典的流程:

  1. with() 绑定生命周期,避免页面销毁后还继续回调导致泄漏;
  2. load() 接收图片模型,比如 URL、File、Uri、资源 id;
  3. Glide 会先根据请求生成一个唯一 Key;
  4. 然后按顺序去查缓存:活动资源 ActiveResources → 内存缓存 LruResourceCache → 磁盘缓存 DiskLruCache
  5. 如果缓存都没命中,就通过网络或本地数据源加载原始数据;
  6. 再经过解码、采样压缩、格式转换、Transform 变换;
  7. 最终回到主线程,把结果设置到 ImageView。

Glide 为了性能做了很多优化,比如:

  • 多级缓存
  • BitmapPool / ArrayPool 对象复用
  • 按需缩放,避免大图直接加载导致 OOM
  • 请求合并,防止同一资源重复加载
  • 生命周期感知,自动暂停/恢复请求

所以一句话总结:
Glide 本质上是一个围绕“资源复用 + 多级缓存 + 生命周期管理”构建的高性能图片加载框架。

2.Glide缓存机制如何工作的

Glide缓存机制包括内存缓存和磁盘缓存,以提高图片加载的性能和减少网络请求。

  • 内存缓存:Glide使用LruResourceCache来实现内存缓存,会根据最近最少使用(LRU)算法来管理内存中的图片资源。当内存不足时,会自动清除最久未使用的图片资源。
  • 磁盘缓存:Glide使用DiskLruCache来实现磁盘缓存,他会将图片资源存储在设备存储中。磁盘缓存还可以避免重复的网络请求,并且即使应用被关闭,图片资源仍然可以被保留
  • 缓存键值:Glide通过图片的URL和图片的尺寸等信息生成一个唯一的键值,用于在缓存中查找和存储图片资源
  • 缓存大小:Glide会根据设备可用内存动态计算内存缓存的大小,通常限制在可用内存的一定比例内。

3.如何自定义Glide的缓存行为

通过DiskCacheStrategy枚举,可以自定义Glide的缓存行为:

  1. DiskCacheStrategy.ALL: 缓存原始图片和转换后的图片到磁盘缓存
  2. DiskCacheStrategy.NONE: 不使用磁盘缓存
  3. DiskCacheStrategy.RESOURCE: 只缓存转换后的图片到磁盘缓存
  4. DiskCacheStrategy.DATA: 只缓存原始图片到磁盘缓存

四大组件

1.Application类的作用是什么?

Application 类的设计初衷就是保存全局状态,并执行应用范围内的初始化操作。开发者通常会继承这个类,用以设置依赖项、配置第三方库,以及管理那些需要在多个 ActivityService 之间持续存在的资源。

默认情况下,每个 Android 应用都会使用系统提供的 Application 基类实现,除非你在 AndroidManifest.xml 文件中明确指定了一个自定义的子类。

2.如果一个 Activity 类未在 AndroidManifest 中注册会发生什么?

如果 Activity 没有在 AndroidManifest 中注册,系统不会把它识别为合法组件。通过 Intent 启动它时会失败,并抛出 ActivityNotFoundException,常见提示是 have you declared this activity in your AndroidManifest.xml?

因为 Activity 的创建和生命周期由系统管理,系统需要通过 Manifest 获取组件信息,比如类名、启动模式、主题、exported、intent-filter 等。普通类可以不注册,但 Activity 作为四大组件之一必须声明。

Activity

生命周期
  1. onCreate():一个Activity被创建时,这是第一个被调用的方法。在这里你可以初始化 Activity、设置 UI 组件,并恢复任何保存的实例状态。除非 Activity 被销毁并重新创建,否则在整个 Activity 生命周期中只会调用一次。
  2. onStart():Activity对用户变得可见,但尚不可交互。该方法在 onCreate() 之后和 onResume() 之前被调用。
  3. onRestart():如果 Activity 停止后又被重新启动(例如,用户重新回到该 Activity),此方法会在 onStart 之前被调用。
  4. onResume()Activity 处于前台,用户可以与之互动。这是恢复暂停的UI更新、动画或输入监听器的地方。
  5. onPause():当 Activity 被另一个 Activity 部分遮挡时(例如,弹出对话框),会调用此方法。Activity 仍然可见但不在焦点上。通常用于暂停动画、传感器更新或保存数据等操作。
  6. onStop()Activity 对用户不再可见(例如,另一个 Activity 进入前台)。你应该释放 Activity 停止期间不需要的资源,比如后台任务或大型对象。
  7. onDestroy():在 Activity 被完全销毁并从内存中移除之前调用。这是释放所有剩余资源的最终清理方法。

异常生命周期:

  1. 配置变更(如屏幕旋转) Activity会被销毁并重建,触发onSaveInstanceState()保存临时数据,重建后通过onRestoreInstanceState()恢复。
  2. 内存不足回收 后台Activity可能会被系统回收,需要通过onSaveInstanceState()来保存数据。

Tips:

  • 避免在onCreate()中执行耗时操作,若初始化耗时(如加载数据库),应该进行异步操作或者ViewModel延迟加载。
  • onDestory的不稳定性:不能依赖onDestroy()释放资源(系统可能直接终止进程),应在onStop()中释放
1.onPause()onStop()有什么区别

onPause()onStop()都表示Avtivity不再处于前台状态,区别在于:

  • onPause()** 表示 Activity 失去焦点,但可能仍然可见**
  • onStop()** 表示 Activity 已经完全不可见。**
2.你能描述一下依次启动 Activity A、然后 Activity B,最后返回 Activity A 时发生的生命周期变化吗?
  • 初次启动Activity A

    Activity A:onCreate() -> onStart() -> onResume()。这是首次创建时的标准流程。此时用户正在与Activity A交互。

  • 从Activity A跳转到Activity B

    Activity A: onPause()。暂停UI更新,释放与可见性相关的轻量资源

    Activity B:onCreate() -> onStart() -> onResume()。创建并进入前台,获得用户焦点。

    Activity A: onStop()。如果 Activity B 完全覆盖了 Activity A(比如是全屏页面),系统会接着调用 onStop()。这一步不是总发生,但常见于标准跳转。

  • Activity B 返回 Activity A
    Activity B: onPause()Activity A: onRestart()onStart()onResume()。重新激活,回到前台继续交互。

    Activity B: onStop()onDestroy()。若系统决定回收它(比如通过返回键退出),就会走完销毁流程。

Activity 切换过程中,前台的 Activity 总是先调用 onPause(),再退到后台。

新启动的 ActivityonCreate() 开始,一路走到 onResume(),接管用户焦点。

而当你返回之前的 Activity,它不会重新 onCreate(),而是通过 onRestart() 恢复状态。

启动模式
启动模式行为特征生命周期回调典型应用场景
standard默认模式,每次启动都会创建实例,允许多个实例存在于同一任务线完整生命周期(onCreate)常规页面,如新闻列表页、普通表单页
singleTop仅目标在栈顶的时候调用复用,(负责新建)onNewIntent防止重复点击页面,如如支付按钮跳转结果页、推送通知快速点击
singleTask在任务栈中查找匹配实例,若存在清除上方所有实例并复用onNewIntent应用主页(要求全局唯一)、深层链接入口(如从浏览器跳转回App主流程)
singleInstance独占一个新任务栈,且该栈中只能存在该ActivityonNewIntent独立功能模块(如系统相机、第三方登录页)
3.屏幕旋转一定会使activity重建吗

不会,可以通过更改manifest的属性进行更改,并且在activity中增加onConfigurationChanged,但是需要手动配置横屏后的组件ui,不方便,不如使用vm

4.如何避免Activity内存泄漏
  1. 静态变量持有Activity引用

单例类或静态变量直接或间接持有Activity的Context

kotlin
object Singleton {var context: Context? = null // 错误:可能持有Activity的引用}

解决方案

  • 把 Activity Context用Application Context来代替
  • 如果实在要引用的话就用弱引用(WeakReference)
kotlin
class Singleton {
    private var activityRef: WeakReference<Activity>? = null 
       fun setActivity(activity: Activity) {
               activityRef = WeakReference(activity)
       }
 }
  1. 非静态内部类+匿名类

比如Handler、Runnable等内部类隐式持有Activity引用。

解决:静态内部类+弱引用,在onDestroy()移除回调

  1. 未正确注销监听器或者回调

场景:注册广播,事件总线,监听器没有及时注销

kotlin
override fun onCreate(savedInstanceState: Bundle?){
    super onCreate(savedInstanceState)
    LocalBroadcastManager.getInstance(this).registerReceiver(receiver,intentFilter)
}

解决方案:在onDestory()中进行反注册

kotlin
override fun onDestory(){
    LocalBroadcastManager.getInstance(this).unregisterReceiver(reciver)
    super onDestory
}
  1. 异步任务没有随Activity销毁终止(AsyncTask、Rxjava、Coroutine等)

解决方法:使用lifecycleScope

  1. 资源未释放

onDestroy()释放资源即可。

5.App的启动流程

简要概括:

1)Launch->AMS->Zygote->ActivityThread

2)ActivityThread->AMS->(Activity/Application)

启动流程:

  1. 点击桌面图标,Launcher进程采用Binder IPC向system_server进程发送startActivity请求
  2. 当system_server收到请求后会向zygote进程发送创建进程的请求
  3. zygote进程fork出新的进程来,即App进程
  4. App进程向Binder IPC向system_server进程发起attachApplication请求
  5. system_server进程收到请求后,进行一系列准备工作后,再次通过Binder IPC向App进程发送scheduleLauchActivty请求
  6. App进程的binder线程(Application Thread)在收到请求后,通过Handler向主线程发送LAUNCH_ACTIVITY消息
  7. 主线程收到消息后,通过发射机制创建目标Activity,并回调activity的onCreate等方法

Fragment

生命周期
  1. onAttach():当 Fragment 被关联到它的父 Activity 时,这是第一个被调用的回调。此时 Fragment 已经绑定,可以和 Activity 的上下文交互了。
  2. onCreate():用于初始化 Fragment。此时 Fragment 已创建,但 UI 还没生成。通常在这里初始化核心组件或恢复之前保存的状态。
  3. onCreateView():当 Fragment 的 UI 第一次被绘制时调用。你需要在这个方法里返回布局的根视图,也就是用 LayoutInflater 加载布局的地方。
  4. onViewStateRestored():在 Fragment 的视图层级创建完成、且保存的状态已恢复到视图后调用。
  5. onViewCreated():在 Fragment 的视图创建完成后触发。常用来设置 UI 组件,以及处理用户交互所需的逻辑。
  6. onStart()Fragment 对用户可见了。这和 ActivityonStart() 类似——此时它已经活跃,但还没处于前台。
  7. onResume()Fragment 完全激活并运行在前台,用户可以与之交互。当 Fragment 的 UI 可见且可操作时,会调用这个方法。
  8. onPause():当 Fragment 不再处于前台但仍然可见时调用。它即将失去焦点,你应该暂停那些不该在后台继续运行的任务。
  9. onStop()Fragment 不再可见。这里要停止那些不需要在屏幕外继续运行的操作。
  10. onSaveInstanceState():在 Fragment 被销毁前调用,用来保存与 UI 相关的状态数据,以便后续能恢复。
  11. onDestroyView():当 Fragment 的视图层级被移除时调用。你应该清理与视图相关的资源,比如清空适配器、置空引用,防止内存泄漏。
  12. onDestroy()Fragment 本身正在被销毁。所有资源都应该在此时释放,但它仍与父 Activity 关联着。
  13. onDetach()Fragment 从父 Activity 上解绑,不再与其关联。这是最后一个回调,标志着 Fragment 生命周期结束。

Fragment 中的 viewLifecycleOwner:

在 Android 开发中,一个 Fragment 有自己的生命周期,它和宿主 Activity 绑定。但 Fragment 也有自己独立的生命周期。这个区别在管理像 LiveData 这样的生命周期感知数据源时特别重要。

viewLifecycleOwner 实例就是用来帮你搞清楚这层关系、避免出问题的。

viewLifecycleOwner 是一个与 Fragment 视图关联的 LifecycleOwner。它代表的是 View 的生命周期——从 onCreateView() 被调用开始,到 onDestroyView() 被调用结束。

这意味着你可以把 UI 相关的数据或资源,绑定到 View 的生命周期上,而不是整个 Fragment。这样能更精准地控制资源,防止内存泄漏这类问题。

Fragment 的视图生命周期比 Fragment 自身短。

如果你用 Fragment 的生命周期(比如 lifecycleOwner)来观察数据或处理事件,一旦 View 被销毁了,你可能还在试图访问它,结果就是崩溃或异常行为。

而使用 viewLifecycleOwner,就能确保所有观察者或生命周期感知组件都只在 View 存在时生效。当 View 被销毁时,更新会自动停止

lifecycleOwner** 和 viewLifecycleOwner 的区别:**

  • lifecycleOwnerFragment 的生命周期):代表整个 Fragment 的生命周期,持续时间更长,与宿主 Activity 绑定。
  • viewLifecycleOwnerFragmentView 生命周期):代表 FragmentView 的生命周期,从 onCreateView 开始,到 onDestroyView 结束。
1.onCreateView()onDestroyView() 的作用是什么?

onCreateView()** 创建 View,onDestroyView() 清理 View;Fragment 可能还活着,但它的 View 可以被反复创建和销毁。**

2.在一个Activity中启动Fragment,他们的生命周期顺序

Activity.onCreate()->Fragment.onAttach()->Fragment.onCreate()->Activity.onStart()->Fragment.onCreateView()->Fragment.onViewCreated->Fragment.onStart()->Activity.onResume()->Fragment.onResume()

3.Activity生命周期与fragment生命周期的区别
  • onAttach(Context context)当Fragment与Activity关联的时候调用(可通过getActivity()来获得宿主Activity)常用来获得Activity的依赖(如回调)
  • onCreateView()在创建Fragment视图层次结构时调用,通过LayoutInflater解析布局。
  • onViewCreated()在onCreateView()创建完视图后调用,用来进行视图的初始化,比如findViewById或者RV适配器的设置
  • onActivityCreated()(已经废弃)在AndroidX中,此方法已经被废弃,可以选择在onCraeteView(),onViewCreated()中监听生命周期
  • onDestoryVew()当Fragment被销毁时调用,如Fragment替换或者移除,用于清除与视图相关的资源
  • onDetach()当Fragment与Activity解除关联的时候调用,释放对Activity的引用,避免内存泄露

Service

Service的启动方式

启动型Service

当某个应用组件调用startService()时,Service就会被启动。它会在后台持续运行,直到自己调用 stopSelf() 或被外部通过 stopService() 明确停止。

使用场景:

  • 播放背景音乐。
  • 上传或下载文件。
kotlin
class MyService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 在后台执行长时间任务
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

绑定型Service

其他组件通过 bindService() 来绑定 Service,并与它建立连接。只要还有组件绑定着它,Service 就会保持活跃;当所有客户端都断开连接后,它会自动停止。

使用场景:

  • 从服务器获取数据(指长时间需要通过 Service 来获取数据)。
  • 管理后台蓝牙连接。
kotlin
class BoundService : Service() {
    private val binder = LocalBinder()

    inner class LocalBinder : Binder() {
        fun getService(): BoundService = this@BoundService
    }

    override fun onBind(intent: Intent?): IBinder = binder
}

前台Service

前台 Service 是一种特殊的 Service,它在运行时会显示一个持续的通知。这种服务适用于需要用户持续感知的任务,比如播放音乐、导航或位置追踪。

kotlin
class ForegroundService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = createNotification()
        startForeground(1, notification)
        return START_STICKY
    }

    private fun createNotification(): Notification {
        return NotificationCompat.Builder(this, "channel_id")
            .setContentTitle("Foreground Service")
            .setContentText("Running...")
            .setSmallIcon(R.drawable.ic_notification)
            .build()
    }
}
Service生命周期

一个 Service 可以以两种模式运行:

  1. 通过 startService() 启动:会一直运行,直到被明确调用 stopSelf()stopService() 停止。
  2. 通过 bindService() 与一个或多个组件绑定:只要还有组件绑定着它,它就会持续存在。

它的生命周期通过 onCreate()onStartCommand()onBind()onDestroy() 等方法进行管理。

startService()

  1. onCreate():当 Service 首次创建时调用。用于初始化 Service 所需的资源。
  2. onStartCommand():当通过 startService() 启动 Service 时触发。该方法负责执行实际任务,并通过返回值(如 START_STICKYSTART_NOT_STICKY)决定服务被杀死后是否重启。
  3. onDestroy():当使用 stopSelf()stopService() 停止 Service 时调用。用于执行清理操作,比如释放资源或停止线程。
kotlin
class SimpleStartedService : Service() {
    override fun onCreate() {
        super.onCreate()
        // 初始化资源
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 执行长时间任务
        return START_STICKY // 如果服务被杀死,系统会尝试重启它
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理资源
    }

    override fun onBind(intent: Intent?): IBinder? = null // 启动型服务不使用绑定
}

bindService()

  1. onCreate():与启动型服务类似,当 Service 创建时调用,用于初始化所需资源。
  2. onBind():当某个组件通过 bindService() 绑定到该服务时触发。此方法返回一个接口(IBinder),供客户端与服务通信。
  3. onUnbind():当最后一个客户端从服务解绑时调用。这里是清理与绑定客户端相关资源的地方。
  4. onDestroy():当服务被终止时调用,负责释放资源并停止正在进行的操作。
kotlin
class SimpleBoundService : Service() {
    private val binder = LocalBinder()

    override fun onCreate() {
        super.onCreate()
        // 初始化资源
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder // 返回绑定服务的接口
    }

    override fun onUnbind(intent: Intent?): Boolean {
        // 当没有客户端绑定时进行清理
        return super.onUnbind(intent)
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理资源
    }

    inner class LocalBinder : Binder() {
        fun getService(): SimpleBoundService = this@SimpleBoundService
    }
}

启动型与绑定型 Service 生命周期的关键区别在于:

  1. Started Service:独立于任何组件运行,直到被明确停止才会结束。
  2. Bound Service:仅在至少有一个客户端绑定时才存在,一旦所有客户端解绑,服务就会终止。
1.Android 中,started servicebound service 有什么区别?分别在什么场景下使用?

Started Service 是通过 startService()startForegroundService() 启动的,主要用于执行一个独立任务。它启动后即使调用者销毁也可以继续运行,需要通过 stopSelf()stopService() 主动停止,典型场景是音乐播放、文件上传下载、持续定位等。

Bound Service 是通过 bindService() 绑定的,主要用于让组件和 Service 建立连接并调用 Service 提供的方法。它的生命周期依赖绑定者,如果只是绑定服务,当所有客户端解绑后 Service 会销毁。典型场景是 Activity 控制播放器、获取服务状态、组件间通信或 AIDL 跨进程通信。

一个 Service 也可以既被启动又被绑定,例如音乐播放器中,startService() 保证后台播放,bindService() 让界面可以控制播放。

BroadcastReceiver

广播

ContentProvider

Okhttp

自己总结的Okhttp源码:Okhttp源码解析

当你在代码中发起一个网络请求时,Okhttp 先通过 Builder 模式构建OkhttpClientRequest,然后将Request封装成一个Call对象,代表一个准备好执行的实际请求。Call里面有同步请求和异步请求两种,在将Call交给Dispatcher时会决定何时,在哪个线程执行。Dispatcher 主要负责管理请求的执行状态和线程池。如果是同步请求,会直接将请求加入同步队列(runningSyncCalls),在当前线程阻塞执行,执行完毕后移除。异步请求则会构建一个 AsyncCall,实际上就是一个 Runnable 对象,在 Dispatcher 中调度使用线程池中的线程去执行 Runnable,再构建相关责任链获取 Response 对象。

使用了建造者模式配置了默认的参数,使用了拦截器链来进行了网络请求的逻辑处理部分。实际上是一个责任链模式,由五个拦截器构成:RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor

  1. RetryAndFollowUpInterceptor(重试与重定向拦截器)

整个责任链的第一个环节。负责在网络请求失败时进行重试,或者在服务器返回 3xx 状态码时自动处理重定向。

  1. BridgeInterceptor(桥接拦截器)

连接应用层和网络层。把你写的业务代码“翻译”成网络能识别的请求(例如自动添加 HostKeep-AliveGzip 等请求头),并把服务器端返回的响应“翻译”回应用层代码。

  1. CacheIterceptor(缓存拦截器)

处理 HTTP 缓存。在请求前检查是否有可用缓存,有则直接返回(不走网络);在响应后判断是否需要将结果写入本地缓存。

  1. ConnectInterceptor(连接拦截器)

负责寻找或建立底层的 TCP/TLS 连接。这里用到了极其重要的连接池(ConnectionPool)机制,通过复用连接大幅减少三次握手和 TLS 握手带来的高昂性能开销。

  1. CallServerInterceptor(请求服务拦截器)

责任链的最后一环。真正的网络 IO 发生在这里,利用 Okio 库向服务器写入请求数据,并读取服务器返回的响应数据。

1.Okhttp 拦截链怎么工作的?应用拦截器和网络拦截器的区别?

OkHttp 使用责任链模式,请求依次经过:应用拦截器 → RetryAndFollowUpInterceptor → BridgeInterceptor → CacheInterceptor → ConnectInterceptor → 网络拦截器 → CallServerInterceptor。
每个拦截器调用 chain.proceed(request) 传递给下一个,可以在调用前修改 Request,调用后修改 Response。
应用拦截器 vs 网络拦截器:

  • 应用拦截器:最先执行,不关心重定向和重试,只调用一次。适合添加公共参数、日志
  • 网络拦截器:在 ConnectInterceptor 之后,能看到真实的网络请求(包括重定向后的)。适合网络层监控

追问:连接池的作用?

ConnectionPool 复用 TCP 连接,避免每次请求都三次握手。默认最多 5 个空闲连接,空闲 5 分钟回收。HTTP/2 还能在同一连接上多路复用。

2.网络请求中的 Token 过期怎么处理?

通过拦截器实现自动刷新 Token:

kotlin
class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
    override fun intercept(chain: Chain): Response {
        // 1. 添加 Token
        val request = chain.request().newBuilder()
            .header("Authorization", "Bearer ${tokenManager.accessToken}")
            .build()

        val response = chain.proceed(request)

        // 2. 401 则刷新 Token
        if (response.code == 401) {
            synchronized(this) {
                // 双重检查:可能其他线程已经刷新了
                val currentToken = tokenManager.accessToken
                if (currentToken == request.header("Authorization")?.removePrefix("Bearer ")) {
                    // Token 没变,说明还没刷新,执行刷新
                    val newToken = tokenManager.refreshTokenSync()
                        ?: return response // 刷新失败,跳转登录

                    // 用新 Token 重试
                    val newRequest = request.newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                    response.close()
                    return chain.proceed(newRequest)
                } else {
                    // Token 已被其他线程刷新,用新 Token 重试
                    val newRequest = request.newBuilder()
                        .header("Authorization", "Bearer ${tokenManager.accessToken}")
                        .build()
                    response.close()
                    return chain.proceed(newRequest)
                }
            }
        }
        return response
    }
}

INFO

synchronized 防止多个请求同时刷新 Token;双重检查避免重复刷新。

3.OkHttp Dispatcher 是什么?它如何控制并发?

Dispatcher 负责异步请求调度。同步请求直接由调用线程执行,异步请求会先进入 Dispatcher,由线程池执行。Dispatcher 内部维护 running/ready 队列,并通过最大并发数和单 Host 最大并发数限制请求。

核心点:

  • maxRequests 控制全局最大并发请求数
  • maxRequestsPerHost 控制同一 Host 的最大并发请求数
  • 超出限制的异步请求会进入等待队列
  • 正在运行的请求结束后,会尝试提升等待队列里的请求

Retrofit

工作流程

首先在 create()方法时创建客户端并生成一个动态代理的实现类,然后在内部调用接口的方法,会调用 InvocationHandler 的 invoke 方法,Retrofit 会利用反射,提取该方法上的所有注解(URL、请求方法、参数等)。Retofit 会把解析的结果封装成一个 ServiceMethod 对象,并缓存在内存中。然后拿着 ServiceMethod 里解析好的请求信息(URL、Header、Body 等),Retrofit 会在底层生成一个 OkhttpCall 对象。默认 Retrofit 会返回一个标准的 Call<T>对象,如果用了 RxJava 等,CallAdapter 会把底层的 OkhttpCall 包装转换成所需要的类型。包装好的 Call 最终被调用执行(enqueue()),任务移交给 Okhttp 执行,然后通过 Converter 来解析返回的响应数据,并回调 CallBack 接口。

Retrofit 注解生命周期

首先我们执行 retrofit.create()方法时会对我们的注解进行解析,解析完后会把我们的解析结果进行缓存,在后续方法调用用到后会直接调用缓存进行处理,而不是在后续用到时再次解析,最后网络请求的时候就失去作用了。

1.Retrofit 的原理?动态代理怎么工作的?

Retrofit 的 create() 方法使用 Proxy.newProxyInstance 创建接口的动态代理对象。调用接口方法时,实际执行的是 InvocationHandler:

  1. 解析方法上的注解(@GET/@POST/@Path/@Query/@Body 等),构建 ServiceMethod
  2. ServiceMethod 缓存在 ConcurrentHashMap 中,避免重复解析
  3. 根据注解信息构建 OkHttp Request
  4. CallAdapter 将 OkHttp Call 适配为目标返回类型(suspend 函数、Flow、RxJava Observable)
  5. Converter 将 ResponseBody 转换为数据类(Gson/kotlinx.serialization)

追问:suspend 函数是怎么支持的?

Retrofit 监测到方法最后一个参数是 Continuation 后,会使用 SuspendForBody 适配器。内部将 OkHttp 的异步 enqueue 回调转换为协程的 suspendCancellableCoroutine。

2.Retrofit 是如何把接口方法变成网络请求的?

Retrofit 通过动态代理为接口创建实现类。调用接口方法时,会进入 InvocationHandler,Retrofit 根据方法上的注解解析出 Http method、path、query、header 等信息,构建 ServiceMethod 和 RequestFactory,最终创建 OkhttpCall 执行请求。

3. Retrofit 注解 @Path、@Query、@Body、@Field 有什么区别?

@Path 替换 URL 路径中的占位符,适合 /users/{id}

@Query 拼接 URL 查询参数,适合 GET 请求筛选条件。

@Body 把对象作为请求体,常用于 JSON POST/PUT。

@Field 用于表单请求,需要配合 @FormUrlEncoded,最终按表单字段提交。

RxJava

采用了响应式编程(消费生产者模型)来完成构建。

流程

kotlin
fun main() {
    Observable.create<Int> { 
        it.onNext(1)
    }
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe()
}

订阅发生时,RxJava 会从下游向上逐层包装 Observer,形成一条链。事件真正发射时,再从上游向下游逐层传递。每个操作符本质上就是包装上游和下游,在onNextonErroronComplete中插入自己的逻辑。

INFO

subscribeOn 影响订阅发生的线程,observerOn 影响后续观察者回调的线程。

subscribeOn 和 observeOn

subscribeOn()

observeOn()

kotlin
Observable.create(ObservableOnSubscribe<Int> { // A8
      it.onNext(1) // B1
      it.onComplete()
    })
      .doOnSubscribe { // A7
        println("00doOnSubscribe在线程" + Thread.currentThread().name + "中") 
      }
      .subscribeOn(Schedulers.newThread()) // A6
      .map { // B2
        println("map1在线程" + Thread.currentThread().name + "中")
      }
      .doOnSubscribe { // A5
        println("11doOnSubscribe在线程" + Thread.currentThread().name + "中")
      }
      .subscribeOn(Schedulers.io()) // A4
      .observeOn(Schedulers.io()) // B3
      .map { // B4
        println("map2在线程" + Thread.currentThread().name + "中")
      }
      .doOnSubscribe { // A3
        println("22doOnSubscribe在线程" + Thread.currentThread().name + "中")
      }
      .subscribeOn(Schedulers.newThread()) // A2
      // 这里是开启整个流,因为 Observable 创建的是冷流
      .subscribe { // A1
        println("onNext在线程" + Thread.currentThread().name + "中") // B5
      }

结果是这样的:

plain
22doOnSubscribe在线程RxNewThreadScheduler-1中
11doOnSubscribe在线程RxIoScheduler-3中
00doOnSubscribe在线程RxNewThreadScheduler-2中
map1在线程RxNewThreadScheduler-2中
map2在线程RxIoScheduler-2中
onNext在线程RxIoScheduler-2中

然后我们可以得出下面的总结:

INFO

下面提到的“操作”包括产生事件、用操作符操作事件以及最终的通过 subscriber 消费事件;

  • 只有第一个 subscribeOn()起作用(多个 subsrcibeOn()无意义,无意义只针对于数据发送源)
  • observeOn() 可以使用多次,每个 observeOn() 将导致一次线程切换(),这次切换开始于这次 observeOn() 的下一个操作;
  • 不论是 subscribeOn() 还是 observeOn(),每次线程切换如果不受到下一个 observeOn() 的干预,线程将不再改变,不会自动切换到其他线程。

merge 和 mergeDelayError

同时运行多个 Observable,支持并发,比如:

kotlin
Observable.merge(
  Observable.create { // 1
    it.onNext(1)
    it.onError(RuntimeException())
  },
  Observable.just(2), // 2
  Observable.just(3)  // 3
)

这种情况下如果使用 merge 会因为 1 的 onError 而强制中断 2 和 3

使用 mergeDelayError 可以使 error 延迟到所有 Observable 都结束再发送,如果有多个 error,他会主动整合所有的 error 到一个 error 对象中

INFO

注意:对于 merge 在出错时会中断后面的 Observable,但他们可能已经被创建出来了,就会导致下游不会被回调,比如在网络请求时,请求已经发出,但却因为前面有个 Observable 抛错,导致后面的网络请求在请求成功的情况下却不会在发给下游,这样会出现很严重的问题

1.RxJava 的核心原理

RxJava 本质上是观察者模式+装饰器链。上游通过 Observable/Flowable 发射事件,下游 Observer/Subscriber 接收事件,中间通过一系列操作符把事件转换、过滤、组合。

订阅发生时,RxJava 会从下游向上游逐层包装 Observer,形成一条链。事件真正发射时,再从上游向下游逐层传递。每个操作符本质上就是包装上游和下游,在 onNext、onError、onComplete 中插入自己的处理逻辑。

2.subscribeOn 和 observeOn 有什么区别?

subscribeOn() 控制上游订阅和事件生产的线程,通常只有第一次调用生效;observeOn() 控制它后面那段链路的回调线程,可以调用多次。

flatMap、concatMap、seitchMap 的区别?

flatMap 会把一个事件转换成一个新的 Observable,并把多个 Observable 合并发射,不保证顺序,适合并发请求。

concatMap 会按上游顺序串行执行,保证结果顺序,适合有顺序要求的任务。

switchMap 只保留最新一次转换出来的 Observable,旧任务会被取消或忽略,适合搜索框联想、输入变化触发请求等场景。

View 绘制

Window/WindowManager/DecorView

Android 中,屏幕上的一个界面,内部嵌套的层级结构是这样的:

Activity -> Window -> DecorView -> ViewGroup/View

plain
Activity
  └── PhoneWindow(Window 的唯一实现)
        └── DecorView(FrameLayout 子类,Window 的根 View)
              ├── TitleBar / ActionBar
              └── ContentView(android.R.id.content)
                    └── 你的布局(setContentView 设置的)
  • Activity: 本身并不负责视图的控制或显示,它是应用组件,负责生命周期管理和业务逻辑。
  • Window:抽象类,实现类通常是 PhoneWindow。负责管理界面的显示和事件的路由(比如把点击事件传递给内部的 View)。
  • DecorView:这是 PhoneWindow 内部包含的一个顶级视图,叫做 DecorView(装饰视图),继承自 FrameLayout,是整个视图树的根节点。包含一个垂直的 LinearLayout,上面部分是 TitleBar(系统的状态栏或应用的标题栏),下面部分是 ContentView(内容区域)。

整体流程

绘制从 ViewRootImpl.performTraversals()开始,执行三大流程:

plain
performTraversals()
├── performMeasure()   → View.measure() → onMeasure()
├── performLayout()    → View.layout() → onLayout()
└── performDraw()      → View.draw() → onDraw()

触发时机:requestLayout()(触发 measure + layout)、invalidate()(触发 draw)。

performMeasure() 方法会调用 DecorView 的 measure() 方法,在 measure() 方法中又会调用自己的onMeasure() 方法。

View 的绘制流程总体为 3 步:测量(measure)、布局(layout)、绘制(draw)。

  • 测量阶段。measure 方法会被父 View 调用,在measure 方法中做一些优化和准备工作后会调用 onMeasure 方法进行实际的自我测量。onMeasure方法在View和ViewGroup做的事情是不一样的:
    • View:View 中的 onMeasure 方法会计算自己的尺寸并通过 setMeasureDimension 保存。
    • ViewGroup:ViewGroup 中的 onMeasure 方法会调用所有子 view的measure 方法进行自我测量并保存。然后通过子View的尺寸和位置计算出自己的尺寸并保存。
  • 布局阶段。layout 方法会被父View调用,layout 方法会保存父 View 传进来的尺寸和位置,并调用 onLayout 进行实际的内部布局。onLayout 在 View 和 ViewGroup 中做的事情也是不一样的:
    • View。 因为 View 是没有子 View 的,所以View的onLayout里面什么都不做。
    • ViewGroup。 ViewGroup 中的 onLayout 方法会调用所有子 View 的 layout 方法,把尺寸和位置传给他们,让他们完成自我的内部布局。
  • 绘制阶段。 draw 方法会做一些调度工作,然后会调用 onDraw 方法进行 View 的自我绘制。draw 方法的调度流程大致是这样的:
    • 绘制背景。 对应 drawBackground(Canvas)方法。
    • 绘制主体。 对应 onDraw(Canvas)方法。
    • 绘制子View。 对应 dispatchDraw(Canvas)方法。
    • 绘制滑动相关和前景。 对应 onDrawForeground(Canvas)

requestLayout vs invalidate

  • requestLayout:标记PFLAG_FORCE_LAYOUT,向上传递到 ViewRootImpl,强制触发 measure + layout。如果布局发生变化,View.layout() 内部会调用 invalidate() 标记脏区域,连带触发 draw。
  • invalidate():标记脏区域,只触发 draw

ConstraintLayout 性能优势

ConstraintLayout 只需要一次 measure + layout 就能完成复杂布局,而嵌套的 LinearLayout/RelativeLayout 需要多次。

原理:ConstraintLayout 使用 Cassowary 线性约束求解算法,将所有约束转化为线性方程组一次性求解,避免了多层嵌套导致的指数级 measure 调用。

plain
嵌套 LinearLayout(3层):measure 调用 2^3 = 8 次
ConstraintLayout(扁平):measure 调用 2 次(水平+垂直各一次)

1.View 的绘制流程?measure、layout、draw 各自做什么?

绘制从 ViewRootImpl.performTraversals() 开始,依次执行三大流程:

  1. measure:**确定 View 的大小。ViewGroup 先测量子 View,再根据子 View 大小确定自身大小。**核心是 MeasureSpec(高2位模式+低30位大小),三种模式:EXACTLY(精确值)、AT_MOST(最大值,wrap_content)、UNSPECIFIED(无限制)。
  2. layout:**确定 View 的位置。ViewGroup 在 onLayout 中调用每个子 View 的 layout(l, t, r, b) **确定其四个顶点坐标。
  3. draw:绘制 View 内容。顺序是:背景 → onDraw(自身内容)→ dispatchDraw(子 View)→ 前景。

追问:自定义 View 中 wrap_content 不处理会怎样?

等同于 match_parent。因为父 EXACTLY + 子 wrap_content 得到 AT_MOST + 父大小,如果 onMeasure 不处理 AT_MOST 直接用 specSize,就是父容器大小。正确做法是在 AT_MOST 模式下计算内容所需大小,取 min(内容大小, specSize)。

追问:requestLayout 和 invalidate 的区别?

requestLayout 强制触发 measure + layout。如果布局发生变化,layout 内部会调用 invalidate 标记脏区域,连带触发 draw。invalidate 只触发 draw,用于内容变化(如颜色、文字)。invalidate 不会重新测量和布局。

2.getWidth() 和 getMeasuredWidth() 的区别?

  • getMeasuredWidth():在 measure 阶段确定,是 View 期望的宽度
  • getWidth():在 layout 阶段确定,是 View 实际的宽度(right - left

通常两者相等,但在 onLayout 中可以让实际宽度与测量宽度不同。

追问:在 onCreate 中获取 View 的宽高为什么是 0?怎么解决?

因为 onCreate 时 View 还没有经过 measure 和 layout 阶段。可以使用 view.post { }解决。

3.View.post(Runnable)原理

如果 View 已经 attach 到 Window(有 ViewRootImpl),post 直接通过 Handler 发送到主线程消息队列。

如果 View 还没有 attach(如 onCreate 中),Runnable 会被暂存到 View.mRunQueue 中,等到 dispatchAttachedToWindow 时再通过 Handler 发送。

4.Activity在onCreate生命周期时调用方法setContentView,此后与系统发生的交互有哪些

第一阶段是 view 树的创建与绑定,会先解析 xml,然后构建 view 层并绑定到 window,第二层级是与windowManager进行交互,第三个就是进行view的绘制流程。

事件分发

三个核心方法

java
// 分发事件
public boolean dispatchTouchEvent(MotionEvent ev)
// 拦截事件(只有 ViewGroup 有)
public boolean onInterceptTouchEvent(MotionEvent ev)
// 消费事件
public boolean onTouchEvent(MotionEvent ev)

分发流程

plain
Activity.dispatchTouchEvent
  → PhoneWindow.superDispatchTouchEvent
    → DecorView.dispatchTouchEvent
      → ViewGroup.dispatchTouchEvent
        → ViewGroup.onInterceptTouchEvent  // 是否拦截?

        ├── 不拦截 → 遍历子 View
        │   → child.dispatchTouchEvent
        │     → child.onTouchEvent         // 子 View 消费?
        │     ├── true → 事件被消费,结束
        │     └── false → 回传给父 ViewGroup.onTouchEvent

        └── 拦截 → ViewGroup.onTouchEvent  // 自己处理

关键规则

  1. DOWN 事件决定后续事件的接收者:如果某个 View 在 DOWN 时返回 true(消费),后续的 MOVE/UP 都会直接发给它
  2. 一旦拦截,后续事件不再询问 onInterceptTouchEvent:ViewGroup 拦截后,后续事件直接交给自己的 onTouchEvent
  3. 子 View 可以请求父 ViewGroup 不拦截
plain
parent.requestDisallowInterceptTouchEvent(true);
// 设置 FLAG_DISALLOW_INTERCEPT,父 ViewGroup 的 onInterceptTouchEvent 不会被调用
// 但 DOWN 事件会重置这个标志
  1. onTouchListener 优先于 onTouchEvent
java
// dispatchTouchEvent 中的逻辑
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
    return true; // OnTouchListener 消费了,不调用 onTouchEvent
}
return onTouchEvent(event); // 否则调用 onTouchEvent

1.事件分发机制?从手指触摸到 View 响应的完整流程?

事件从硬件到应用的完整链路:

  1. 触摸屏产生中断 -> InputManagerService -> InputDispatcher
  2. 通过 Socket 发送到应用进程的 InputChannel
  3. ViewRootImpl 的 WindowInputEventReceiver 接收
  4. 进入 View 树的分发流程

View 树分发流程:

  • Activity.dispatchTouchEvent → PhoneWindow → DecorView → ViewGroup.dispatchTouchEvent
  • ViewGroup 先调用 onInterceptTouchEvent 判断是否拦截
  • 不拦截则倒序遍历子 View(后添加的先收到),调用 child.dispatchTouchEvent
  • 子 View 的 dispatchTouchEvent 中:先调 OnTouchListener.onTouch,返回 false 才调 onTouchEvent
  • onTouchEvent 的 ACTION_UP 中触发 OnClickListener.onClick

关键规则:

  • DOWN 事件确定事件接收者(mFirstTouchTarget),后续 MOVE/UP 直接发给它
  • 子 View 可以调用 requestDisallowInterceptTouchEvent(true) 阻止父 ViewGroup 拦截
  • 如果所有子 View 都不消费,事件回传给 ViewGroup 自己的 onTouchEvent

追问:onTouchListener、onTouchEvent、onClickListener 的优先级?

onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。onTouch 返回 true 则 onTouchEvent 不调用,onClick 也不会触发。

2.滑动冲突的解决

外部拦截法和内部拦截法。

外部拦截法:在父 ViewGroup 的 onInterceptTouchEvent 中判断:

内部拦截法: 子 View 通过 requestDisallowInterceptTouchEvent 控制。

3.设置了 onTouchListener 后 onClickListener 不会被调用的原因

onClickListener在 onTouchEvent 中调用,如果 OnTouchListener.onTouch() 返回了true,onTouchEvent()就不会再被调用了。

4.如果在 ViewGroup 中拦截了 Action_Down 事件会怎样

会导致 mFirstTouchTarget 无法被赋值,后续一系列的 MOVE/UP 事件都无法调用 onInterceptTouchEvent 方法,intercepted 恒为 true,

mFirstTouchTarget 恒为 null,后续所有事件只能交给自身处理,无法分发给子 View。

RecyclerView

1.RecyclerView的绘制流程?

  1. 数据源变更时通过Adapter进行通知。
  2. LayoutManager测量和布局:RV会通知LayoutManager进行测量和布局,确定每个ItemView的位置。
  3. ViewHolder创建和绑定:RV调用Adapter的onCreateViewHolder方法创建ViewHolder,并通过onBindViewHolder将数据绑定到ViewHolder上。
  4. 设置ItemDecoration并绘制分割线等装饰。

2.介绍下四级缓存?

  • 第一级缓存:Scrap(屏幕内缓存)。这里面包含了两个列表,第一个是mAttachedScrap:存放当前屏幕上的ViewHolder,在 onLayoutChildren 时,LayoutManager 会先把所有子 View detach 并放入 mAttachedScrap,重新布局时再从中取回。第二个是mChangedScrap:存放数据已变换的的ViewHolder(调用notifyItemChanged),添加itemView时快速从里面取出完成局部刷新。

INFO

关键区别:

  • mAttachedScrap 复用时不需要重新绑定(数据没变)
  • mChangedScrap 是为了动画存在的,最终会被回收到 Pool
  • 第二级缓存:mCachedViews(刚离开屏幕的缓存),默认大小为2。特点:
    • 按position精确匹配
    • 命中后直接复用,不调用onBindViewHolder(数据完全一致)
    • 容量满时,最老的ViewHolder被移到RecycledViewPool
    • 适用场景:用户来回小幅滑动时,刚滑出去的item马上滑回来
  • 第三级缓存:mViewCacheExtension(自定义缓存)。开发者自定义的缓存层,插在CachedViews和RecycledViewPool之间。
  • 第四级缓存:RecycledViewPool ViewHolder(回收池),有一个RecyclerViewPool,每种ViewHolder类型魔人保存五个,支持多RV复用,按ViewType进行匹配(不关心position),命中后需要调用 onBindViewHolder 重新绑定数据,ViewHolder被放入之前也会清除所有绑定状态。

3.ViewHolder的回收流程?

ViewHolder的回收发生在滑动的过程中,item移出屏幕时:

Plaintext
item 滑出屏幕
  → LayoutManager 调用 removeAndRecycleView()
    → Recycler.recycleViewHolderInternal(holder)
      → 先尝试放入 mCachedViews
        → 如果 mCachedViews 满了
          → 把最老的(index 0)移到 RecycledViewPool
          → 当前 holder 放入 mCachedViews 末尾
        → 如果 mCachedViews 没满
          → 直接放入 mCachedViews
      → 如果不能放入 mCachedViews(如被标记 INVALID)
        → 直接放入 RecycledViewPool

4.LayoutManager工作原理

LayoutManager负责:

  • 决定 item 的摆放位置(线性、网格、瀑布流)
  • 决定何时回收不可见的 item
  • 处理滚动
  • 支持预取(Prefetch)

LayoutManager本身不保存缓存,在需要View时,向Recycler拿,发现View滚出屏幕不可见了,就主动把它塞回给Recycler

5.比ListView的优点

  1. 灵活性:Rv可以定制更加符合自身需求的Adapter以及LayoutManger等,允许开发者灵活设计列表与外观。
  2. 性能优化:itemviewholder的引入,使得rv更加方便的处理更加复杂的数据集,并且ViewHolder的复用机制减少内存消耗。

5.mCachedViews和RecycledViewPool的区别

mCachedViews按position匹配,命中后直接复用不需要bind,性能最好。

RecycledViewPool按ViewType匹配,命中后需要重新bind。mCachedViews满了之后,最老的ViewHolder会被移到RecycledViewPool。

Compose

Jetpack Compose

Slot Table 与Gap Buffer

Compose编译器将@Composable函数转换为对Slot Table的操作,使用Gap Buffer数据结构存储组合树:

  • Slot Table存储所有Composable的状态和参数
  • Gap Buffer使得插入/删除操作高效(类似文本编辑器的实现)
  • 重组时,Compose比较新旧参数,只更新变化的部分
重组(Recomposition)

当state变化时,Compose只重新执行读取了该State的Composable函数。

重组作用域:Compose编译器在每个Composable函数调用处插入重组作用域。

Snapshot系统

Compose的状态管理基于Snapshot系统:

  • mutableStateOf创建的State对象会被Snapshot系统追踪
  • 读取State时注册依赖关系
  • 写入State时通知所有依赖的重组作用域
稳定性推断

Compose编译器会推断类型的稳定性,决定是否可以跳过重组:

Kotlin
// 稳定类型:所有属性都是 val 且类型稳定
data class User(val name: String, val age: Int) // ✅ 稳定

// 不稳定类型:有 var 属性或不稳定类型
data class User(var name: String) // ❌ 不稳定
data class State(val items: List<Item>) // ❌ List 接口不稳定(可能是 MutableList)

@Stable@Immutable 注解手动标记稳定性。或使用 kotlinx.collections.immutableImmutableList

总结起来就是,在Compose函数运行时会使用Snapshot先创建一个快照分支;然后记录下当前快照分支下对State对象的读取操作;每读取一个State对象,就会与当前Scope进行绑定;在修改State对象时,会查找对应的Scope,最后触发Compose函数重组,重新调用Compose函数。

副作用API
Kotlin
// LaunchedEffect:进入组合时启动协程,key 变化时重启
LaunchedEffect(userId) {
    val user = repository.getUser(userId)
    // ...
}

// DisposableEffect:需要清理的副作用
DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event -> /* ... */ }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
    }
}

// rememberCoroutineScope:获取与组合绑定的 CoroutineScope
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { /* ... */ } })

// derivedStateOf:派生状态,减少不必要的重组
val filteredList by remember {
    derivedStateOf { list.filter { it.isActive } }
}

// snapshotFlow:将 Compose State 转为 Flow
LaunchedEffect(Unit) {
    snapshotFlow { scrollState.firstVisibleItemIndex }
        .collect { index -> /* ... */ }
}

Compose编译器做了什么

@Composable的本质

@Composable不是普通的注解,会改变函数的类型签名。它会在编译期对每个@Composable函数做以下变换:

Kotlin
// 你写的代码
@Composable
fun Greeting(name: String) {
    Text("Hello $name")
}

// 编译器变换后(简化)
fun Greeting(name: String, $composer: Composer, $changed: Int) {
    $composer.startRestartGroup(2104126067) // group key,基于源码位置的哈希

    if ($changed and 0b0001 == 0 && $composer.getSkipping()) {
        // 参数没变,跳过重组
        $composer.skipToGroupEnd()
    } else {
        // 执行函数体
        Text("Hello $name", $composer, ...)
    }

    $composer.endRestartGroup()?.updateScope { composer, _ ->
        // 注册重组回调:当需要重组时,重新调用自己
        Greeting(name, composer, $changed or 0b0001)
    }
}

关键变换:

  1. 注入 $composer 参数:Composer 是重组的执行引擎,管理 Slot Table 的读写
  2. 注入 $changed 参数:位掩码,跟踪每个参数是否发生变化(每个参数占 2 bit)
  3. 生成 group key:基于源码文件路径 + 行号 + 列号的哈希值,唯一标识这个 Composable 在组合树中的位置
  4. 包裹 startGroup/endGroup:在 Slot Table 中标记这个 Composable 的范围

Slot Table与Gap Buffer

Slot Table是什么

Slot Table是Compose存储组合树状态的核心数据结构。它用两个线性数组存储整棵树:

Plaintext
groups: IntArray    → 存储 Group 的元数据(key, size, parent, data slot 范围)
slots: Array<Any?> → 存储实际数据(State 值、remember 的值、CompositionLocal 等)
Plaintext
组合树:
  Column
  ├── Text("Hello")
  └── Button(onClick)
      └── Text("Click")

Slot Table(线性化):
groups: [Column | Text | Button | Text]
slots: ["Hello" | onClick_lambda | "Click"]

为什么用线性数组而不是树结构?

  • 内存紧凑,缓存友好
  • 遍历快(顺序访问)
  • 适合Gap Buffer 优化
Gap Buffer算法

Gap Buffer是文本编辑器常用的数据结构。核心思想:在数组中维护一个”间隙“,插入/删除操作只需要移动gap到目标位置。

Plaintext
初始状态(首次组合后):
[A][B][C][D][E]  gap在末尾

需要在 B 和 C 之间插入 X:
1. 移动 gap 到 B 后面:
   [A][B][___gap___][C][D][E]
2. 在 gap 位置写入 X:
   [A][B][X][__gap__][C][D][E]

需要删除 D:
1. 移动 gap 到 D 位置:
   [A][B][X][C][___gap___][E]
   (D 被 gap 覆盖,等于删除)

Compose 重组时的流程:

  1. 将 gap 移到当前重组位置
  2. 遍历旧的 Slot Table,与新的组合结果对比
  3. 相同的 group 保留(跳过)
  4. 新增的 group 写入 gap
  5. 删除的 group 被 gap 覆盖
首次组合VS重组

首次组合时:

Composer.inserting = true
→ 所有 Composable 都执行
→ 数据写入 Slot Table
→ 生成 LayoutNode 树
→ 交给 Compose UI 进行 measure/layout/draw

重组:

Plaintext
Composer.inserting = false
→ 遍历 Slot Table,对比新旧数据
→ 参数没变的 Composable 跳过(skipToGroupEnd)
→ 参数变了的 Composable 重新执行,更新 Slot Table
→ 只有变化的 LayoutNode 需要重新 measure/layout/draw

重组机制

重组作用域(RecomposeScope)

每个 Restartable 的 Composable 函数对应一个 RecomposeScope。当函数内读取的 State 发生变化时,这个 Scope 被标记为 invalid,等待重组。

Kotlin
@Composable
fun Counter() {  // ← 这是一个 RecomposeScope
    var count by remember { mutableStateOf(0) }  // 读取 State
    Text("Count: $count")  // ← 这也是一个 RecomposeScope
    Button(onClick = { count++ }) {
        Text("Add")  // ← 这也是一个 RecomposeScope
    }
}

count 变化时,只有读取了 count 的 Scope 需要重组。Text("Add") 没有读取 count,不会重组。

State 变化如何触发重组

完整链路:

Plaintext
1. count++
   → MutableState.setValue()
   → Snapshot.writeObserver 被通知

2. writeObserver 记录:这个 State 变了
   → 查找所有读取过这个 State 的 RecomposeScope
   → 将这些 Scope 标记为 invalid

3. Recomposer 收到通知
   → 在下一帧(通过 Choreographer)执行重组
   → 只重新执行 invalid 的 Scope 对应的 Composable 函数

4. 重组执行
   → Composer 遍历 Slot Table
   → 到达 invalid Scope 时,重新执行函数体
   → 对比新旧参数,更新 Slot Table
   → 生成新的 LayoutNode 变更
智能跳过的条件

Composable 函数被跳过需要满足:

  1. 函数是 Restartable 的(有 startRestartGroup)
  2. 所有参数都是稳定类型
  3. 所有参数的值与上次相同(通过 equals 或 $changed 位掩码判断)

稳定类型的定义:

  • 基本类型(Int, Float, Boolean, String 等)
  • 函数类型(lambda)—— 但有陷阱,见下文
  • 标记了 @Stable@Immutable 的类
  • 所有属性都是 val 且类型稳定的 data class
Kotlin
// ✅ 稳定:所有属性都是 val + 基本类型
data class User(val id: Int, val name: String)

// ❌ 不稳定:有 var 属性
data class User(val id: Int, var name: String)

// ❌ 不稳定:List 不是稳定类型(可能被外部修改)
data class UserGroup(val users: List<User>)

// ✅ 手动标记稳定(你保证 List 不会被修改)
@Immutable
data class UserGroup(val users: List<User>)
@Stable vs @Immutable
Kotlin
// @Immutable:所有属性永远不变(创建后不会修改)
// Compose 可以完全信任 equals 结果
@Immutable
data class Color(val r: Int, val g: Int, val b: Int)

// @Stable:属性可能变化,但变化时会通知 Compose(通过 MutableState)
// 适用于包含 MutableState 的类
@Stable
class CounterState {
    var count by mutableStateOf(0)  // 变化时自动通知
}

区别:

  • @Immutable:更强的保证,对象创建后不会变。Compose 可以更激进地跳过
  • @Stable:对象可能变,但变化是可观察的。Compose 仍然可以跳过(因为变化会触发重组)

Snapshot 系统

MutableState 的实现
Kotlin
// mutableStateOf 返回的是 SnapshotMutableStateImpl
fun <T> mutableStateOf(value: T): MutableState<T> =
    SnapshotMutableStateImpl(value, StructuralEqualityPolicy())

class SnapshotMutableStateImpl<T>(
    value: T,
    val policy: SnapshotMutationPolicy<T>
) : StateObject, MutableState<T> {

    // 实际值存储在 StateRecord 链表中(支持多版本)
    private var next: StateStateRecord<T> = StateStateRecord(value)

    override var value: T
        get() {
            // 读取时通知 readObserver
            val snapshot = Snapshot.current
            snapshot.readObserver?.invoke(this)  // ← 关键:记录谁读了这个 State
            return next.readable(this, snapshot).value
        }
        set(value) {
            // 写入时通知 writeObserver
            val snapshot = Snapshot.current
            val record = next.writable(this, snapshot)
            if (!policy.equivalent(record.value, value)) {
                record.value = value
                snapshot.writeObserver?.invoke(this)  // ← 关键:通知 State 变了
            }
        }
}
读取追踪

当 Composable 函数执行时,Composer 设置了 readObserver:

Kotlin
// Composer 在执行重组时
Snapshot.observe(
    readObserver = { state ->
        // 记录:当前 RecomposeScope 读取了这个 State
        currentRecomposeScope.recordRead(state)
    },
    writeObserver = { state ->
        // 记录:这个 State 被修改了
        // 找到所有读取过它的 Scope,标记为 invalid
    }
) {
    // 在这个 block 中执行 Composable 函数
    composable()
}

这就是 Compose 的"自动依赖追踪":你不需要手动声明依赖关系,只要在 Composable 中读取了某个 State,Compose 就自动知道这个 Composable 依赖这个 State。

Snapshot 隔离

每次重组在自己的 Snapshot 中执行,类似数据库的事务隔离:

Kotlin
// 重组开始
val snapshot = Snapshot.takeMutableSnapshot()

snapshot.enter {
    // 在这个 Snapshot 中执行重组
    // 读取的是 Snapshot 创建时的数据快照
    // 写入的修改暂时不可见给其他线程
}

// 重组完成,应用修改
snapshot.apply()  // 类似 commit
// 或者
snapshot.dispose()  // 类似 rollback

好处:

  • 重组过程中,其他线程修改 State 不会影响当前重组
  • 重组失败可以回滚
  • 支持并发重组(不同 Scope 可以在不同线程重组)

副作用 API 详解

LaunchedEffect

在 Composable 进入组合时启动协程,离开时自动取消。key 变化时重启。

Kotlin
@Composable
fun SearchScreen(query: String) {
    var results by remember { mutableStateOf(emptyList<Item>()) }

    // query 变化时,取消旧协程,启动新协程
    LaunchedEffect(query) {
        delay(300)  // 防抖
        results = api.search(query)
    }

    ItemList(results)
}

源码原理:

Kotlin
@Composable
fun LaunchedEffect(key1: Any?, block: suspend CoroutineScope.() -> Unit) {
    val applyContext = currentComposer.applyCoroutineContext
    // remember 一个 LaunchedEffectImpl
    remember(key1) {
        LaunchedEffectImpl(applyContext, block)
    }
}

// LaunchedEffectImpl 实现了 RememberObserver
class LaunchedEffectImpl : RememberObserver {
    private var job: Job? = null

    override fun onRemembered() {
        // 进入组合时启动协程
        job = scope.launch(context, block = task)
    }

    override fun onForgotten() {
        // 离开组合时取消协程
        job?.cancel()
    }

    override fun onAbandoned() {
        job?.cancel()
    }
}
DisposableEffect

需要清理资源的副作用(类似 onDestroy):

Kotlin
@Composable
fun LocationTracker() {
    val context = LocalContext.current

    DisposableEffect(Unit) {
        val listener = LocationListener { location -> /* ... */ }
        val manager = context.getSystemService<LocationManager>()
        manager.requestLocationUpdates(GPS_PROVIDER, 0, 0f, listener)

        onDispose {
            // 离开组合时清理
            manager.removeUpdates(listener)
        }
    }
}

与 LaunchedEffect 的区别:

  • LaunchedEffect:异步操作(网络请求、延迟任务)
  • DisposableEffect:需要配对的注册/注销操作(监听器、回调)
SideEffect

每次成功重组后执行(不是挂起函数,同步执行):

Kotlin
@Composable
fun Analytics(screenName: String) {
    // 每次重组后同步执行
    SideEffect {
        analytics.setCurrentScreen(screenName)
    }
}

适用场景:将 Compose 状态同步到非 Compose 管理的对象。

derivedStateOf

将多个 State 合并为一个派生 State,只在结果变化时触发重组:

Kotlin
@Composable
fun FilteredList(items: List<Item>, query: String) {
    // ❌ 每次 items 或 query 变化都重组,即使过滤结果没变
    val filtered = items.filter { it.name.contains(query) }

    // ✅ 只在过滤结果变化时重组
    val filtered by remember(items, query) {
        derivedStateOf { items.filter { it.name.contains(query) } }
    }

    LazyColumn {
        items(filtered) { item -> ItemRow(item) }
    }
}

经典场景:列表滚动时判断是否显示"回到顶部"按钮:

Kotlin
val listState = rememberLazyListState()

// 只在 "是否可见" 这个布尔值变化时重组,而不是每次滚动都重组
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

if (showButton) {
    FloatingActionButton(onClick = { /* scroll to top */ }) { ... }
}
snapshotFlow

将 Compose State 转为 Flow,在 State 变化时发射新值:

Kotlin
@Composable
fun ScrollLogger(listState: LazyListState) {
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .distinctUntilChanged()
            .collect { index ->
                analytics.logScroll(index)
            }
    }
}

原理:snapshotFlow 内部创建 Snapshot,在 block 中读取 State 时记录依赖,State 变化时重新执行 block 并发射新值。

1.Compose的重组原理?怎样优化性能?

Compose 使用 Slot Table 存储组合树的状态。当 State 变化时,Compose 标记受影响的重组作用域(Scope),只重新执行这些 Composable 函数,比较新旧参数决定是否更新 UI。

重组的关键:

  • Compose 编译器为每个 Composable 生成一个 group key(基于源码位置)
  • 参数不变的 Composable 会被跳过(skip)
  • 只有 @Stable@Immutable 标记的类型,或基本类型,才能被正确比较

性能优化:

  1. 缩小重组范围:将读取 State 的代码放在尽可能小的 Composable 中
  2. 使用 remember:缓存计算结果,避免重组时重复计算
  3. derivedStateOf:将多个 State 合并为一个派生 State,减少不必要的重组
  4. key():在列表中为 item 指定稳定的 key,避免不必要的重组
  5. @Stable/@Immutable:标记数据类,让 Compose 知道可以安全跳过

2. Compose 中的 remember 和 rememberSaveable 的区别?

  • remember:在重组时保持状态,但配置变更(旋转屏幕)时丢失
  • rememberSaveable:在重组和配置变更时都保持状态(内部使用 Bundle 保存)

3.LaunchedEffect、DisposableEffect、rememberCoroutineScope分别适合什么场景?

LaunchedEffect用于在Composable进入Composition后启动协程,离开Composition协程取消;key 变化时旧协程取消并重新启动,适合首屏加载、动画、根据状态触发一次性异步任务。

DisposableEffect 用于需要注册和反注册的副作用,比如添加 LifecycleObserver、注册监听器;它必须提供 onDispose 做清理。

rememberCoroutineScope 返回一个绑定到当前 Composition 的 CoroutineScope,适合在点击事件等非 Composable 调用点手动启动协程。

4.derivedStateOf的适用场景?

derivedStateOf适合输入状态变化频繁,但UI只需要在派生结果变化时才重组的场景,比如列表滚动位置不断变化,但只关心是否超过第一个item来显示一个“回到顶部”按钮。能用来减少不必要的重组。

5.Compose的稳定性Stable/Unstable对性能有什么影响?

Compose 编译器会根据参数稳定性决定 Composable 是否可以跳过重组。稳定参数如果和上次相等,Compose 可以跳过对应 Composable;不稳定参数在父级重组时更容易导致子级也重组。

稳定通常意味着不可变,或者 Compose 能判断值是否变化。可变对象、var 属性、普通可变集合等容易被推断为不稳定。

6.Strong Skipping是什么?对Compose重组有什么影响?

Strong Skipping是Compose Compiler的模式,它放宽了可以跳过的条件:启用后,restartable Composable即使带有不稳定参数,也可以成为skippable。

跳过时参数比较规则不同:稳定参数通常按equals比较,不稳定参数按实例相等===较。Strong Skipping还会对Composable内部lambda做更多自动remember,减少因为lambda重新分配造成的无效重组。

Strong Skipping不是让你忽略状态建模。UI State仍应尽量不可变、单向流动,避免用可变对象绕过Compose的状态观察。

7.Compose中状态提升是什么?为什么推荐下沉事件上提?

状态提升是把 Composable 内部状态移动到调用方,由父级持有状态并把 state 和 event lambda 传给子级。这样子组件更容易复用、测试,也更符合单向数据流。

常见写法是:子组件接收 valueonValueChange,自己不直接持有业务状态;ViewModel 或上层 Screen 持有 UI State,并处理事件。

好处是状态来源清晰,避免多个组件各自维护一份状态导致不一致;同时也方便配置变更恢复、预览和单元测试。

8.Compose手势处理有哪些层级?为什么优先用clickable而不是pointerInput?

Compose 手势从高到低大致分为:组件内置手势、手势 Modifier、底层 pointerInput。优先使用高层 API,因为它通常已经处理语义、无障碍、焦点、键盘触发、按压反馈和事件消费。

面试回答:简单点击用 clickable,长按/双击用 combinedClickable,拖拽/滚动/缩放优先用专门 Modifier,只有复杂自定义手势才用 pointerInput

9.pointerInput的key有什么作用?为什么key变化会影响手势?

pointerInput的block运行在协程中,key用来决定这段手势协程的生命周期。key变化时,旧的pointerInput协程会取消,新的协程会启动。

如果手势逻辑依赖外部参数,key设置不当可能导致捕获旧值;但key过于频繁变化,又可能让手势识别过程被中断。

10.Compose 中嵌套滚动和手势冲突怎么处理?

Compose 使用 nested scroll 系统协调父子滚动。内置滚动组件通常已经支持嵌套滚动;自定义联动可以通过 Modifier.nestedScroll(connection) 参与 pre-scroll、post-scroll、fling 等阶段。

不要简单理解成 View 体系里的 requestDisallowInterceptTouchEvent。Compose 更推荐通过手势消费和 nested scroll 协议协调父子组件。典型场景是折叠 Toolbar、BottomSheet 内部列表、外层容器和内层列表联动。

MVP&MVC&MVVM&MVI

MVC

model-view-controller,其中model负责数据处理,view感知model的变化并表现在界面上,controller感知view发生的变化,并提醒model对数据进行一定的操作

缺点:Activity 同时是 Controller 和 View,职责不清,容易臃肿,耦合严重

MVP

model-view-present

View 和 Presenter 通过接口通信,Presenter 可测试。但接口爆炸,生命周期管理复杂

缺点:可能会造成内存泄漏,用户关闭了View层,但如果Model层仍在进行耗时操作,然后Presenter也持有了View层的引用,所以就会无法回收view层,就会造成内存泄漏。

MVVM

View 观察 ViewModel 的 LiveData/StateFlow,数据驱动 UI。ViewModel 不持有 View 引用,配置变更时存活。耦合度很低。

MVI

单向数据流,所有UI状态集中在一个不可变State中。可预测,可追溯,但复杂度较高。

系统原理/Binder/启动流程

1.Binder的原理?为什么Android选择Binder?

Binder是Android特有的IPC机制,基于C/S架构。

选择Binder的原因:

  1. 性能:只需一次数据拷贝。发送方通过 copy_from_user 将数据拷贝到内核缓冲区,接收方的用户空间通过 mmap 映射到同一块物理内存,直接访问。而 Socket 需要两次拷贝(发送方→内核→接收方)。
  2. 安全:Binder 驱动在内核层面记录调用方的 UID/PID,不可伪造。其他 IPC(如 Socket)的身份信息由用户空间填写,可以伪造。
  3. 易用:面向对象的调用方式,AIDL 自动生成代理代码,调用远程方法就像调用本地方法。

追问:一次拷贝是怎么实现的?

接收进程在 Binder 驱动中通过 mmap 将一块内核缓冲区映射到自己的用户空间。发送进程的数据通过 copy_from_user 拷贝到这块内核缓冲区后,接收进程可以直接在用户空间访问,不需要再 copy_to_user。

2.App启动流程?从点击图标到Activity显示?

  1. Launcher 调用 startActivity,通过 Binder 发送给 AMS
  2. AMS 检查目标 Activity 的进程是否存在
  3. 进程不存在 → AMS 通过 Socket 请求 Zygote fork 新进程
  4. Zygote fork 后,新进程执行 ActivityThread.main()
  5. main() 中创建主线程 Looper 并 Looper.loop()
  6. 创建 ActivityThread 实例,通过 Binder 向 AMS 注册(attachApplication
  7. AMS 回调 bindApplication:创建 Application → attachBaseContext → ContentProvider.onCreate → Application.onCreate
  8. AMS 回调 scheduleLaunchActivity:创建 Activity → attach → onCreate → onStart → onResume
  9. ViewRootImpl.performTraversals 执行首帧绘制
  10. 用户看到内容

追问:Zygote 为什么用 fork 而不是新建进程?

fork 是 COW(Copy-On-Write),子进程共享父进程的内存页,只在写入时才复制。Zygote 预加载了 Framework 类和资源,fork 后子进程直接共享这些内容,不需要重新加载,大幅加快启动速度。

追问:为什么 Zygote 用 Socket 而不是 Binder?

fork 时如果有多线程,子进程只会保留调用 fork 的线程,其他线程消失。如果 Zygote 使用 Binder(Binder 有线程池),fork 后 Binder 线程消失会导致死锁。所以 Zygote 使用单线程的 Socket 通信。

3.Activity的启动流程?涉及哪些系统组件

  1. 调用方 startActivityInstrumentation.execStartActivity

  2. 通过 Binder 调用 AMS 的 startActivity

  3. AMS 中:

    • ActivityStarter:解析 Intent,确定启动模式

    • 查找或创建 TaskRecord(Task 栈)

    • 创建 ActivityRecord

    • 暂停当前 Activity(回调 onPause)

  4. 如果目标进程不存在,通知 Zygote fork

  5. 目标进程就绪后,AMS 通过 Binder 回调 ActivityThread

  6. ActivityThread.handleLaunchActivity:

    • 通过反射创建 Activity 实例

    • 调用 activity.attach(创建 PhoneWindow)

    • 调用 onCreate(setContentView 创建 View 树)

    • 调用 onStart、onResume

  7. WindowManager.addView → 创建 ViewRootImpl → performTraversals

4.Binder是用来解决什么问题的?

Binder 是 Android 主要的跨进程通信机制。不同进程有独立内存空间,不能直接调用对方对象,Binder 提供了一套让客户端像调用本地接口一样调用远程服务的机制。

常见场景包括:

  • App 调用系统服务,比如 ActivityManagerService、PackageManagerService。
  • AIDL 跨进程通信。
  • Service 的跨进程绑定。

5.App启动大致经过哪些步骤?

冷启动大致流程:

  1. 用户点击图标,Launcher 通过 Binder 请求系统启动目标 Activity。
  2. 系统进程中的 AMS 判断目标应用进程是否存在。
  3. 如果进程不存在,通过 Zygote fork 出应用进程。
  4. 应用进程启动 ActivityThread,创建主线程 Looper。
  5. ActivityThread 创建 Application 和 Activity。
  6. 依次执行 Activity 的 onCreateonStartonResume,完成首帧绘制。

Arouter/依赖注入

路由框架原理(以ARouter为例)

整体架构:

Plaintext
编译期(APT)                    运行时
┌──────────────┐            ┌──────────────┐
│ @Route 注解   │            │ ARouter.init │
│ RouteProcessor│ ──生成──→  │ 加载路由表     │
│ 生成路由表类   │            │              │
└──────────────┘            │ navigation() │
                            │ 匹配路由      │
                            │ 执行拦截器    │
                            │ 跳转/获取服务  │
                            └──────────────┘

编译期:APT生成路由表

Java
// 你写的代码
@Route(path = "/home/main")
public class HomeActivity extends AppCompatActivity { ... }

@Route(path = "/user/detail")
public class UserDetailActivity extends AppCompatActivity { ... }

// APT 生成的代码(每个模块一个类)
public class ARouter$$Group$$home implements IRouteGroup {
    @Override
    public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/home/main", RouteMeta.build(
            RouteType.ACTIVITY,
            HomeActivity.class,
            "/home/main",
            "home",  // group
            null,    // paramsType
            -1       // priority
        ));
    }
}

// 根路由表(索引 group → 对应的 IRouteGroup 类)
public class ARouter$$Root$$app implements IRouteRoot {
    @Override
    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("home", ARouter$$Group$$home.class);
        routes.put("user", ARouter$$Group$$user.class);
    }
}
运行时:路由查找与跳转
Java
// ARouter.init() 加载路由表
public static void init(Application application) {
    // 1. 扫描 dex 文件,找到所有 ARouter$$Root$$ 开头的类
    Set<String> routerMap = ClassUtils.getFileNameByPackageName(
        context, "com.alibaba.android.arouter.routes");

    // 2. 反射实例化,加载路由表到内存
    for (String className : routerMap) {
        if (className.startsWith(ROOT_PREFIX)) {
            ((IRouteRoot) Class.forName(className).newInstance())
                .loadInto(Warehouse.groupsIndex);
        }
    }
}

// navigation() 跳转
public Object navigation(Context context, Postcard postcard, int requestCode) {
    // 1. 根据 path 查找路由信息
    LogisticsCenter.completion(postcard);
    // 内部:从 Warehouse.routes 中查找 RouteMeta
    // 如果 group 还没加载,先加载 group

    // 2. 执行拦截器链
    interceptorService.doInterceptions(postcard, new InterceptorCallback() {
        @Override
        public void onContinue(Postcard postcard) {
            // 3. 拦截器通过,执行跳转
            _navigation(context, postcard, requestCode);
        }

        @Override
        public void onInterrupt(Throwable exception) {
            // 被拦截
        }
    });
}

private Object _navigation(Context context, Postcard postcard, int requestCode) {
    switch (postcard.getType()) {
        case ACTIVITY:
            Intent intent = new Intent(context, postcard.getDestination());
            intent.putExtras(postcard.getExtras());
            ActivityCompat.startActivityForResult(activity, intent, requestCode);
            break;

        case PROVIDER:
            // 获取服务实例(单例)
            return postcard.getProvider();

        case FRAGMENT:
            // 创建 Fragment 实例
            Fragment fragment = postcard.getDestination().newInstance();
            fragment.setArguments(postcard.getExtras());
            return fragment;
    }
}
拦截器
Kotlin
// 登录拦截器:未登录时跳转到登录页
@Interceptor(priority = 8, name = "登录拦截器")
class LoginInterceptor : IInterceptor {
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.extra == NEED_LOGIN && !UserManager.isLoggedIn()) {
            // 拦截,跳转到登录页
            ARouter.getInstance().build("/user/login").navigation()
            callback.onInterrupt(null)
        } else {
            // 放行
            callback.onContinue(postcard)
        }
    }

    override fun init(context: Context) {}
}

// 使用:标记需要登录的页面
@Route(path = "/order/detail", extras = NEED_LOGIN)
class OrderDetailActivity : AppCompatActivity()

依赖注入(Hilt)

内存抖动/性能优化

可以参考之前写的Android性能优化

//TODO Android内存回收机制

Git

git merge与git rebase的区别

git mergegit rebase两个命令都⽤于从⼀个分⽀获取内容并合并到当前分⽀。

不同点:

  1. git merge会⾃动创建⼀个新的commit,如果合并时遇到冲突的话,只需要修改后重新commit
    1. 优点:能记录真实的commit情况,包括每个分⽀的详情
    2. 缺点:由于每次merge会⾃动产⽣⼀个commit,因此在使用⼀些可视化的git工具时会看到这些自动产生的commit,这些commit对于程序员来说没有什么特别的意义,多了反而会影响阅读。
  2. git rebase会合并之前的commit历史。
    1. 优点:可以得到更简洁的提交历史,去掉了merge 产生的commit
    2. 缺点:因为合并而产生的代码问题,就不容易定位,因为会重写提交历史信息

场景:

  • 当需要保留详细的合并信息,建议使⽤git merge, 尤其是要合并到master
  • 当发现⾃⼰修改某个功能时提交比较频繁,并觉得过多的合并记录信息对自己来说没有必要,那么可尝试使用git rebase

git pull与git fetch的区别

git pull在拉取到最新的后会自动更改工作区中的内容,然后并对代码进行合并

git fetch拉取到以后并不会更新本地分支,也不会自动合并。

计网

//TODO