Skip to content

面经

Java

Java基础

TODO

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并发

Kotlin

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

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/依赖注入

内存抖动/性能优化

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