【Android进阶】Android视图加载与刷新

【Android进阶】Android视图加载与刷新

本文介绍了 Android 的Activity组件内部的视图加载与刷新流程

初始化

整体的冷启动流程在这篇文章有详细记录:

【Android进阶】APP冷启动流程解析

Activity Window View初始加载

Activity、Window 和 View 这三者是构成安卓应用用户界面的核心。

这三者之间的层级和协作关系:

  • Activity (活动):是安卓应用的四大组件之一,是用户交互的直接入口。它本身并不负责视图的绘制,而是作为窗口(Window)的容器,并管理界面的生命周期(例如,创建、暂停、销毁等)。你可以把它想象成一个舞台的管理者或导演。
  • Window (窗口):每个 Activity 都包含一个 Window 对象,通常是 PhoneWindow 的实例。Window 才是真正代表一个“窗口”的概念。它负责承载界面元素,并将这些元素传递给 WindowManager 进行显示。你可以把它看作是舞台本身,所有的布景(View)都在这个舞台上。
  • View (视图):是所有 UI 控件(如 Button, TextView)的基类。它负责在屏幕上绘制具体的内容,并处理用户的触摸事件。一个 Window 内部通常包含一个复杂的 View 树(View Hierarchy),最顶层的 View 被称为 DecorView。你可以把 View 看作是舞台上的演员和布景。

总结来说,Activity 持有一个 Window,而 Window 持有一个 View 树(以 DecorView 为根)。Activity 负责逻辑控制和生命周期管理,Window 负责承载和管理视图,而 View 负责最终的绘制和事件处理。

activity_window

初始化时机与关键周期事件

Activity 对象的初始化发生在 ActivityThread 中,通过 performLaunchActivity() 方法完成。在这个过程中,系统会通过反射调用 Activity 的无参构造函数来创建 Activity 实例。紧接着,系统会调用 Activity 的 attach() 方法,在这个方法内部,Activity 会创建一个 PhoneWindow 实例,从而将 Activity 和 Window 关联起来。

attach 时期

attach() 方法并不是 Activity 生命周期的一部分,开发者通常不需要也不应该重写它。它是框架在内部用于初始化 Activity 的一个关键步骤。

  1. 提供 Contextattach() 的最重要职责是关联一个 Context 对象。在调用 attach() 之前,Activity 实例内部的 mBase (Context) 是 null 的。调用之后,Activity 才拥有了上下文,从而能够执行诸如 getResources()getSystemService()getPackageName() 等操作。没有 Context,Activity 几乎什么都做不了。
  2. 创建 Window:在 attach() 方法内部,Activity 会创建一个 PhoneWindow 的实例,并赋值给成员变量 mWindow。这意味着在 onCreate() 被调用之前,Activity 已经有了一个关联的窗口对象。这就是为什么你可以在 onCreate() 里立即调用 setContentView() 的原因,因为 setContentView() 实际上是调用了 mWindow.setContentView()
  3. 关联其他组件:除了 Context 和 Window,attach() 还会将 Activity 与其他一些重要的系统组件关联起来,例如 Application 对象、Instrumentation 等。
// Activity.java window的创建与初始化
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        ...) {
    // 创建PhoneWindow实例
    mWindow = new PhoneWindow(this, window);
    // 设置Window回调
    mWindow.setCallback(this);
    // 设置Window管理器
    mWindow.setWindowManager(...);
}

onCreate 时期

onCreate() 是我们熟知的 Activity 生命周期的第一个回调方法。它是开发者进行 Activity 初始化的主要入口。

最常见的操作就是调用 setContentView(R.layout.activity_main),这一步依赖于在 attach() 中创建好的 Window 对象。

// PhoneWindow的setContentView方法
public void setContentView(int layoutResID) {
    // 1. 检查是否有DecorView,没有则创建
    if (mContentParent == null) {
        installDecor();
    } else {
        // 如果已有内容视图,则移除
        mContentParent.removeAllViews();
    }
    
    // 2. 将布局inflate到mContentParent中
    mLayoutInflater.inflate(layoutResID, mContentParent);
    
    // 3. 通知Activity内容已改变
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

mContentParent 实例通常是一个 FrameLayout 对象。用于容纳内容视图,这一步就是将 R.layout.main 对应的视图结构,作为子视图添加(addView())到这个 mContentParent(即 FrameLayout)中。

onResume() 时期

Activity和窗口创建完成后, ActivityThread 调用 handleResumeActivity 来执行其 onResume() 流程,在 Activity 的 onResume() 周期回调之后,执行 makeVisible()

然后 WindowManager 执行 addView 动作,开启视图绘制逻辑,创建 ViewRootImpl 对象,并调用其 setView 方法。

public void addView(...) {
     // 创建ViewRootImpl对象
     root = new ViewRootImpl(view.getContext(), display);
     ...
     try {
         // 执行ViewRootImpl的setView函数
         root.setView(view, wparams, panelParentView, userId);
     } catch (RuntimeException e) {
         ...
     } 
}

setView() 源码:

/*frameworks/base/core/java/android/view/ViewRootImpl.java*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
      synchronized (this) {
         if (mView == null) {
             mView = view;
         }
         ...
         // 开启绘制硬件加速,初始化RenderThread渲染线程运行环境
         enableHardwareAcceleration(attrs);
         ...
         // 1.触发绘制动作
         requestLayout();
         ...
         inputChannel = new InputChannel();
         ...
         // 2.Binder调用访问系统窗口管理服务WMS接口,实现addWindow添加注册应用窗口的操作,并传入inputChannel用于接收触控事件
         res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
         ...
         // 3.创建WindowInputEventReceiver对象,实现应用窗口接收触控事件
         mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
         ...
         // 4.设置DecorView的mParent为ViewRootImpl
         view.assignParent(this);
         ...
      }
}

setView() 方法中,ViewRootImpl 会将传入的 View(即 DecorView)与窗口管理器(WindowManager)关联起来,并设置必要的参数。随后,ViewRootImpl 会调用 requestLayout() 来请求布局更新,这会触发后续的测量、布局和绘制流程。

关于绘制三大步主要涉及不同的View和ViewGroup的测量布局规则不同,细节也可以看冷启动文章。

UI刷新流程

Choreographer 编舞者介绍

Choreographer 是 Android 框架中一个至关重要的系统服务,它主要负责协调动画、输入事件和 UI 绘制操作的计时,确保这些操作都在每一次屏幕硬件刷新信号(Vsync)到来时同步进行。

简单来说,Choreographer的核心作用是实现流畅的、与屏幕刷新同步的 UI 渲染

核心作用:同步 Vsync 信号

Choreographer 最主要的作用是将应用程序的渲染操作(如绘制、动画计算)与显示屏的垂直同步信号(Vertical Synchronization,简称 Vsync)对齐

Vsync 信号是显示硬件发出的一个周期性信号,表示屏幕已经完成了当前帧的显示,可以开始接收下一帧的数据。 在大多数设备上,Vsync 信号的频率是 $60Hz$,意味着每 $16.67$ 毫秒($1000ms / 60$ 帧)发生一次。

没有 Choreographer 协调的情况下 ,如果应用在屏幕刷新到一半时提交了新的帧数据,就会导致屏幕的上下部分显示两帧不同的内容,形成视觉上的“撕裂”现象(Tearing)。

Choreographer 确保应用的绘制操作只在 Vsync 信号到来后才开始执行,并且在下一个 Vsync 信号到来之前完成,从而彻底消除画面撕裂

如果应用程序在 16.67ms 内没有完成 Measure、Layout 和 Draw 的全部过程,它就会错过当前的 Vsync 信号,导致该帧无法及时显示,用户就会感觉到“卡顿”或“丢帧”(Jank)。

Choreographer 的职责是提供一个清晰的计时框架,让开发者能明确知道自己有多少时间来完成渲染。它为所有需要基于时间同步的操作(如动画、滚动)提供了一个统一、可靠的时间源(Vsync 时间),确保它们以相同的节奏进行。

还可以将在一个短时间内发生的多个 View.invalidate() 请求合并起来,只在下一个 Vsync 周期内统一执行一次 Measure/Layout/Draw,避免不必要的重复渲染,优化性能。

Choreographer工作流程详解

当您执行一个需要更新 UI 的操作(例如调用 $View.invalidate()$ 或启动一个动画)时,Choreographer 的工作流程如下:

  1. 注册回调: 应用层(如 ViewRootImpl 或 Animator)会向 Choreographer 注册一个回调。
  2. 等待 Vsync: Choreographer 收到注册请求后,不会立即执行,而是等待系统下一次 Vsync 信号的到来。
  3. Vsync 信号到达: 当 Vsync 信号到来时,Choreographer 会被唤醒。
  4. 执行回调: Choreographer 会在当前这一帧的处理周期内,按照预定的优先级顺序依次执行已注册的各类回调:
    • CALLBACK_INPUT: 处理输入事件(如触摸)。
    • CALLBACK_ANIMATION: 执行动画计算(如 $ValueAnimator$ 的值更新)。
    • CALLBACK_TRAVERSAL (最重要): 执行 View 树的“遍历”($Measure$、 $Layout$、 $Draw$)操作,即完成 UI 的实际渲染。
    • CALLBACK_COMMIT: 提交绘制结果到 SurfaceFlinger。

所有的操作在一个 Vsync 周期(16.67ms)内完成,并将新的图像数据提交给显示系统,等待下一次 Vsync 信号到来时显示。

View.invalidate() 刷新流程

整个渲染流水线通常可以分为以下几个核心阶段:触发 (Invalidate)同步 (Sync/Vsync)绘制 (Draw)提交 (Issue Commands)光栅化 (Rasterization)显示 (Display)

1. 触发与同步阶段

当 View 的内容发生变化,需要重绘时,调用此方法。它不会立即重绘,而是将 View 标记为“脏 (dirty)”。 invalidate() 最终会将重绘请求传递给 ViewRootImplViewRootImpl 会调度一个重绘操作 (通过 Choreographer.postCallback),等待下一个 Vsync 信号。

设备屏幕以固定的刷新率(如 60Hz)定时发出垂直同步信号 (Vsync)。Choreographer 收到 Vsync 信号。

同步 (Sync) 阶段开始后ViewRootImpl 会执行 traversal。包括 Measure 和 Layout 以确定 View 的位置。还有动画任务计算动画的下一帧属性值。

2. 绘制阶段 (CPU 生成 DisplayList)

系统从根 View 开始递归调用 draw() 方法。但在硬件加速开启时,这个 draw() 不再是直接绘制像素,而是记录绘制操作。

每个 View 的 draw() 方法会将绘制命令(如 “画一个矩形”、”画一个 Bitmap” 等)记录到它自己的 DisplayList 中。DisplayList 是一种可重用的,优化的渲染操作序列。这一步由 Android 的渲染引擎 (在旧版本是 OpenGL ES,新版本是 Vulkan/Skia) 在 CPU 上完成。

3. 提交与传输阶段 (CPU/GPU 协同)

当所有 View 的 DisplayList 都生成后,这些列表会被交给 RenderThread (渲染线程),这是一个独立于主线程的线程。RenderThread 会处理 DisplayList,并将所需的资源(例如,新解码的 Bitmap)从 CPU 内存传输到 GPU 内存,作为 纹理 (Texture)。这是 CPU 和 GPU 内存之间的同步操作。RenderThread 将 DisplayList 中的高级绘制命令,转换为底层的图形 API 命令,即 Draw Calls(通常是 OpenGL ES 或 Vulkan API 调用),并将这些命令排队等待 GPU 执行。

4. 光栅化与处理阶段 (GPU 工作为主)

光栅化 (Rasterization) 阶段开始:GPU 从队列中取出 Draw Calls。光栅化是将向量图形指令(如绘制一个三角形)转换为屏幕上的像素颜色值的过程。 GPU 利用其强大的并行计算能力,对 Draw Calls 中引用的纹理进行采样、应用着色器 (Shader) 程序(如顶点着色器和片段着色器)来确定每个像素的最终颜色。 GPU 将处理完成的像素数据写入到它控制的 帧缓冲区 (Frame Buffer) 中。通常有前后两个缓冲区(双缓冲机制)。

5. 显示阶段 (系统级工作)

当一帧完全渲染到“后缓冲区”后,RenderThread 会调用 swapBuffers() 或类似的命令。这个操作会告诉 SurfaceFlinger(系统级的窗口合成器)该帧已准备好。

SurfaceFlinger 是一个系统服务,它负责收集所有可见窗口(应用、状态栏、导航栏等)的最新帧缓冲区,并根据它们的 Z-order、位置和透明度,将它们合成到最终的屏幕缓冲区中。这个合成过程本身也可以由 GPU 加速完成。

在下一个 Vsync 信号到来时,显示硬件(Display Hardware)从最终的合成缓冲区读取数据,并将图像电流发送到屏幕,最终用户才能看到更新后的内容。

【Android进阶】Android热门原理流程总结

【Android进阶】Android热门原理流程总结

本文介绍了在Android应用层开发过程中,比较重要的运行流程总结

JVM内存模型

jvm_ram

  • 程序计数器:一小块区域, 线程私有 。记录了每个线程的代码执行到了哪一行,各种循环,判断都是通过这个区域存的数值来走的。Java多线程是时间分片,各个线程在一段时间内占用这个核来执行任务,这个线程切换到另一个线程,其恢复的依据也是计数器的值。
  • 虚拟机栈:周期与线程相同,也是 线程私有 。每个方法执行时,都会创建一个栈帧, 栈帧里面存储方法内的局部变量表,方法出口等等信息 。每个方法执行到退出的过程,就是一个个的方法栈帧入栈出栈的过程。这个区域有两个异常,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果JVM允许动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。
  • 本地方法栈:和虚拟机栈作用一样,但是服务于 本地的Native方法 。同样会抛出上面的两种异常。
  • Java堆:最大的一块,所有 线程共享 的数据。几乎所有的对象实例都在这里保存。Java堆是垃圾收集器管理的内存区域。Java堆可以处于物理上不连续的内存空间中,可以选择固定大小或可扩展。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出 OutOfMemoryError 异常。
  • 方法区: 线程共享 。用于存储已被虚拟机加载的 对象类型信息、常量、静态变量、即时编译器编译后的代码缓存 等数据。对其要求比较宽松,几乎不用考虑垃圾回收,但是回收也是有必要的,主要针对常量的回收和类型卸载。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。
    • 运行时常量池,其是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行期间也可以将新的常量放入池中。

类加载流程

父子类加载的具体流程

  1. 加载阶段
    • 父类优先:当加载一个类时,JVM会先检查其父类是否已加载
    • 递归加载:如果父类未被加载,则会递归加载父类及其父类,直到Object类
    • 子类后加载:所有父类加载完成后,才开始加载子类
  2. 准备阶段
    • 父类优先:为父类的静态变量分配内存并设置默认值
    • 子类后处理:然后为子类的静态变量分配内存并设置默认值
  3. 初始化阶段
    • 父类优先:执行父类的静态代码块和静态变量赋值
    • 子类后处理:然后执行子类的静态代码块和静态变量赋值

实例化时的加载顺序

当创建子类实例时,加载顺序如下:

  1. 父类静态成员和静态块(只在第一次加载类时执行一次)
  2. 子类静态成员和静态块(只在第一次加载类时执行一次)
  3. 父类实例变量初始化
  4. 父类构造代码块
  5. 父类构造函数
  6. 子类实例变量初始化
  7. 子类构造代码块
  8. 子类构造函数

代码示例

class Parent {
    static {
        System.out.println("Parent静态代码块");
    }
    
    {
        System.out.println("Parent构造代码块");
    }
    
    public Parent() {
        System.out.println("Parent构造函数");
    }
}

class Child extends Parent {
    static {
        System.out.println("Child静态代码块");
    }
    
    {
        System.out.println("Child构造代码块");
    }
    
    public Child() {
        System.out.println("Child构造函数");
    }
}

public class Test {
    public static void main(String[] args) {
        new Child();
    }
}

输出:

Parent静态代码块 Child静态代码块 Parent构造代码块 Parent构造函数 Child构造代码块 Child构造函数

设计模式

见另一篇详细文章: 设计模式

垃圾回收流程

JVM

根搜索算法(GC ROOT Tracing)

Java中采用了该算法来判断对象是否是存活的,也叫可达性分析。

通过一系列名为 GC Roots 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论来说就是从GC Roots到这个对象不可达)时,则证明对象是不可用的,即该对象是“死去”的,同理,如果有引用链相连,则证明对象可以,是“活着”的。

哪些可以作为GC Roots的对象呢?Java 语言中包含了如下几种:

    1)虚拟机栈(栈帧中的本地变量表)中的引用的对象。

    2)方法区中的类静态属性引用的对象。

    3)方法区中的常量引用的对象。

    4)本地方法栈中JNI(即一般说的Native方法)的引用的对象。

    5)运行中的线程

    6)由引导类加载器加载的对象

    7)GC控制的对象

回收流程

现代商用虚拟机基本都采用分代收集算法来进行垃圾回收,当然这里的分代算法是一种混合算法,不同时期采用不同的算法来回收。

由于不同的对象的生命周期不一样,分代的垃圾回收策略正式基于这一点。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。该算法包含三个区域:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)

jvm_find

年轻代(Young Generation)

所有新生成的对象首先都是放在年轻代中。年轻代的目标就是尽可能快速地回收哪些生命周期短的对象。

新生代内存按照8:1:1的比例分为一个Eden区和两个survivor(survivor0,survivor1)区。

  • Eden区,字面意思翻译过来,就是伊甸区,人类生命开始的地方。当一个实例被创建了,首先会被存储在该区域内,大部分对象在Eden区中生成。
  • Survivor区,幸存者区,字面理解就是用于存储幸存下来对象。

回收时机:

  1. 一开始都在Eden区里,当Eden快满了就触发回收,之后,先将Eden区还存活的对象复制到一个Survivor0区,然后清空Eden区。
  2. 当这个Survivor0区也存放满了后,则将Eden和Survivor0区中存活对象都复制到另外一个survivor1区,然后清空Eden和这个Survivor0区,此时的Survivor0区就也是空的了。
  3. 然后将Survivor0区和Survivor1区交换,即保持Servivor1为空,如此往复。

这种回收算法也叫 复制算法 ,即将存活对象复制到另一个区域,然后尽可能清空原来的区域。

新生代发生的GC也叫做 Minor GC ,MinorGC发生频率比较高,不一定等Eden区满了才会触发。

为什么设置两个survivor区域?

如果只有一个eden区和一个survivor区,那么假设场景,当发生ygc后,存活对象从eden迁移到survivor,这样看好像没什么问题,很棒,但是假设eden满了,这个时候要进行ygc,那么发现此时,eden和survivor都保存有存活对象,那么你是不是要对这两个区域进行gc,找出存活对象,那么你想想是不是难度很大,还容易造成碎片,如果你使用复制算法,那么难度很大,那么耗时很长。如果你使用标记清除算法,那么很容易造成内存碎片。

​所以如果存在两个survivor区,那么工作就非常的轻松,只需要在eden区和其中一个survivor(b1)找出存活对象,一次性放到另一个空的survivor(b2),然后再直接清除eden区和survivor(b1),这样效率是不是就很快了。

年轻代往老年代转移的条件

  1. 有一个JVM参数 -XX:PretenureSizeThreshold ,默认值是0,表示任何情况都先把对象分配给Eden区。若设置为1048576字节,也就是1M。则表示当创建的对象大于1M时,就会直接把这个对象放入到老年区,就根本不会经过新生区了。这么做的原因: 大对象在经历复制算法进行GC的时候会降低性能
  2. 如果新生区中的某个对象 经历了15次GC 后,还是没有被回收掉,那么它就会被转入老年区。
  3. 如果当Survivor1区不足以存放Eden区和Survivor0的存活对象时,就将存活对象 直接放到年老代

如果年老代也满了,就会触发一次Major GC(即Full GC),即新生代和年老代都进行回收。

年老代(Old Generation)

在新生代中经历了多次GC后仍然存活的对象,就会被放入到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

年老代比新生代内存大很多(大概比例2:1?),当年老代中存满时触发Major GC,即Full GC,Full GC发生频率比较低,年老代对象存活时间较长,存活率比较高。

一开始对象都是任意分布的,在经历完垃圾回收之后,就会标记出哪些是存活对象,哪些是垃圾对象,然后就会把这些存活的对象在内存中进行整理移动,尽量都挪到一边去靠在一起,然后再把垃圾对象进行清除,这样做的好处就是避免了垃圾回收后产生的大片内存碎片。

即此处采用的叫 Compacting 算法,由于该区域比较大,而且通常对象生命周期比较长,compaction需要一定的时间,所以这部分的GC时间比较长。较为耗时,比复制算法慢10倍;

所以如果系统频繁出现Full GC,会严重影响系统性能,出现卡顿。所以JVM优化的一大问题就是减少Full GC频率。

持久代(Permanent Generation)

持久代用于存放静态文件,如Java类、方法等,该区域比较稳定,对GC没有显著影响。这一部分也被称为运行时常量,有的版本说JDK1.7后该部分从方法区中移到GC堆中,有的版本却说,JDK1.7后该部分被移除,有待考证。

DVM & Art

这两个虚拟机的垃圾回收见另一篇详细文章:

【Android进阶】JVM&DVM&ART虚拟机对比

强引用、弱引用、软引用、虚引用

强引用是最常见的引用类型,通过new创建的对象默认都是强引用。只要强引用存在,垃圾回收器永远不会回收被引用的对象。可能导致内存泄漏(当对象不再需要但仍有引用指向它时),使用场景为普通对象创建,需要长期持有的对象。

弱引用通过WeakReference类创建,当垃圾回收器执行时,无论内存是否足够都会回收被弱引用引用的对象。使用场景为缓存对象,当对象不再需要时,可以被垃圾回收器回收。一般为临时缓存,主要防止内存泄漏。

软引用通过SoftReference类创建,当内存不足时,垃圾回收器会回收软引用引用的对象。使用场景为内存敏感的对象,当内存不足时,可以被垃圾回收器回收。例如缓存对象,图片缓存等。

虚引用通过PhantomReference类创建,虚引用不会影响对象的生命周期,主要用于跟踪对象被垃圾回收器回收的状态。使用场景为对象被垃圾回收器回收时的回调。

内存泄漏常见场景

见性能优化篇: 【Android性能优化】内存

线程间通信

如何实现多线程安全?synchronized 和 ReentrantLock 的区别?

线程池

见另一篇文章: 【通用开发】Java线程池

安卓设备开机流程

作为应用开发,除了SystemUI和Launcher外,我们更多关注的是应用层的启动流程。对于系统启动稍作了解即可。

【Android进阶】Android设备开机流程

冷启动流程

见另一篇文章: 【Android进阶】APP冷启动流程解析

Handler & 消息处理机制

【Android进阶】Handler消息机制的上下层设计与流程详解

Activity & Window 初始化

在 Android 中,Activity 和 Window 通过一系列紧密的协作关系绑定在一起,共同构成用户界面的基础架构。以下是它们的绑定机制详细分析:

基本关系框架

Activity
└── PhoneWindow (Window的唯一实现)
    └── DecorView (顶级View)
        └── 内容区域(包含开发者设置的布局)

绑定过程的关键步骤

Activity创建时初始化Window,在Activity的attach()方法中完成初始绑定:

// Activity.java
final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        ...) {
    // 创建PhoneWindow实例
    mWindow = new PhoneWindow(this, window);
    // 设置Window回调
    mWindow.setCallback(this);
    // 设置Window管理器
    mWindow.setWindowManager(...);
}

Window的创建时机

在Activity的attach()方法中创建,实际类型是PhoneWindow(Window的唯一实现类),与Activity生命周期绑定,一个Activity对应一个Window。

关键绑定点说明

绑定点说明
mWindow.setCallback(this)将Activity设置为Window的回调接口,用于接收Window的各种事件通知
mWindow.setWindowManager()建立与WindowManager的连接,用于管理Window的显示位置和状态
setContentView()通过Window将视图层级与Activity关联,开发者设置的布局最终会添加到DecorView中

Activity → Window 的通信

主要通过直接调用Window的方法:

// Activity中调用Window方法的示例
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

//通过Window.Callback接口回调:
// Window.Callback接口主要方法
public interface Callback {
    boolean dispatchKeyEvent(KeyEvent event);
    boolean dispatchTouchEvent(MotionEvent event);
    void onContentChanged();
    void onWindowFocusChanged(boolean hasFocus);
    // ...
}

Activity实现了这个接口:

// Activity.java
public class Activity extends ContextThemeWrapper 
        implements Window.Callback, ... {
    // 实现回调方法
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 处理触摸事件
    }
}

视图层级绑定

通过setContentView()建立视图绑定关系:

Activity.setContentView()

public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
}

PhoneWindow.setContentView()

Activity与Window的生命周期关键交互点

  • onCreate() Window已创建但视图未显示 通常在这里调用setContentView()
  • onStart()/onResume() Window开始变得可见 ViewRootImpl建立连接
  • onAttachedToWindow() View被附加到Window时回调 可以获取真实的宽高参数
  • onWindowFocusChanged() Window获得/失去焦点时回调 标志真正的用户交互开始/结束

设计原理分析

这种绑定机制实现了以下设计目标,职责分离:

  • Activity负责业务逻辑和生命周期
  • Window负责视图管理和系统交互

布局膨胀(Layout Inflation)流程分析

布局膨胀是将XML布局文件转换为实际的View对象层次结构的过程。

基本流程概述

  • 布局文件解析:将XML文件转换为可处理的节点结构
  • View对象创建:根据XML标签创建对应的View实例
  • 属性应用:将XML属性设置到View对象上
  • 层次构建:递归处理子View,构建完整的View树

核心类与组件

  • LayoutInflater:执行膨胀过程的核心类
  • XmlPullParser:用于解析XML布局文件
  • AttributeSet:表示XML属性集合的接口

初始化LayoutInflater

LayoutInflater inflater = LayoutInflater.from(context);
// 或
LayoutInflater inflater = (LayoutInflater) 
    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

inflate()方法调用

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

实际膨胀过程

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        // 1. 解析XML
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        
        // 2. 临时存储结果View
        View result = root;
        
        try {
            // 3. 查找根节点
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // 跳过非开始标签
            }
            
            // 4. 获取根元素名称
            final String name = parser.getName();
            
            // 5. 处理特殊标签
            if (TAG_MERGE.equals(name)) {
                // 处理<merge>标签
                rInflate(parser, root, attrs, false);
            } else {
                // 6. 创建根View
                final View temp = createViewFromTag(root, name, attrs);
                
                // 7. 递归创建子View
                rInflateChildren(parser, temp, attrs);
                
                // 8. 决定是否附加到root
                if (root != null && attachToRoot) {
                    root.addView(temp);
                }
                
                result = temp;
            }
        } catch (Exception e) {
            // 异常处理
        }
        
        return result;
    }
}

View创建过程(createViewFromTag)

View createViewFromTag(View parent, String name, AttributeSet attrs) {
    // 1. 处理<blink>等特殊标签(已废弃)
    if (name.equals("blink")) {
        // ...
    }
    
    // 2. 尝试使用Factory创建View
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    
    // 3. 没有Factory则使用系统默认方式创建
    if (view == null) {
        try {
            // 4. 处理带点号的全限定名
            if (-1 == name.indexOf('.')) {
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
        } catch (Exception e) {
            // 异常处理
        }
    }
    
    return view;
}

递归膨胀子View(rInflate)

void rInflate(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) {
    // 1. 获取布局深度
    final int depth = parser.getDepth();
    
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
        
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
        
        // 2. 获取当前标签名
        final String name = parser.getName();
        
        // 3. 处理特殊标签
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            // 处理<include>标签
            parseInclude(parser, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge> must be the root element");
        } else {
            // 4. 创建普通View
            final View view = createViewFromTag(parent, name, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            
            // 5. 递归处理子View
            rInflateChildren(parser, view, attrs);
            
            // 6. 添加到父View
            viewGroup.addView(view);
        }
    }
}

性能优化相关

  • 使用 AsyncLayoutInflater 进行异步加载(API 24+)
  • 预加载常用布局并缓存
  • 减少布局层级,避免不必要的嵌套
  • 使用 merge 标签减少层级

自定义View问题:

  • 确保实现了所有必要的构造函数
  • 检查自定义属性是否正确定义和引用
  • 理解布局膨胀流程有助于优化布局性能,解决布局相关问题,以及实现高级自定义功能

setContentView流程

setContentView 是 Android 开发中用于设置 Activity 界面布局的核心方法。以下是它的详细工作流程:

基本调用流程

  • Activity.setContentView(),这是开发者最常调用的入口方法,有多个重载版本:传入布局资源ID、View对象等
  • 委托给 Window 对象,Activity 内部通过 getWindow().setContentView() 委托处理。Window 是抽象类,实际实现是 PhoneWindow
  • PhoneWindow.setContentView(),这是真正的实现核心

PhoneWindow.setContentView() 主要步骤

public void setContentView(int layoutResID) {
    // 1. 检查是否有DecorView,没有则创建
    if (mContentParent == null) {
        installDecor();
    } else {
        // 如果已有内容视图,则移除
        mContentParent.removeAllViews();
    }
    
    // 2. 将布局inflate到mContentParent中
    mLayoutInflater.inflate(layoutResID, mContentParent);
    
    // 3. 通知Activity内容已改变
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

installDecor() 过程

这是创建窗口装饰的关键方法:

private void installDecor() {
    if (mDecor == null) {
        // 1. 创建DecorView
        mDecor = generateDecor();
        // 配置DecorView属性
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
    }
    
    if (mContentParent == null) {
        // 2. 生成mContentParent(实际是ContentView的父容器)
        mContentParent = generateLayout(mDecor);
        
        // 3. 设置其他窗口装饰元素
        // 如标题栏、ActionBar等
    }
}

generateLayout() 过程

这个方法根据窗口特性选择不同的窗口装饰布局:

  • 根据主题风格选择基础布局(如 R.layout.screen_simple)
  • 将选定的布局inflate到DecorView中
  • 找到内容视图的容器(ID为android.R.id.content的FrameLayout)
  • 返回这个内容容器作为mContentParent

重要注意事项

  • 多次调用setContentView:后续调用会替换之前的内容视图,但DecorView不会重建
  • 主题影响:窗口装饰布局的选择受Activity主题影响
  • 性能考虑:inflate布局是相对耗时的操作,应优化布局文件
  • 时机问题:必须在Activity.onCreate()之后调用,某些窗口特性需要在setContentView之前设置
  • 异步inflate:Android 8.0+支持异步inflate(使用AsyncLayoutInflater)

View绘制三部曲

创建Activity时,实际调用了ActivityThread的performLaunchActivity,这时候DecorView会被创建。

在handleResumeActivity时,DecorView会被Activity里的windowManager添加到PhoneWindow窗口中,实际是了ViewRootImpl的setView方法将DecorView传进去。

再之后会走到 ViewRootImplperformTraversals 方法,真正开始ViewTree的工作流程。这个方法非常长,非常重要,这里面主要执行了3个方法,分别是performMeasure、performLayout和performDraw。

Measure

在Measure测量的时候,会用到一个MeasureSpec类,这个类内部的一个32位的int值,其中高2位代表了SpecMode,低30位则代表SpecSize。SpecMode指的是测量模式,SpecSize指的是测量大小 通过位运算来给这个常量的高2位赋值,有三个情况:

  • 00—UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
  • 11—- AT_MOST:最大模式,对应于wrap_comtent属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
  • 01—–EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值。

每一个普通View都有一个 MeasureSpec 属性来对其进行测量。而对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定。

performMeasure这个方法里,会对一众的子ViewGroup和子View进行测量。

View的onMeasure方法:实际是看 getDefaultSize() 来解析其宽高的,注意对于View基类来说,为了扩展性,它的两个MeasureSpec,AT_MOST和EXACTLY处理是一样的,即其宽高直接取决于所设置的specSize,所以自定义View直接继承于View的情况下,要想实现wrap_content属性,就需要重写onMeasure方法,自己设置一个默认宽高值。

ViewGroup的Measure方法:它没有onMeasure,有一个 measureChildren() 方法:简单来说就是 根据自身的MeasureSpec子元素的的LayoutParams属性 来得出的子元素的MeasureSpec 属性。有一点注意的是如果父容器的 MeasureSpec 属性为AT_MOST,子元素的LayoutParams属性为WRAP_CONTENT,最后计算出的子元素MeasureSpec为AT_MOST,相当于设置matchparent。

每一种ViewGroup的计算方式都不尽相同,像LinearLayout的就是单纯的在其方向上所有子元素的宽/高都加在一起。

Layout

ViewGroup中的layout方法用来确定子元素的位置,View中的layout方法则用来确定自身的位置。

所以一般都是ViewGroup来计算子View的参数,并调用子控件的layout方法。

View的layout方法,其中分别传入 left,top,right,bottom 四个参数,表示其距离父布局的四个距离,再走到setFrame,最后到onLayout,这是一个空方法,由继承的类自己实现。

像LinearLayout,其各个子控件会按照顺序排布,childTop值越来越大,子View就会按照顺序排布,而不是叠到一起。

Draw

官方注释清楚地说明了每一步的做法,它们分别是:

(1)如果需要,则绘制背景。
(2)保存当前canvas层。
(3)绘制View的内容。
(4)绘制子View。
(5)如果需要,则绘制View的褪色边缘,这类似于阴影效果。
(6)绘制装饰,比如滚动条。

绘制背景drawBackGround的时候,如果有偏移值,就会在偏移之后的Canvas上绘制。

第三步onDraw和第四步dispatchDraw都是空实现,由子View自定。

像ViewGroup就重写了dispatchDraw方法,遍历子View去绘制,需要注意的是会检索是否有缓存,如果有会直接拿缓存来显示。

热门八股问题

onresume获取不到View的宽高,而View.post就可以拿到:

  1. onCreate和onResume中无法获取View的宽高,是因为还没执行View的绘制流程。
  2. view.post之所以能够拿到宽高,是因为在绘制之前,会将获取宽高的任务放到Handler的消息队列,等到View的绘制结束之后,便会执行。

三种动画

补间动画

核心特性

  • 操作对象:作用于整个 View
  • 动画类型:平移(Translate)、缩放(Scale)、旋转(Rotate)、透明度(Alpha)
  • 资源定义:可通过 XML 或代码定义
  • 视觉限制:只改变绘制位置,不改变实际属性
Animation anim = AnimationUtils.loadAnimation(this, R.anim.slide_in);
view.startAnimation(anim);

优缺点分析

  • 简单易用,容易实现
  • 动画效果简单
  • 资源占用低
  • 无法实现复杂的动画
  • 无法精确控制动画效果
  • 只改变绘制位置,不改变实际属性

属性动画

核心特性

  • 操作对象:可作用于任何对象的任意属性
  • 核心类:ValueAnimator、ObjectAnimator、AnimatorSet
  • 高级功能:插值器、估值器、动画组合
  • 真实改变:实际修改目标属性值
// 透明度动画
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f);
alphaAnim.setDuration(1000);
alphaAnim.start();

// 组合动画
AnimatorSet set = new AnimatorSet();
set.playTogether(
    ObjectAnimator.ofFloat(view, "translationX", 0f, 100f),
    ObjectAnimator.ofFloat(view, "rotation", 0f, 360f)
);
set.setDuration(500).start();

优缺点分析

  • 功能强大,可操作任何属性
  • 动画效果更真实
  • 支持复杂的动画组合
  • 实现相对复杂
  • 资源消耗较高

帧动画

帧动画(Frame Animation)是Android中最基础的动画类型之一,它通过快速切换一系列静态图片来产生动画效果,类似于传统电影或GIF动画的工作原理。

核心特性

  • 逐帧播放:按顺序显示一系列图片
  • 资源形式:通常使用多张PNG/JPG图片
  • 实现方式:通过AnimationDrawable类实现
  • 控制方式:可控制播放速度、循环次数等

性能优化建议

图片优化:

  • 使用WebP格式替代PNG可减小体积
  • 确保图片尺寸不过大
  • 使用适当的压缩工具处理图片
  • 避免重复创建AnimationDrawable实例
  • 考虑使用单例模式管理常用动画
  • 根据设备性能调整帧率
  • 在Activity/Fragment不可见时停止动画
@Override
protected void onPause() {
    super.onPause();
    if (animation != null && animation.isRunning()) {
        animation.stop();
    }
}

事件分发

Android 的点击事件分发机制是一个典型的 责任链模式 ,事件从最外层的 ViewGroup 开始,沿着视图层级依次传递,直到被某个 View 消费为止。

事件分发三大核心方法

  • dispatchTouchEvent(MotionEvent event) - 事件分发入口
  • onInterceptTouchEvent(MotionEvent event) - 事件拦截(仅ViewGroup)
  • onTouchEvent(MotionEvent event) - 事件处理

完整分发流程

  1. Activity 层级分发

     // Activity.dispatchTouchEvent()
     public boolean dispatchTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             onUserInteraction(); // 用户交互回调
         }
         if (getWindow().superDispatchTouchEvent(ev)) {
             return true; // 被Window处理
         }
         return onTouchEvent(ev); // 最后由Activity处理
     }
    
  2. ViewGroup 层级分发,ViewGroup 的分发流程最为复杂:
     // ViewGroup.dispatchTouchEvent() 简化流程
     public boolean dispatchTouchEvent(MotionEvent ev) {
         // 1. 检查拦截
         if (onInterceptTouchEvent(ev)) {
             return super.dispatchTouchEvent(ev); // 转为View的处理流程
         }
            
         // 2. 遍历子View寻找能处理事件的View
         for (int i = childrenCount - 1; i >= 0; i--) {
             View child = getChildAt(i);
             if (child.dispatchTouchEvent(ev)) {
                 mFirstTouchTarget = child; // 记录触摸目标
                 return true; // 事件已消费
             }
         }
            
         // 3. 没有子View处理则自行处理
         return super.dispatchTouchEvent(ev);
     }
    
  3. View 层级处理
     // View.dispatchTouchEvent()
     public boolean dispatchTouchEvent(MotionEvent event) {
         // 1. 先检查OnTouchListener
         if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
             return true;
         }
            
         // 2. 再调用onTouchEvent
         return onTouchEvent(event);
     }
    

事件序列处理机制 一个完整的触摸事件通常包含:

  • ACTION_DOWN - 手指按下(必须处理)
  • ACTION_MOVE - 手指移动(可能多次)
  • ACTION_UP - 手指抬起
  • ACTION_CANCEL - 事件被取消

关键规则:

  • 如果 View 不消费 ACTION_DOWN,后续事件不会传递给它
  • 一旦某个 View 开始消费事件,整个事件序列都会交给它
  • 父View可以通过 onInterceptTouchEvent 中途拦截事件

事件分发UML序列图:

[Activity] -> [Window] -> [DecorView] -> [RootViewGroup] 
    -> [ChildViewGroup] -> [TargetView]
  1. 自上而下传递询问是否拦截
  2. 自下而上传递询问是否处理
  3. 确定目标后直接传递给目标View

详细图解:

常见场景分析

场景1:点击按钮

  1. Activity 收到事件,传递给 Window
  2. DecorView 的 ViewGroup 开始分发
  3. 遍历子View找到按钮View
  4. 按钮的 onTouchEvent 返回 true 消费事件

场景2:滑动冲突

// 解决滑动冲突示例:外部拦截法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercepted = false; // 必须不拦截DOWN
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要当前事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    return intercepted;
}

场景3:自定义事件处理

// 自定义View处理双击事件
private GestureDetector mGestureDetector;

public MyView(Context context) {
    mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            // 处理双击
            return true;
        }
    });
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return mGestureDetector.onTouchEvent(event);
}

性能优化建议

  • 减少视图层级 - 层级越深,分发路径越长
  • 避免过度拦截 - 只在必要时使用 onInterceptTouchEvent
  • 使用 TouchDelegate - 扩大小View的点击区域
  • 合理使用 requestDisallowInterceptTouchEvent - 子View阻止父View拦截

多点触控处理

Copy
@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getActionMasked();
    int pointerIndex = event.getActionIndex();
    int pointerId = event.getPointerId(pointerIndex);
    
    switch (action) {
        case MotionEvent.ACTION_POINTER_DOWN:
            // 非第一个手指按下
            break;
        case MotionEvent.ACTION_POINTER_UP:
            // 非最后一个手指抬起
            break;
    }
    return true;
}

两列表嵌套滑动

横向和纵向列表,滑动冲突解决。

例如横向列表内部嵌套了一个纵向列表,对外部ReccyclerView的onInterceptTouchEvent方法进行拦截,然后判断内部RecyclerView是否需要拦截,需要则拦截。

public class HorizontalRecyclerView extends RecyclerView {
    private float startX, startY;
    
    public HorizontalRecyclerView(Context context) {
        super(context);
    }
    
    public HorizontalRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = e.getX();
                startY = e.getY();
                // 必须不拦截DOWN,否则子View无法收到后续事件
                return false;
                
            case MotionEvent.ACTION_MOVE:
                float endX = e.getX();
                float endY = e.getY();
                float distanceX = Math.abs(endX - startX);
                float distanceY = Math.abs(endY - startY);
                
                // 横向滑动距离大于纵向,且角度小于30度时拦截事件
                if (distanceX > distanceY && distanceX > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
                    return true; // 拦截事件,父RecyclerView处理
                }
                break;
        }
        return super.onInterceptTouchEvent(e);
    }
}

AIDL & Binder基础原理

Android中的Binder和AIDL是实现跨进程通信(IPC)的关键机制。

Binder通信原理

架构组成

  • 用户空间:应用程序和服务运行的地方,通过Binder提供的接口进行通信。
  • 内核空间:包含Binder驱动,负责处理进程间的数据传递和同步。
  • Binder驱动:管理Binder操作的核心组件,处理通信请求,管理内存映射,确保数据正确传递。

工作流程

  • 服务注册:服务端创建Binder对象并注册到ServiceManager,表明可接受请求。
  • 客户端查找服务:客户端通过ServiceManager查询服务,获取服务端Binder代理对象。
  • 建立通信通道:Binder在客户端和服务端之间建立通信通道,通过共享内存区域直接访问数据。
  • 数据传输:客户端调用服务端接口方法,Binder将方法调用和参数打包成数据,通过共享内存传递给服务端,服务端处理后返回结果。

通信细节

  • 基于内存映射:Binder通信基于内存映射(mmap)实现,通过映射内存区域,数据传输只需一次复制,提高性能。
  • 线程池机制:服务端通过Binder线程池处理IPC请求,线程池动态扩展、复用线程,提升并发能力。

AIDL通信原理

  • 定义与作用
  • AIDL(Android Interface Definition Language)是Android提供的一种用于定义跨进程通信接口的语言。
  • 通过AIDL,可以定义客户端和服务端之间通信的接口和方法,实现跨进程调用。

工作流程

  • 定义AIDL接口:创建AIDL文件,定义接口和方法。
  • 实现AIDL接口:服务端实现AIDL接口,创建Binder对象,并在Service中返回该Binder对象。
  • 客户端绑定服务:客户端通过bindService绑定服务,获取服务端返回的Binder代理对象。
  • 调用接口方法:客户端通过Binder代理对象调用服务端的方法,Binder机制负责底层通信。

关键组件

  • Stub类:服务端实现的Binder类,继承自AIDL生成的Stub类。
  • Proxy类:客户端的Binder代理类,用于代理服务端的方法调用。
  • Parcel:用于封装和传输数据,支持多种数据类型,高效且轻量。

总结

  • Binder是Android跨进程通信的核心机制,通过内核驱动和内存映射实现高效通信。
  • AIDL是基于Binder实现的通信接口定义语言,简化了跨进程通信的开发流程,使客户端和服务端能够以面向对象的方式进行通信。

Service 的两种启动方式(startService 和 bindService)区别?

BroadcastReceiver 的动态注册和静态注册区别?

ContentProvider 的作用?如何实现跨进程数据共享?

插件化原理?如何实现热修复?

如果让你优化一个卡顿的页面,你会从哪些方面入手?

如何保证 App 的稳定性?(Crash 监控、异常捕获)

【Android进阶】Android热门依赖库知识点总结

【Android进阶】Android热门依赖库知识点总结

本文介绍了在Android应用层开发过程中,日常使用的重要的依赖库知识点总结梳理

官方组件

ViewModel & Lifecycle

onSaveInstanceState 存储数据原理

onSaveInstanceState 是 Android 中用于临时保存 Activity 或 Fragment 状态的重要机制,主要用于应对系统配置变更(如屏幕旋转)或系统资源回收等情况。

  • 当 Activity 可能被销毁时(如配置变更或后台回收),系统会调用 onSaveInstanceState(Bundle outState)
  • 开发者可以重写此方法,将需要保存的状态数据存入提供的 Bundle 对象
  • 当 Activity 重新创建时,系统会将保存的 Bundle 传递给 onCreate(Bundle savedInstanceState) 或 onRestoreInstanceState(Bundle savedInstanceState)
  • 内部使用 ArrayMap 实现键值存储,支持基本数据类型、String、Parcelable 和 Serializable 对象,数据会被序列化为字节流
  • 系统使用 Binder 事务缓冲区传输这些数据(大小限制约1MB)
  • 数据临时保存在系统进程中,不是持久化存储,进程终止后数据会丢失
  • onSaveInstanceState 被调用的频率可能增加

ViewModel 存数据的原理

ViewModel 是 Android Jetpack 架构组件的一部分,它能在配置变更(如屏幕旋转)时保留数据,但在应用进程被完全终止时数据会丢失。

  • 每个 Activity/Fragment 拥有一个 ViewModelStore 实例
  • ViewModelStore 内部使用 HashMap 存储 ViewModel 实例
  • 配置变更时,ViewModelStore 被保留在内存中
  • ViewModel 的生命周期比创建它的 Activity/Fragment 更长
  • 当 Activity 因配置变更销毁重建时,ViewModel 不会被清除,当 Activity 真正完成(finish())时,ViewModel 会被清除

底层实现原理 ViewModelProvider 工作机制:

// 获取 ViewModel 的简化流程
ViewModelProvider(owner).get(MyViewModel::class.java)

// 内部实现关键步骤:
1. 检查 ownerActivity/Fragment ViewModelStore
2. 如果不存在对应 ViewModel 实例则通过 Factory 创建新实例
3. 将新实例存入 ViewModelStore  HashMap

配置变更可以保存数据原理

配置变更场景:

  • 当 Activity 因配置变更被销毁时,系统保留了一个特殊的”非配置实例”
  • 这个非配置实例持有 ViewModelStore
  • 新创建的 Activity 实例会获取前一个实例的 ViewModelStore

ViewModeProvider 的作用

  • 负责实例化 ViewModel 对象
  • 确保不会重复创建相同的 ViewModel 实例
  • 将 ViewModel 与特定的 Activity/Fragment 生命周期关联
  • 确保 ViewModel 在配置变更时不会被销毁
  • 与 ViewModelStore 协作管理 ViewModel 实例的存储

ViewModel 的工厂模式(ViewModelProvider.Factory)有什么作用

ViewModelProvider.Factory 是 ViewModelProvider 的工厂接口,用于创建 ViewModel 实例。

  • 当 ViewModelProvider 找不到已存在的 ViewModel 实例时,会调用 Factory 的 create 方法创建新实例
  • 可以通过实现 Factory 接口,根据不同的构造参数创建不同的 ViewModel 实例
  • 可以通过依赖注入框架(如 Dagger、Hilt)来提供 Factory 的实现,实现 ViewModel 的依赖注入
  • 可以通过自定义的 ViewModelFactory 来实现不同的 ViewModel 实例创建逻辑,如根据不同的参数创建不同的 ViewModel 实例

Lifecycle 工作机制

Lifecycle 是 Android Jetpack 架构组件的一部分,用于管理 Activity 和 Fragment 的生命周期。

  • 每个 Activity/Fragment 都有一个 LifecycleRegistry 实例
  • LifecycleRegistry 内部维护了一个 LifecycleOwner 和 Lifecycle 的对应关系
  • 当 Activity/Fragment 生命周期状态改变时,LifecycleRegistry 会通知所有的 LifecycleObserver
  • LifecycleObserver 可以通过注解或手动注册来监听特定的生命周期事件

底层实现原理 LifecycleRegistry 工作机制:

// 生命周期状态变化时的简化流程
1. 调用 LifecycleRegistry.markState(Lifecycle.State.CREATED)
2. 遍历所有 LifecycleObserver调用其对应的生命周期方法 onStart()

// 内部实现关键步骤:
1. 维护一个 LifecycleOwner  Lifecycle 的对应关系
2.  LifecycleOwner 生命周期状态改变时遍历所有 LifecycleObserver调用其对应的生命周期方法

ViewModel如何感知生命周期

ViewModel 的生命周期感知主要依赖于以下组件协作:

  • ViewModelStoreOwner:Activity/Fragment 实现的接口,提供 ViewModelStore
  • ViewModelStore:实际存储 ViewModel 实例的容器
  • LifecycleOwner:Activity/Fragment 提供的生命周期来源

Fragment共享viewmodel

Fragment 间共享数据, 实际使用中,可以通过以下方式来实现:

// 多个 Fragment 通过 activity 获取同一个 ViewModel
val sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)

LiveData & Flow

什么是 LiveData?它的主要特点是什么?

  • LiveData 是一种可观察的数据持有者类,具有生命周期感知能力
  • 特点:生命周期感知、自动更新UI、避免内存泄漏、数据始终保持最新状态

LiveData 与 RxJava/Observable 有什么区别?

  • LiveData 是生命周期感知的,专为 Android 设计
  • RxJava 更强大但更复杂,需要手动管理订阅生命周期
  • LiveData 自动处理订阅和取消订阅

LiveData 的生命周期感知是如何实现的?

  • 通过 LifecycleOwner 关联组件生命周期
  • 在 STARTED 或 RESUMED 状态时激活观察者
  • 在 DESTROYED 状态时自动移除观察者

LiveData 的观察者模式是如何工作的?

  • 使用 Observer 接口注册观察者
  • 数据变化时通知处于活跃状态的观察者
  • 新观察者注册时会立即收到当前值

LiveData 的 setValue() 和 postValue() 有什么区别?

  • setValue() 必须在主线程调用
  • postValue() 可以在后台线程调用,内部会切换到主线程

LiveData 如何保证数据不丢失?

  • 新观察者注册时会立即收到最后一次的数据
  • 配置更改时不会丢失数据(与 ViewModel 配合)

如何合并多个 LiveData 源?

  • 使用 MediatorLiveData 可以观察多个 LiveData 源
  • 将多个源添加为 MediatorLiveData 的源

在 Repository 层应该返回 LiveData 吗?

  • 不建议,Repository 应保持框架无关性
  • 建议返回 suspend 函数或 Flow,由 ViewModel 转换为 LiveData

LiveData 在 View 和 ViewModel 之间如何分工?

  • ViewModel 暴露 LiveData
  • View(Activity/Fragment) 观察 LiveData 并更新UI
  • 业务逻辑应放在 ViewModel 中

LiveData 与数据绑定(Data Binding)如何配合?

  • 直接在布局文件中绑定 LiveData
  • 需要设置生命周期所有者 binding.lifecycleOwner = this

LiveData 与 StateFlow/SharedFlow 如何选择?

  • LiveData 适合 Android UI 层,简单场景
  • StateFlow/SharedFlow 适合复杂数据流,跨平台逻辑层
  • LiveData 自动生命周期感知,Flow 需要手动收集

Room

DataStore & SharedPreferences & MMKV

SP

通过xml文件存储。 优点:

  • Android 原生支持,无需额外依赖
  • 简单易用,API 直观
  • 适合存储少量简单数据(键值对)

缺点:

  • 同步API,可能导致主线程阻塞
  • 不支持跨进程
  • 没有类型安全
  • 性能较差,特别是大数据量时
  • 不支持异常处理,可能丢失数据
  • 全量写入,即使只修改一个值也要重写整个文件

DataStore

Google推荐的替代SharedPreferences的方案。

  • 异步API(基于Kotlin协程/Flow)
  • 类型安全(通过Protobuf)
  • 更好的错误处理机制
  • 不会阻塞UI线程
  • 支持数据一致性保证

MMKV

腾讯开源的高性能KV存储框架。

  • 基于内存映射,读写速度快
  • 支持多种加密方式
  • 支持多进程
  • 自动增量更新,效率高
  • 支持加密
  • 支持多种数据类型
  • 微信团队开发,经过大规模验证
内存映射技术

内存映射技术是一种将文件内容映射到内存中的技术。

  • 内存映射允许应用程序直接读写文件内容,而无需通过传统的文件读写操作
  • 内存映射减少了文件读写的系统调用次数,提高了读写效率
  • 内存映射将文件内容映射到内存中,减少了数据的拷贝操作,提高了数据访问速度

Lottie

  • Lottie 通过 Android 的 Canvas API 逐帧绘制矢量图形
  • 利用 View 的硬件加速层(通过 setLayerType(LAYER_TYPE_HARDWARE, null))提升性能
  • ValueAnimator:核心动画引擎,根据时间插值计算动画进度
  • JSONObject/JSONArray:解析 After Effects 导出的动画 JSON 文件
  • 对包含位图资源的动画,使用 BitmapFactory 解码
  • 在后台线程(通过 HandlerThread)解析动画 JSON 避免主线程阻塞,通过 Handler 将渲染结果同步到 UI 线程
  • 使用 LruCache 缓存常用动画,避免重复解析
  • 对非活跃动画采用弱引用策略减少内存占用

ViewBinding & DataBinding

ViewBinding 是 Android 官方提供的一种类型安全的视图绑定机制,它通过在编译时生成绑定类来替代传统的 findViewById() 方法。

Android Gradle 插件会:

  • 解析布局XML文件:处理所有包含 merge 或根标签的布局文件
  • 生成绑定类:为每个布局文件生成对应的绑定类(如 ActivityMainBinding)
  • 优化代码:移除未使用的绑定引用

运行时工作流程:

  • 调用生成的 inflate() 方法
  • 内部使用 LayoutInflater 加载布局
  • 调用 bind() 方法进行视图绑定
  • 对布局中的每个带有ID的视图执行一次 findViewById()
  • 将找到的视图引用保存在绑定类的字段中
  • 返回绑定类实例

优点:

  • 类型安全:绑定类中包含所有视图的强类型引用
  • 内部有缓存机制,避免重复查找
  • 避免了 findViewById() 的调用,提高了性能
  • 简化了代码编写,减少了错误

缺点:

  • 编译时生成绑定类,可能会增加 APK 大小
  • 对于复杂布局,可能会导致生成的绑定类过于庞大

DataBinding 是 Android 官方提供的数据绑定框架,它允许开发者以声明式方式将布局与数据源绑定,自动同步 UI 和数据变化。

编译过程会执行以下操作:

  • 布局文件预处理,解析所有包含 layout 标签的 XML 文件.
  • 将普通布局转换为 DataBinding 专用格式(添加绑定桥梁)
  • 生成 BR 类(类似 R 类,用于绑定资源)
  • 为每个绑定布局生成对应的绑定类(如 ActivityMainBindingImpl)
  • 生成 ViewDataBinding 的子类
  • 实现观察者模式和绑定逻辑

双向绑定实现:

<EditText android:text="@={user.name}"/>
  • 设置文本变化监听器
  • 数据变化时更新UI
  • UI变化时更新数据源

ViewPager2

Gson & Moshi & KotlinX Serialization

Proguard

Jetpack Compose

三方组件

RecyclerView

用途

  • 大数据集合展示: RecyclerView适用于展示大量数据,通过ViewHolder的复用机制减少内存消耗。
  • 复杂布局: 支持不同的LayoutManager,可以实现线性、网格、瀑布流等多种复杂布局。
  • 滑动性能优化: 通过异步加载和局部刷新等手段,提升滑动的流畅度。

核心组件

  • Recycler:是构建自定义的布局管理器的核心帮助类,几乎干了所有的获取视图、缓存视图等和回收相关的活动。能让RecyclerView能快速的获取一个新视图来填充数据或者快速丢弃不再需要的视图。
  • Adapter:是所有数据的来源,负责提供数据并创建ViewHolders以及将数据绑定到ViewHolders上的重要组件,可以视为是recycler对外的工作的对接者
  • ViewHolder:存取状态信息,在recycler内部也对viewHolder进行了状态信息的存取(是否正在被改变,是否被删除或添加)
  • LayoutManager:决定RecyclerView中Items的排列方式,包含了Item View的获取与回收;当数据集发生变化时,LayoutManager会自动调整子view的位置和大小,以保证RecyclerView的正常显示和性能。

RecyclerView的View缓存机制

  • ViewHolder模式: RecyclerView使用ViewHolder模式来缓存视图。当ItemView滑出屏幕时,对应的ViewHolder会被缓存,而不是立即销毁。当需要新的ItemView时,可以从缓存中获取ViewHolder,避免频繁的View创建和销毁。
  • Recycler池: RecyclerView通过Recycler池来管理缓存的ViewHolder。Recycler池中维护了一个可回收的ViewHolder队列,通过这个池来快速获取可重用的ViewHolder。
  • 复用机制: 当新的数据需要显示时,RecyclerView会调用Adapter的onBindViewHolder方法,将新的数据绑定到已存在的ViewHolder上,而不是创建新的View。
  • Scrap缓存: 在RecyclerView内部还有一个Scrap缓存,用于存储一些没有被完全废弃的ItemViews。这个缓存用于快速重用视图,减少了ViewHolder的创建和初始化时间。

Retrofit + Okhttp

Retrofit的设计模式

动态代理机制

核心流程:

public <T> T create(final Class<T> service) {
    return (T) Proxy.newProxyInstance(
        service.getClassLoader(),
        new Class<?>[] { service },
        new InvocationHandler() {
            @Override public Object invoke(Object proxy, Method method, Object[] args) {
                // 将接口方法转换为HTTP请求
                return loadServiceMethod(method).invoke(args);
            }
        });
}
  • 通过Java动态代理技术生成API接口的实现
  • 方法调用时,将注解配置转换为HTTP请求参数
  • 最终通过OkHttp执行实际网络请求

Java的动态代理是一种在运行时动态创建代理类和代理对象的能力。它不需要在编译时就知道具体的代理类,而是通过反射机制在程序运行过程中生成代理类和它的实例。

动态代理的优点

(1)解耦: 将横切关注点(如日志、事务、权限等)从业务逻辑中分离出来,提高了代码的内聚性和可维护性。(2)灵活性: 可以在运行时动态地为对象添加功能,无需修改原有代码。(3)复用性: 相同的代理逻辑可以应用于不同的接口和类。(4)AOP(面向切面编程)的基础: 许多AOP框架(如Spring AOP)底层都使用了动态代理来实现切面功能。

建造者模式 (Builder Pattern)

应用场景:Retrofit 实例的配置和创建

典型实现:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .client(okHttpClient)
    .build();

优势:

  • 支持链式调用,配置灵活
  • 隔离复杂对象的创建过程
  • 保证构建过程的一致性

适配器模式 (Adapter Pattern)

应用场景:Call 到其他类型的转换

核心组件:

CallAdapter.Factory:适配器工厂基类

RxJavaCallAdapterFactory:将 Call 适配为 RxJava 的 Observable

CoroutineCallAdapterFactory:将 Call 适配为 Kotlin 协程的 suspend 函数

工作流程:

OkHttpCall --> CallAdapter.adapt() --> Observable/SuspendFunction/其他类型

工厂方法模式 (Factory Method Pattern)

应用场景:Converter 和 CallAdapter 的创建

实现示例:

public interface Converter.Factory {
    // 根据类型创建转换器
    Converter<ResponseBody, ?> responseBodyConverter(
        Type type, Annotation[] annotations, Retrofit retrofit);
    
    // 根据类型创建请求体转换器
    Converter<?, RequestBody> requestBodyConverter(
        Type type, Annotation[] annotations, Retrofit retrofit);
}

派生工厂:

GsonConverterFactory
MoshiConverterFactory
ScalarsConverterFactory

装饰者模式 (Decorator Pattern)

应用场景:OkHttpCall 的封装

实现方式:

final class OkHttpCall<T> implements Call<T> {
    private final ServiceMethod<T, ?> serviceMethod;
    private final @Nullable Object[] args;
    
    // 装饰原始的OkHttp Call
    private @Nullable okhttp3.Call rawCall;
}

作用:

  • 在不改变原有 Call 接口的情况下增强功能
  • 添加了缓存、线程切换等附加功能

策略模式 (Strategy Pattern)

应用场景:HTTP 请求方法的处理

实现体现:

  • 每个 HTTP 注解(@GET/@POST 等)对应不同的请求策略
  • RequestBuilder 根据注解选择不同的参数处理方式

示例:

void parseMethodAnnotation(Annotation annotation) {
    if (annotation instanceof GET) {
        parseHttpMethodAndPath("GET", ((GET) annotation).value());
    } else if (annotation instanceof POST) {
        parseHttpMethodAndPath("POST", ((POST) annotation).value());
    }
    // 其他HTTP方法...
}

观察者模式 (Observer Pattern)

应用场景:通过 Callback 处理异步响应

典型实现:

call.enqueue(new Callback<User>() {
    @Override public void onResponse(Call<User> call, Response<User> response) {
        // 成功回调
    }
    
    @Override public void onFailure(Call<User> call, Throwable t) {
        // 失败回调
    }
});

责任链模式 (Chain of Responsibility)

应用场景:OkHttp 的拦截器体系

Retrofit 中的集成:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new LoggingInterceptor())
    .addNetworkInterceptor(new StethoInterceptor())
    .build();

处理流程:

请求 → 拦截器1 → 拦截器2 → ... → 拦截器N → 服务器
响应 ← 拦截器N ← ... ← 拦截器2 ← 拦截器1 ← 

设计模式综合应用示例

// 建造者模式创建Retrofit实例
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(new OkHttpClient.Builder() // 装饰者模式增强OkHttpClient
        .addInterceptor(new HttpLoggingInterceptor()) // 责任链模式
        .build())
    .addConverterFactory(new GsonConverterFactory()) // 工厂方法模式
    .build();

// 动态代理模式创建API实例
GitHubService service = retrofit.create(GitHubService.class);

// 适配器模式将Call转换为RxJava Observable
Observable<List<Repo>> observable = service.listRepos("user");

// 观察者模式处理响应
observable.subscribe(new Observer<List<Repo>>() {
    @Override public void onSubscribe(Disposable d) {}
    
    @Override public void onNext(List<Repo> repos) {
        // 处理数据
    }
    
    @Override public void onError(Throwable e) {
        // 错误处理
    }
});

Retrofit 通过精心组合这些经典设计模式,实现了简单易用与强大功能之间的完美平衡,成为 Android 网络请求的事实标准。理解这些设计模式有助于开发者更好地使用和扩展 Retrofit。

OKhttp的优势

  • 拦截器机制: OkHttp通过拦截器链来处理请求和响应。每个拦截器可以在请求或响应被处理前或后进行自定义操作。
  • 连接池: OkHttp使用连接池来管理HTTP连接,减少了创建和销毁连接的开销。
  • 缓存机制: OkHttp支持缓存机制,通过缓存策略来控制是否使用缓存。
  • 异步请求: OkHttp支持异步请求,通过回调或协程来处理请求结果。
  • 支持HTTPS: OkHttp支持HTTPS协议,通过SSL/TLS加密来保护数据安全。
  • 支持GZIP压缩: OkHttp支持GZIP压缩,通过压缩请求和响应数据来减少网络传输的数据量。

HTTP 和 HTTPS 的区别?HTTPS 的加密流程?

Ktor

Ktor 是 JetBrains 推出的异步网络框架,支持全栈开发(客户端+服务端),基于 Kotlin 协程设计,100% Kotlin 原生支持。而且随着KMP技术发展,Ktor也已经支持跨平台网络请求。

核心特性:

  • 协程优先:所有API设计为挂起函数
  • 轻量级:模块化设计,按需引入组件
  • 多平台支持:Android、iOS、JVM、JavaScript、Native
  • 插件化架构:通过安装(install)功能扩展能力

和Retrofit对比

  • 协程支持:Ktor 支持 Kotlin 协程,简化异步编程
  • 功能丰富:Ktor 提供了更多的功能,如文件上传、WebSocket、服务器端渲染等
  • 跨平台支持:Ktor 支持跨平台开发,如 Android、iOS、JVM、JavaScript、Native
  • 社区活跃:Ktor 社区活跃,有丰富的文档和示例

CIO引擎

对比OkHttp:

特性CIO 引擎OkHttp 引擎
实现语言100% KotlinJava + Kotlin
线程模型协程事件循环线程池 + NIO
内存占用更低 (~1/3 of OkHttp)较高
HTTP/2支持需要手动启用默认支持
WebSocket基础支持更成熟实现
平台支持全平台 (包括Native)仅JVM/Android
DNS解析纯Kotlin实现依赖系统实现

Hilt & Dagger & Koin 依赖导入

依赖注入

依赖注入是一种设计模式,用于将对象的依赖从内部移到外部进行管理。它的主要好处包括:

解耦代码:通过接口或抽象类注入依赖,降低模块之间的耦合度。 提升测试性:可以使用 Mock 对象注入,方便单元测试。 易于维护:通过集中管理依赖关系,减少手动创建对象的复杂性。

三者简介

  • Dagger是一个用于管理依赖注入的框架,它通过编译时生成代码来实现依赖注入。
  • Hilt是一个用于简化Android依赖注入的框架,它基于Dagger,并提供了一些额外的功能,如简化依赖注入的配置。
  • Koin是一个轻量级的依赖注入框架,它支持Kotlin语言,并提供了一些额外的功能,如简化依赖注入的配置。

写法举例

Dagger

@Module
class NetworkModule {
    @Provides
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()
}

@Component(modules = [NetworkModule::class])
interface AppComponent {
    fun inject(activity: MainActivity)
}

// 在Application中初始化
class MyApp : Application() {
    val appComponent = DaggerAppComponent.create()
}

Hilt

@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var okHttpClient: OkHttpClient
}

// Application类需添加注解
@HiltAndroidApp
class MyApp : Application()

Koin

val appModule = module {
    single { OkHttpClient.Builder().build() }
}

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidContext(this@MyApp)
            modules(appModule)
        }
    }
}

class MainActivity : AppCompatActivity() {
    private val okHttpClient: OkHttpClient by inject()
}

对比表格

概念Dagger 2HiltKoin
工作原理编译时代码生成基于Dagger的预配置Android方案运行时依赖解析
配置方式手动创建Component/Module注解驱动+预定义Android组件DSL声明式配置
编译时间影响显著(需kapt处理)显著(继承自Dagger)
运行时性能最佳(编译期解决依赖)同Dagger轻微开销(运行时解析)

Glide & Coil

RxJava

EventBus

LeakCanary

LeakCanary 是 Square 公司开发的一款 Android 内存泄漏检测工具,它能够自动检测应用中的内存泄漏并生成详细的泄漏轨迹报告。

  • LeakCanary 基于 Java 的 WeakReference(弱引用) 和 ReferenceQueue(引用队列) 机制

工作原理流程图:

graph TD
    A[监控对象] --> B[创建KeyedWeakReference]
    B --> C[添加到引用队列]
    D[触发GC] --> E[检查引用队列]
    E -->|对象仍在队列外| F[判定为泄漏]
    E -->|对象进入队列| G[判定为已回收]

LeakCanary 默认监控以下Android组件:

  • 销毁的Activity
  • 销毁的Fragment
  • 销毁的View
  • 销毁的ViewModel

泄漏检测触发时机

  • Activity/Fragment销毁时:通过注册 Application.ActivityLifecycleCallbacks
  • View/ViewModel销毁时:通过各自的销毁回调
  • 手动监控:AppWatcher.objectWatcher.watch(myObject)

泄漏分析过程

  • 当检测到可能的内存泄漏时,调用 Debug.dumpHprofData() 生成堆转储文件
  • 使用 Shark 解析器分析堆转储(替代旧版HAHA)
  • 查找泄漏对象的引用链
  • 识别最短强引用路径(从GC Roots到泄漏对象)
  • 排除系统类引用(过滤噪声)

核心优化技术

  1. 性能优化措施
    • 延迟检测:默认5秒后执行检测(等待主线程空闲)
    • 采样分析:在Debug版本中全量分析,Release版本抽样
    • 后台线程:所有分析操作在独立线程执行
  2. 准确性保障
    • 多次GC验证:确保对象真的无法被回收
    • 引用链验证:确认泄漏路径的有效性
    • 排除软/弱引用:只关注强引用泄漏

常见泄漏场景处理:

  • 单例持有Context:改用Application Context
  • Handler内存泄漏:使用静态Handler+WeakReference
  • 匿名内部类持有:改为静态内部类

LeakCanary 通过巧妙的弱引用监控和堆转储分析技术,为Android开发者提供了简单高效的内存泄漏检测方案,极大提升了内存问题排查效率。

Kotlin

lateinit 和 by lazy 的区别

lateinit(延迟初始化)

  • 用于可变的 var 属性
  • 必须显式初始化后才能访问
  • 不能用于基本数据类型(Int, Boolean等)
  • 编译时不检查初始化状态,运行时检查

by lazy(延迟加载)

  • 用于不可变的 val 属性
  • 首次访问时自动初始化
  • 可以配置属性,自定义初始化逻辑
  • 可以用于基本数据类型

实现原理

lateinit

  • 编译时生成额外代码
  • 获取时,自动检查初始化状态
  • 生成一个访问器方法,用于获取属性

by lazy

  • 使用委托模式
  • 内部维护初始化状态和值
  • 首次访问时,生成一个访问器方法
  • 该方法内部使用 synchronized 确保线程安全
  • 可以配置为非线程安全模式,提升速度

协程

见更全面的总结文章: Kotlin协程浅谈

apply & with & let & run

Kotlin 提供了几个强大的作用域函数(scope functions),它们都用于在对象的上下文中执行代码块,但在使用方式和场景上有所不同。

函数上下文对象引用返回值是否扩展函数典型使用场景
letitLambda 结果非空对象处理、链式操作
runthisLambda 结果对象配置、计算返回值
withthisLambda 结果对象分组操作
applythis对象本身对象初始化
alsoit对象本身附加效果、日志记录

let

// 安全调用非空对象
val length = nullableString?.let { 
    println("Processing: $it")
    it.length  // 返回值
} ?: 0

// 链式操作
user?.let { it.copy(name = "NewName") }?.let(::print)
  • 通过 it 引用对象
  • 返回 lambda 表达式结果
  • 常用于空安全检查

run

val result = service.run {
    port = 8080  // 直接访问属性
    query()      // 直接调用方法
    // 最后一行作为返回值
}

// 替代构造器模式
val rectangle = Rectangle().run {
    width = 10
    height = 20
    area()  // 返回面积
}
  • 通过 this 引用对象(可省略)
  • 返回 lambda 表达式结果
  • 适合对象转换和计算

with

val output = with(StringBuilder()) {
    append("Hello")
    append(" ")
    append("World")
    toString()  // 返回结果
}
  • 非扩展函数
  • 通过 this 引用对象
  • 返回 lambda 表达式结果
  • 适合对同一对象进行多次操作

apply

val myView = TextView(context).apply {
    text = "Hello"
    textSize = 16f
    setPadding(10, 0, 10, 0)
    // 返回TextView对象本身
}

// 替代Builder模式
val intent = Intent().apply {
    action = "ACTION_VIEW"
    data = Uri.parse("https://kotlinlang.org")
}
  • 通过 this 引用对象
  • 返回对象本身
  • 主要用于对象初始化

also

val numbers = mutableListOf(1, 2, 3).also {
    println("Before adding: $it")
    it.add(4)
}.also {
    println("After adding: $it")
}
  • 通过 it 引用对象
  • 返回对象本身
  • 适合执行附加操作

数据类

Kotlin编译时移动生成了hashcode equals toString copy等方法。

底层实现:编译器生成的数据类大致如下:

public final class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return age == user.age && name.equals(user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }

    public User copy(String name, int age) {
        return new User(name, age);
    }

    public String component1() {
        return name;
    }

    public int component2() {
        return age;
    }
}
  • 附加功能:数据类支持解构声明,可以将对象的属性直接解构到多个变量中。

扩展函数原理

Kotlin 允许你为现有类添加新功能,而不需要继承这个类或使用装饰者模式。通过扩展函数,你可以直接在类外部为它添加新方法。 示例:

fun String.hello() = "Hello, $this"
println("World".hello())  // 输出: Hello, World

解析:

  • 简化的语法:扩展函数可以让你对现有类添加自定义功能,而无需修改类的定义。比如上例中的hello()函数,并不是String类的内置方法,但你可以像调用内置方法一样使用它。
  • 底层实现:扩展函数在编译时被转换为静态方法,类的实例作为第一个参数传递。
public static String hello(String receiver) {
     return "Hello, " + receiver;
}

Lambda

  • 简化的语法:Lambda 表达式是一种简洁的函数表示形式,可以用更少的代码来表达相同的逻辑。它在 Kotlin 中被广泛用于集合操作、异步编程等场景。
  • 底层实现:Lambda 表达式在编译时被转换为匿名函数类的实例。Kotlin 提供了一组函数接口,如 Function1、Function2,分别表示接受 1 个参数、2 个参数的函数。
    Function2<Integer, Integer, Integer> sum = new Function2<Integer, Integer, Integer>() {
      @Override
      public Integer invoke(Integer a, Integer b) {
          return a + b;
      }
    };
    int result = sum.invoke(1, 2);
    
  • 捕获变量:如果 Lambda 表达式在定义时捕获了外部变量,那么编译器会生成一个闭包类来封装这些变量。

When关键字

  • 简化的语法:when 表达式比传统的 switch 语句更强大,它不仅可以匹配值,还可以匹配条件,甚至使用复杂的表达式。此外,它也可以返回一个结果。
  • 底层实现:编译器会将 when 表达式转换为一系列条件检查(if-else 语句),或者在可能的情况下转换为 Java 字节码中的 switch 语句。
val result = when (x) {
    1 -> "One"
    2 -> "Two"
    else -> "Unknown"
}

String result;
if (x == 1) {
    result = "One";
} else if (x == 2) {
    result = "Two";
} else {
    result = "Unknown";
}

?空安全

转换成Java实际就是一个null检查,如何配合?:操作符,就是三目运算符后面赋一个默认值

委托

在 Kotlin 中,委托(Delegation) 是一种强大的设计模式,它允许对象将部分功能委托给另一个辅助对象来实现。Kotlin 原生支持多种委托方式,主要分为以下几种:

  1. 类委托(Class Delegation) 通过 by 关键字,将类的接口实现委托给另一个对象,常用于 “装饰器模式” 或 “代理模式”。

示例:委托接口实现

interface Printer {
    fun print(message: String)
}

class DefaultPrinter : Printer {
    override fun print(message: String) {
        println("Default Printer: $message")
    }
}

// 委托给 printer 对象
class CustomPrinter(private val printer: Printer) : Printer by printer {
    // 可以覆盖部分方法
    override fun print(message: String) {
        println("Before Printing...")
        printer.print(message) // 调用委托对象的方法
        println("After Printing...")
    }
}

fun main() {
    val defaultPrinter = DefaultPrinter()
    val customPrinter = CustomPrinter(defaultPrinter)
    customPrinter.print("Hello, Kotlin!")
}

输出: Before Printing… Default Printer: Hello, Kotlin! After Printing…

适用场景:

  • 增强或修改现有类的行为(如日志、缓存、权限控制)。
  • 避免继承,使用组合代替。
  1. 属性委托(Property Delegation) Kotlin 提供标准库委托(如 lazy、observable),也可以自定义委托。

(1) lazy 延迟初始化

val lazyValue: String by lazy {
    println("Computed only once!")
    "Hello"
}

fun main() {
    println(lazyValue) // 第一次访问时计算
    println(lazyValue) // 直接返回缓存值
}

输出: Computed only once! Hello Hello

(2) observable 监听属性变化

import kotlin.properties.Delegates

var observedValue: Int by Delegates.observable(0) { _, old, new ->
    println("Value changed from $old to $new")
}

fun main() {
    observedValue = 10  // 触发回调
    observedValue = 20  // 再次触发
}

输出: Value changed from 0 to 10 Value changed from 10 to 20

(3) vetoable 可拦截修改

var positiveNumber: Int by Delegates.vetoable(0) { _, old, new ->
    new > 0  // 只有 new > 0 时才允许修改
}

fun main() {
    positiveNumber = 10  // 允许
    println(positiveNumber)  // 10
    positiveNumber = -5     // 拒绝修改
    println(positiveNumber)  // 仍然是 10
}

(4) 自定义属性委托

class StringDelegate(private var initValue: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Getting value: $initValue")
        return initValue
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Setting value: $value")
        initValue = value
    }
}

fun main() {
    var text by StringDelegate("Default")
    println(text)  // 调用 getValue
    text = "New Value"  // 调用 setValue
}

输出: Getting value: Default Default Setting value: New Value

字符串连接

编译后实际还是java的➕拼接

Object关键字

声明单例类、伴生对象(Companion Object)以及匿名对象。

object Singleton {
    val name = "Kotlin Singleton"
}

class MyClass {
    companion object {
        val instance = MyClass()
    }
}

作用为单例类 生成静态内部实例,私有构造器,确保单例

public final class Singleton {
    public static final Singleton INSTANCE = new Singleton();

    private Singleton() {
        // private constructor
    }

    public String getName() {
        return "Kotlin Singleton";
    }
}

【Android进阶】Android事件分发流程

【Android进阶】Android事件分发流程

本文介绍了 Android 系统触摸事件分发的流程

首先经由一个经典的ANR报错引入:

// 1. InputDispatcher 记录应用无响应
I/InputDispatcher: Application is not responding: Window{<Window Token> u0 com.your.package/com.your.package.YourActivity}.
    It has been 5001.0ms since event, 5001.0ms since wait started. 
    Reason: Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. 
    Wait queue length: 1. Wait queue head age: 5001.0ms.

// 2. WindowManagerService 记录输入事件调度超时
I/WindowManager: Input event dispatching timed out sending to com.your.package/com.your.package.YourActivity.
    Reason: Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago.

Application is not responding: Window{...} 指出了是哪个应用程序窗口发生了无响应。触摸事件到达已经超过 5 秒(ANR 默认超时时间)。

Reason: Waiting to send non-key event... 这就是核心原因。 它表示 InputDispatcher 正试图发送一个后续的触摸事件(比如 ACTION_MOVE 或 ACTION_UP),但是前一个事件(通常是 ACTION_DOWN)发送到这个窗口后,窗口还没有返回结果(即没有完成 Activity.dispatchTouchEvent() 的调用)。在 ANR 触发的 5 秒总时长中,这个等待事件在队列头部停留的时间已经超过了 500 毫秒(这个数值是内部用于判断是否阻塞的阈值,不是总的 ANR 超时)。

事件传递到Window之前

这部分在APP冷启动,点击Launcher图标的阶段有所介绍。直接复制过来:

Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReaderInputDispatcherInputReaderInputDispatcher 是跑在 SystemServer进程中的两个 native 循环线程,负责读取和分发 Input 事件。

  • InputReader 负责从 EventHub 里面把 Input事件 读取出来,然后交给 InputDispatcher 进行事件分发
  • InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装后,寻找并分发到目标窗口;

system_server 的native线程 InputReader 读取到了一个触控事件。它会唤醒 InputDispatcher 去进行事件分发,先放入 InboundQueue 队列中,再去 寻找处理事件的窗口 ,找到窗口后就会放入 OutboundQueue 队列,等待通过socket通信发送到 launcher应用 的窗口中,此时事件处于 WaitQueue 中,等待事件被处理,若5s内没有处理,就会向 systemserver 报ANR异常。

input_event

Launcher进程接收到之后,通过 enqueueInputEvent 函数放入 “aq” 本地待处理队列中,唤醒 UI线程deliverInputEvent 流程进行事件分发处理,具体交给界面window里的类型来处理。

从View布局树的根节点DecorView开始遍历整个View树上的每一个子View或ViewGroup界面进行事件的分发、拦截、处理的逻辑。

这次的触摸事件被消耗后,Launcher及时调用 finishInputEvent 结束应用的处理逻辑,再通过JNI调用到native层InputConsumer的 sendFinishedSignal 函数通知 InputDispatcher 事件处理完成,及时从 waitqueue 里移除待处理事件,避免ANR异常。

整个处理流程是按照责任链的设计模式进行

整体传递流程

应用程序在Activity创建之初,在 setView 方法中,就通过 WindowManager 发起Binder通信向 WMS 注册了自己的 Window 到系统进程中。后续有触摸事件时, InputDispatcher 会接收到来自 InputReaderMotionEvent 。InputDispatcher 会查询 WMS 持有的窗口列表,根据触摸事件的 X/Y 坐标,找出在触摸点下方的最顶层、可见且可接收输入的窗口。WMS 还会判断当前哪个窗口拥有输入焦点 (Input Focus)。对于非触摸事件(如按键),焦点窗口是首选目标。最终, InputDispatcher 确定了事件要发送到的目标窗口。

在此之前,会将这个触摸事件封装成一个 MotionEvent

  • Window (PhoneWindow)MotionEvent 最终会被发送给这个触摸点所对应的窗口Window)实例。在 Android 中,Activity 对应的 Window 实际是 PhoneWindow 的一个实例。PhoneWindowsuperDispatchTouchEvent() 方法是事件进入 View 体系的第一站。
  • DecorView (根 ViewGroup)PhoneWindow 会将事件传递给其所持有的 DecorView
    • DecorView 是一个 FrameLayout,它是 Activity 窗口的根 View。它包含了状态栏、标题栏(如果存在)以及 Activity 的实际内容区域。
    • DecorView 是一个 ViewGroup,它实现了 dispatchTouchEvent() 方法。
  • Activity 的 dispatchTouchEvent(): DecorView 在分发事件时,会调用 ActivitydispatchTouchEvent(MotionEvent ev) 方法。
    • 这是 Activity 参与事件分发的最早入口点。 Activity 的 dispatchTouchEvent() 方法通常会调用 getWindow().superDispatchTouchEvent(ev),最终会把事件重新传给 DecorViewdispatchTouchEvent()
  • View/ViewGroup 树的向下分发:
    • DecorView(作为根 ViewGroup)调用自己的 dispatchTouchEvent(),开始将事件向下分发给其子视图(即你通过 setContentView() 设置的布局)。
    • 事件沿着 View 树,从父 ViewGroup 经过 onInterceptTouchEvent() 检查,然后传给子 View/ViewGroupdispatchTouchEvent(),直到找到一个处理该事件的 View

总结

  • 触摸事件首先被系统发送给对应的 Window (PhoneWindow)。
  • Window 调用 Activity 的 dispatchTouchEvent()(Activity 获得第一次处理机会)。
  • Activity 的 dispatchTouchEvent() 又将事件交给 Window 的 DecorView
  • DecorView(作为 View 树的根)开始将事件向下传递给内部的 View/ViewGroup 控件,完成真正的 View 树内部分发。

所以,Activity 和 Window 都是事件进入 View 树之前的重要环节,它们之间有交替的关系。

源码分析待补齐

【Android进阶】Android设备开机流程

【Android进阶】Android设备开机流程

本文介绍了Android设备开机流程

Android设备的启动过程是一个复杂的多阶段过程,涉及硬件初始化、引导加载、操作系统内核加载、系统服务启动等多个环节。那么从上电开始,到显示出系统桌面,中间经历了哪些关键步骤呢?

硬件上电阶段

设备接通电源后, 电源管理芯片(Power Management IC) 首先开始工作。PMIC负责为各个硬件组件提供稳定的电压和电流。按照预设顺序依次激活CPU、内存等关键组件。

CPU初始启动

CPU从预设的复位向量(Reset Vector)地址开始执行。通常是芯片内部固化的一段非常小的启动代码(BootROM)。这段代码是芯片制造商预先写入的,不可修改

执行芯片内部BootROM代码

BootROM代码执行基本的硬件初始化,验证并加载下一阶段的引导程序(通常是从特定存储区域),实现安全验证(如验证Bootloader签名)

安全启动(Secure Boot)验证

现代Android设备都支持安全启动机制,BootROM会验证Bootloader的数字签名,只有签名验证通过的Bootloader才能被加载执行

Bootloader阶段

第一阶段Bootloader(Primary Bootloader)

通常存储在设备的只读存储区域(如eMMC的boot分区)

负责初始化基本硬件(如内存控制器、时钟等)

加载并验证第二阶段Bootloader

第二阶段Bootloader(Secondary Bootloader)

更复杂的引导程序(如U-Boot)

初始化更多硬件设备

提供基本的命令行交互界面(在开发模式下)

加载Linux内核和初始RAM磁盘(initrd)

Bootloader模式。设备启动时按特定组合键(如Volume+Power)可进入Bootloader模式。支持刷写新系统(fastboot模式),恢复出厂设置(recovery模式),选择启动分区(多系统切换)

Linux内核启动阶段

内核加载与初始化 Bootloader将压缩的Linux内核加载到内存,解压并跳转到内核入口点(start_kernel函数)

内核开始初始化:

  • 设置内存管理
  • 初始化中断控制器
  • 设置进程调度器
  • 初始化设备驱动

设备树(Device Tree)解析

ARM架构设备使用设备树(Device Tree)描述硬件配置,内核解析设备树文件(.dtb),了解设备硬件配置,根据设备树初始化相应硬件驱动

init进程启动

内核启动的第一个用户空间进程是init(pid=1)

init进程负责:

  • 解析init.rc脚本
  • 创建系统关键目录
  • 启动ueventd、logd等基础服务
  • 挂载文件系统

Android系统启动阶段

解析并执行init.rc和设备相关的 init.<device>.rc 脚本。

启动关键守护进程:

  • ueventd:处理设备事件
  • logd:日志服务
  • servald:SELinux相关服务
  • healthd:电池健康监控
  • debuggerd:调试服务

Zygote进程启动

init进程最终会启动Zygote进程,Zygote是Android应用的基础进程,特点:

  • 预加载常用Java类和资源
  • 包含一个Dalvik/ART虚拟机实例
  • 监听socket等待孵化新应用进程

System Server启动

Zygote会孵化出System Server进程。System Server是Android框架的核心,负责启动几乎所有系统服务:

  • ActivityManagerService(AMS)
  • PackageManagerService(PMS)
  • WindowManagerService(WMS)
  • PowerManagerService
  • 等等…

服务启动顺序

System Server按特定顺序启动服务:

  • 引导服务(Bootstrap Services):
    • ActivityManagerService
    • PowerManagerService
    • PackageManagerService
  • 核心服务(Core Services):
    • BatteryService
    • UsageStatsService
  • 其他服务(Other Services):
    • StatusBarManagerService
    • InputMethodManagerService

桌面环境启动阶段

System Server启动完成后,AMS会启动Launcher应用。Launcher是设备的桌面环境,负责:

  • 显示应用图标
  • 处理应用启动请求
  • 管理小部件和壁纸

Launcher加载并显示:

  • 主屏幕布局
  • 应用抽屉
  • 小部件
  • 壁纸

同时,还需要从PackageManager获取已安装应用列表,为每个应用创建快捷方式图标

最终用户界面

所有系统服务和桌面组件初始化完成后,设备显示完整的桌面环境,用户可以开始与设备交互

【Android进阶】Android平台Kotlin开发的一些知识点(初级到深入)

【Android进阶】Android平台Kotlin开发的一些知识点(初级到深入)

本文罗列了Android平台Kotlin开发的一些知识点(初级到深入)

Kotlin 已成为 Android 开发的首选语言,包括 Jetpack Compose 等现代 UI 框架。这份全面的面试题列表涵盖了 Kotlin 基础知识、使用 Kotlin 进行 Android 开发以及 Jetpack Compose,所有题目均按经验水平分类。


🟢 初级 (Beginner Level)

🧠 Kotlin 基础

  • Kotlin 是什么?它与 Java 有何不同?
  • Kotlin 的主要特点是什么?
  • Kotlin 如何处理空安全?
  • valvar 有什么区别?
  • 什么是可空类型 (nullable) 和非可空类型 (non-nullable)?
  • 什么是 Elvis 运算符 (?:)?
  • 解释安全调用运算符 (?.)。
  • !! 在 Kotlin 中有什么用?
  • 什么是扩展函数 (extension functions)?
  • 如何在 Kotlin 中编写一个简单的类?
  • 什么是数据类 (data class)?
  • 如何在 Kotlin 中定义属性?
  • 什么是 Kotlin 中的字符串模板 (string interpolation)?
  • 什么是默认参数 (default parameters) 和具名参数 (named parameters)?
  • Kotlin 如何支持函数式编程?

🔁 控制流 (Control Flow)

  • 解释 Kotlin 中的 ifwhenfor 循环。
  • when 为什么比 Java 中的 switch 更好?
  • whiledo-while 循环在 Kotlin 中如何工作?

🧩 函数 (Functions)

  • 什么是 Kotlin 中的 Lambda 函数?
  • 什么是高阶函数 (higher-order function)?
  • 什么是默认参数 (default arguments) 和具名参数 (named arguments)?
  • 什么是内联函数 (inline function)?
  • 什么是局部函数 (local function)?
  • 如何定义一个可变参数 (vararg)?
  • 如何在 Kotlin 中使用中缀表示法 (infix notation)?

📚 集合 (Collections)

  • ListSetMap 之间的区别是什么?
  • Kotlin 中的可变 (mutable) 和不可变 (immutable) 集合有什么区别?
  • 如何使用 Kotlin 过滤列表?
  • 什么是 mapfilterreduce
  • 如何在 Kotlin 中对列表进行排序?
  • flatMapmap 之间有什么区别?
  • associateBygroupBy 有什么用?

📱 Android 基础 (in Kotlin)

  • 如何使用 Kotlin 搭建一个基本的 Android 项目?
  • Kotlin 中的 Activity 是什么?
  • XML 在 Android UI 中扮演什么角色?
  • 如何使用 Intent 启动一个新的 Activity?
  • 什么是 findViewById 以及如何在 Kotlin 中使用 ViewBinding?
  • 什么是 RecyclerView?
  • AndroidManifest.xml 文件是做什么用的?
  • 什么是 Fragment 及其生命周期?
  • 如何使用 Kotlin 在 Activity 之间传递数据?
  • 如何在 Kotlin 中处理运行时权限?

🟡 中级 (Intermediate Level)

🧱 Kotlin 中的面向对象概念

  • Class 和 Data Class 有什么区别?
  • 什么是主构造函数 (primary constructor) 和次构造函数 (secondary constructor)?
  • 解释 Kotlin 中的 init 块。
  • openfinalabstract 有什么区别?
  • 什么是密封类 (sealed class)?
  • 什么是枚举类 (enum class)?
  • 什么是对象声明 (object declaration)?
  • 什么是伴生对象 (companion object)?
  • Kotlin 中的接口 (interface) 是什么?
  • 接口可以有默认实现吗?

🔍 高级 Kotlin 函数和作用域

  • letapplyalsorunwith 之间有什么区别?
  • 什么是作用域函数链 (scope function chaining)?
  • this 关键字有什么用?
  • Kotlin 中的 it 是什么?
  • 什么是标签返回 (labeled returns)?

🔤 泛型和类型系统

  • 如何声明一个泛型类或函数?
  • 泛型中的 inout 是什么?
  • 解释 Kotlin 的类型推断 (type inference)。
  • Kotlin 中的具体化类型 (reified type) 是什么?
  • 什么是类型别名 (typealias)?

🌐 协程 (基础)

  • Kotlin 中的协程 (coroutines) 是什么?
  • 如何启动一个协程?
  • 什么是 suspend 函数?
  • 什么是 GlobalScopeviewModelScopelifecycleScope
  • launchasync 之间有什么区别?
  • 什么是 withContext
  • Kotlin 中的调度器 (Dispatchers) 是什么?
  • 如何取消协程?
  • 如何在协程中处理异常?

🧩 Android + Kotlin (MVVM, Lifecycle)

  • Android 中的 MVVM 架构是什么?
  • 什么是 LiveData 以及如何使用它?
  • 什么是 ViewModel 及其重要性?
  • 如何在 Kotlin 中观察 LiveData?
  • 什么是生命周期感知组件 (lifecycle-aware components)?
  • 如何使用 Hilt 进行 Android 依赖注入?
  • Room 数据库在 Kotlin 中如何工作?
  • Room 中的 DAO 是什么?
  • DAO 中的 suspend 函数是什么?

🔵 高级 (Advanced Level)

🔁 协程和并发

  • 什么是结构化并发 (structured concurrency)?
  • CoroutineExceptionHandler 如何工作?
  • 什么是 SupervisorJob
  • 冷流 (cold flow) 和热流 (hot flow) 有什么区别?
  • 什么是 Kotlin Flow?
  • FlowStateFlowSharedFlow 之间有什么区别?
  • 什么是 collectLatest
  • 如何在 Flow 中处理背压 (backpressure)?
  • Flow 中的 debouncethrottle 是什么?
  • Flow 中的操作符 (operators) 是什么?

🛠️ DSL (领域特定语言)

  • 什么是 Kotlin DSL?
  • Kotlin DSL 在 Gradle 中是如何使用的?
  • 使用 Kotlin DSL 解释构建器模式 (builder pattern)。
  • 如何在 Kotlin 中创建自己的 DSL?

🌍 Kotlin Multiplatform

  • 什么是 Kotlin Multiplatform (KMP)?
  • KMP 中的 expectactual 是什么?
  • KMP 支持哪些平台?
  • Kotlin Multiplatform 有哪些局限性?
  • Kotlin Multiplatform 中如何共享代码?

🏷️ 注解和反射

  • 如何创建一个自定义注解?
  • Kotlin 如何支持反射 (reflection)?
  • 什么是 @JvmStatic@JvmOverloads@JvmName
  • Kotlin 中的 kclass 是什么?
  • 如何使用 Kotlin 反射?

⚙️ 性能和内存

  • Kotlin 如何管理内存?
  • 什么是内联类 (inline classes)?
  • Kotlin 1.5+ 中的值类 (value class) 是什么?
  • Kotlin 如何处理装箱/拆箱 (boxing/unboxing)?
  • 如何优化协程性能?

🎨 Jetpack Compose (基础到中级)

  • 什么是 Jetpack Compose?
  • Compose 与 XML 有何不同?
  • 如何创建一个 Composable 函数?
  • 什么是 @Composable 注解?
  • Compose 中的 remember 是什么?
  • 什么是 mutableStateOf
  • 重组 (recompositions) 如何工作?
  • 如何使用 LazyColumn 创建列表?
  • Compose 中的 Modifier 是什么?
  • 如何在 Compose 中管理点击事件?
  • 如何在 Compose 中实现导航?
  • rememberSaveableremember 有什么区别?
  • 如何使用 LaunchedEffect
  • SideEffectDisposableEffectDerivedStateOf 如何工作?
  • 如何将 ViewModel 与 Compose 集成?
  • 如何在 Compose 中观察 LiveData 或 StateFlow?
  • 如何在 Compose 中管理主题和样式?
  • 如何预览 Composable?
  • 如何使用 Scaffold、TopAppBar、BottomNavigation?
  • 如何创建自定义 Composable?
  • 如何在 Compose 中使用 ConstraintLayout?
  • 什么是 Compose 的 Slot API?

🔁 常见的 Kotlin + Android 集成问题

  • Kotlin 在 Android 开发中是如何使用的?
  • ActivityFragment 在 Kotlin 中有什么区别?
  • 如何在 Kotlin 中处理生命周期?
  • 如何使用 Kotlin 实现 ViewModel?
  • 什么是 LiveData 和 StateFlow?
  • Kotlin 中的 Jetpack Compose 是什么?
  • Kotlin 如何与 Retrofit 和 Room 协同工作?
  • 如何在 Kotlin 中编写单元测试?
  • 如何将 Kotlin 与 Dagger/Hilt 结合使用?
  • Kotlin 中常用的设计模式有哪些?
  • 如何在 Android 中实现 Clean 架构?
  • 使用 Kotlin 进行 Android 开发的最佳实践有哪些?
  • 如何测试 Compose UI?
  • 如何将 Firebase 与 Kotlin 结合使用?
  • 如何在 Kotlin Android 应用中保护 API 密钥?
  • 如何使用 WorkManager?
  • 如何使用协程安排后台任务?
  • 如何优化应用启动时间?
  • 如何使用 Jetpack Compose 实现深色主题?

🧠 专家级和实际 Android 问题 (新增)

Jetpack Compose:高级

  • Jetpack Compose 中重组的内部工作原理是什么?
  • 防止不必要的重组有哪些关键策略?
  • 如何在 Compose 中管理复杂的表单 UI 状态?
  • 如何优化 LazyColumn 性能?
  • LazyColumn 中的 key 是什么,为什么它们很重要?
  • 如何在 Jetpack Compose 中应用动画?
  • Compose 中的 AnimatedVisibility 是什么?
  • 如何使用 AnimatedContent 实现过渡效果?
  • 如何在 Compose 中检测手势?
  • 什么是指针输入修饰符 (pointer input modifiers)?

Clean 架构 + 架构模式

  • Android 中的 Clean 架构是什么?
  • Clean 架构中有哪些层?
  • 领域层 (domain layer) 如何与数据层 (data layer) 通信?
  • 存储库模式 (Repository pattern) 和用例模式 (UseCase pattern) 有何区别?
  • Android 架构中的关注点分离 (separation of concerns) 是什么?
  • 如何在 Android 中实现接口驱动开发 (interface-driven development)?
  • 什么是 SOLID 原则?它如何在 Android 中应用?
  • 如何在一个大型 Android 项目中组织包?
  • 使用共享结果包装器(密封类)有什么好处?
  • 在 Clean 架构中如何处理单一数据源 (single source of truth)?

依赖注入 (DI)

  • 什么是依赖注入?
  • 构造函数注入 (constructor injection) 和字段注入 (field injection) 有何区别?
  • Hilt 是什么,它与 Dagger 有何不同?
  • 如何使用 Hilt 注入 ViewModel?
  • 如何在 Hilt 中管理依赖项作用域 (ActivityScoped, Singleton)?
  • 如何使用 Hilt 注入接口?
  • 如何在 Hilt 中使用限定符 (Qualifiers)?
  • Hilt 中的 EntryPoint 是什么?

测试

  • 如何为 ViewModel 编写单元测试?
  • 什么是模拟 (mocking) 以及如何在 Kotlin 中使用它?
  • 如何测试 LiveData?
  • 如何使用 CoroutineTestRule?
  • 什么是 Robolectric 及其用途?
  • 如何测试 Room 数据库?
  • 如何测试 Compose UI?
  • 如何测试 Flow 或 StateFlow?

性能、安全及其他

  • 如何提高应用启动性能?
  • 如何分析 Android 中的内存泄漏?
  • Compose 中常见的性能瓶颈有哪些?
  • 如何在 Android 中保护敏感用户数据?
  • 如何检测 ANR (应用无响应)?
  • Android 中的 StrictMode 是什么?

补充:Kotlin 陷阱和模式

  • 有哪些值得注意的 Kotlin 陷阱?
  • 如何在 Kotlin 中使用 Result 类?
  • inlinenoinlinecrossinline 之间有什么区别?
  • 解释密封接口 (sealed interface) 的用途。
  • 如何在 Kotlin 中使用委托模式 (Delegation pattern)?
  • 什么是协程通道 (coroutine channels)?
  • JobDeferred 之间有什么区别?
  • 如何并行处理多个 Flow?

【Android进阶】Window打断动画优化记录

【Android进阶】Window打断动画优化记录

本文记录了Android平台上Window打断动画优化的记录

在Android系统中,绝大多数应用的界面逻辑承载于Activity组件之中,有一些需要在特殊层级显覆盖示的界面,需要使用WindowManager来添加某些特殊type的Window,显示页面内容。

并且,Activity应用的页面进场出场动画,不指定的情况下,都有系统默认的Transition处理。而使用WindowManager来添加和移除的页面,是没有默认动画效果的。

【Android进阶】RecyclerView原理和LazyColumn

【Android进阶】RecyclerView原理和LazyColumn

本文介绍了RecyclerView的优化原理,和Compose中的LazyColumn组件的实现原理。

RecyclerView的优化点

最初,要在Android界面中显示一个列表,使用的组件是 ListView ,但是由于 ListView 的性能问题,在Android 5.0之后,Google引入了 RecyclerView 组件。 RecyclerView 提供一个高度可定制的列表视图,同时保持了良好的性能和用户体验。

RecyclerView 用于在有限的屏幕空间内显示大量数据列表或网格。它是 ListViewGridView 的升级版,提供了更好的性能和灵活性。

核心理念:视图回收 (View Recycling)

RecyclerView 的核心优化点是 视图回收机制 。当列表中的项滚动出屏幕时,RecyclerView 不会销毁其视图。相反,它会回收这些不再可见的视图,并将其重新用于屏幕上即将显示的新项。通过视图回收机制,显著减少了视图创建的开销,尤其是在处理大量数据时表现出色。

对比ListView 来说,ListView 的视图回收机制依赖于开发者在 getView() 方法中手动实现 ViewHolder 模式来缓存子视图的引用 (findViewById() 操作耗时)。如果开发者不使用 ViewHolder 模式,那么每次 getView() 被调用时(即使是重用 convertView),都会重复调用 findViewById() 来查找子视图,这会严重影响滚动性能。

findViewById() 的实现方式是从当前视图(通常是 Activity 的根视图或一个 ViewGroup)开始,递归地遍历整个视图树,查找具有指定 ID 的视图。具体的遍历算法可能是深度优先搜索(DFS)或广度优先搜索(BFS),但无论哪种,它都必须检查视图树中的每一个节点,直到找到匹配的 ID 或者遍历完整个树。

RecyclerView 则强制并内置了 ViewHolder 模式,要求你必须创建并使用 ViewHolder 来持有视图引用,从而从根本上解决了这个问题。

动画

动画实现难度方面,ListView 在添加、删除或移动 item 时,实现动画效果非常复杂,通常需要手动处理和控制。RecyclerView 内置了 ItemAnimator 接口,使得为列表项的增删改提供平滑的动画变得非常简单和优雅。

职责解耦

ListView 将视图回收、布局管理和数据绑定等职责都集中在 ListView 和 Adapter 内部,导致代码耦合度较高。 RecyclerView 将这些职责分离到独立的组件中 (Adapter、ViewHolder、LayoutManager、ItemAnimator、ItemDecoration),使得组件更加解耦,易于测试、维护和扩展。

数据更新效率

ListView 只有一个 notifyDataSetChanged() 方法来通知数据变化。这意味着即使只有一个 item 发生了变化,整个列表也可能需要重新绘制,效率低下。

RecyclerView 提供了更精细的数据更新通知方法,如 notifyItemInserted()、notifyItemRemoved()、notifyItemChanged() 等,这些方法可以告知 RecyclerView 具体哪些 item 发生了变化,从而实现局部更新和更流畅的动画。

RecyclerView 的主要组成部分

要使用 RecyclerView,通常需要以下几个关键组件协同工作:

  1. RecyclerView 本身 (The ViewGroup):
    • RecyclerView 是一个 ViewGroup,它负责承载和管理列表中的所有视图。
    • 你把它添加到你的布局文件中,就像添加任何其他 UI 元素一样。
  2. ViewHolder (视图持有者):
    • 列表中的每个独立元素都由一个 ViewHolder 对象进行定义。
    • ViewHolder 的作用是持有并提供对单个列表项布局中所有视图的引用(例如 TextViewImageView 等)。
    • 当你创建 ViewHolder 时,它还没有任何关联的数据。RecyclerView 会在需要时将其绑定到其数据。
    • 你需要通过扩展 RecyclerView.ViewHolder 来定义自己的 ViewHolder 类。
  3. Adapter (适配器):
    • Adapter 负责将你的数据与 ViewHolder 绑定,并管理列表项的创建和更新。
    • RecyclerView 通过在 Adapter 中调用方法来请求视图并将视图绑定到其数据。
    • 你需要通过扩展 RecyclerView.Adapter 来定义自己的 Adapter 类。
    • Adapter 主要有三个重要方法:
      • onCreateViewHolder(): 当 RecyclerView 需要一个新的 ViewHolder 来表示列表项时,会调用此方法。你在这里创建 ViewHolder 及其关联的视图布局。
      • onBindViewHolder(): 当 RecyclerView 准备好将数据绑定到 ViewHolder 时,会调用此方法。你在这里获取特定位置的数据,并将其填充到 ViewHolder 的视图中。
      • getItemCount(): 返回列表中项的总数。
  4. LayoutManager (布局管理器):
    • LayoutManager 负责在 RecyclerView 中定位和排列列表中的各个元素,并决定何时回收和重用不再可见的项视图。
    • RecyclerView 库提供了几种开箱即用的 LayoutManager
      • LinearLayoutManager: 将项排列成一维列表(垂直或水平滚动)。
      • GridLayoutManager: 将项排列成二维网格。
      • StaggeredGridLayoutManager: 将项排列成错列的二维网格,每列稍微偏移。
    • 如果这些内置的 LayoutManager 不符合你的需求,你也可以通过扩展 RecyclerView.LayoutManager 抽象类来创建自定义的布局管理器。

视图回收的详细流程

RecyclerView 在处理每个子项视图时,采用了一套高度优化和解耦的机制,旨在实现高性能的列表滚动,尤其是在处理大量数据时。核心是视图回收 (View Recycling)职责分离 (Separation of Concerns)

下面详细介绍 RecyclerView 是如何处理每个子项视图的:

1. LayoutManager:布局与可见性管理

LayoutManagerRecyclerView 处理子项视图的第一个关键参与者。它的主要职责包括:

  • 布局 (Layout):决定列表项在 RecyclerView 中的排列方式,例如垂直线性、水平线性、网格或瀑布流等。它负责测量和放置每个可见的子视图。
  • 滚动 (Scrolling):管理用户的滚动事件,并根据滚动方向和速度决定哪些视图应该进入屏幕,哪些应该离开屏幕。
  • 视图附着/分离 (Attach/Detach Views):当视图进入屏幕时,LayoutManager 会将其附着 (attach)RecyclerView;当视图离开屏幕时,它会将其分离 (detach)。这里的分离并不是销毁,而是将其从 RecyclerView 的视图层级中移除,但保留在缓存中。
  • 视图回收/重用策略 (Recycling/Reusing Strategy)LayoutManager 会与 Recycler 合作,决定何时回收视图(当视图离开屏幕)以及何时重用视图(当需要显示新项时)。

当用户滚动 RecyclerView 时,LayoutManager 会不断计算哪些数据项应该可见。对于这些可见的数据项:

  • 如果有一个可以重用的废弃 (scrap)回收 (recycled) 视图可用,LayoutManager 会尝试使用它。
  • 如果没有可重用的视图,LayoutManager 会通知 Adapter 创建一个新的视图。

2. Recycler (缓存机制):视图回收池

RecyclerView 内部有一个强大的 Recycler 机制,它维护了 多个视图缓存池 ,以高效地管理视图的回收和重用:

  • Scrap Heap (废弃堆):
    • 这是一个非常轻量的缓存,用于存储最近被分离但可能很快再次附着的视图。
    • 例如,当你执行一个微小的滚动,或者进行一个 notifyItemChanged() 操作时,视图可能只是暂时离开屏幕,然后又回来。
    • 这里的视图不会被解除绑定 (unbound),因此不需要再次调用 onBindViewHolder()
    • LayoutManager 会优先从这里查找可重用的视图。
  • View Cache (视图缓存):
    • 这个缓存池存储的是最近滚出屏幕的 ViewHolder,它们已经被从 RecyclerView 中分离。
    • 当一个 ViewHolderScrap Heap 无法被重用时,LayoutManager 会尝试从 View Cache 中获取。
    • View Cache 中的 ViewHolder 仍然持有视图引用,但它们可能已经与之前的数据解绑,需要通过 onBindViewHolder() 重新绑定新数据。
    • 默认情况下,这个缓存的大小是有限的(通常为 2)。
  • RecycledViewPool (回收视图池):
    • 这是一个更深层的缓存,存储的是已经完全回收的 ViewHolder
    • ViewHolder 离开 View CacheLayoutManager 明确将其回收时,它会进入 RecycledViewPool
    • 这里的 ViewHolder 是按视图类型 (view type) 进行分类存储的。如果你的 RecyclerView 有多种不同的 item 布局,它们会分别存储在各自的池中。
    • RecycledViewPool 取出的 ViewHolder 必须重新绑定数据,即总是会调用 onBindViewHolder()
    • 这个池是可以在多个 RecyclerView 实例之间共享的(例如在嵌套 RecyclerView 中),进一步提高了效率。
  • Attached Views (已附着视图):
    • 这些是当前屏幕上可见的、已经被附着到 RecyclerView 中的视图。它们没有被回收,也没有进入任何缓存池。

3. Adapter:数据与视图的桥梁

Adapter 是数据和视图之间的桥梁,它与 LayoutManagerViewHolder 紧密协作,负责以下工作:

  • getItemCount(): 告诉 RecyclerView 总共有多少个数据项。
  • getItemViewType(int position):
    • 如果你的列表有不同类型的 item 布局(例如,一个列表项是图片,另一个是文字),你需要重写这个方法,返回一个唯一的整数来标识不同类型。
    • RecyclerView 会根据 viewTypeRecycledViewPool 中查找相应类型的 ViewHolder 进行重用,避免混淆不同布局的视图。
  • onCreateViewHolder(ViewGroup parent, int viewType):
    • LayoutManager 需要一个新的 ViewHolder 时(即 Scrap HeapView Cache 都没有可重用的视图,或者需要一个新类型的视图时),会调用此方法。
    • 在这个方法中,你通过 LayoutInflater.from(parent.getContext()).inflate() 创建一个新的视图布局。
    • 然后,你将这个视图传递给你的自定义 ViewHolder 构造函数,ViewHolder 会在这里通过 findViewById() 查找并缓存其内部的子视图引用。
    • 最后,返回这个新创建的 ViewHolder 实例。这个方法通常只会被调用有限的次数,因为一旦创建了足够多的视图来填充屏幕,就会开始进行视图回收。
  • onBindViewHolder(ViewHolder holder, int position):
    • LayoutManager 需要将一个 ViewHolder 与特定位置的数据项关联起来时,会调用此方法。
    • 无论 ViewHolder 是新创建的还是从缓存中重用的,这个方法都会被调用。
    • 你在这里从数据源中获取 position 对应的数据,然后使用 holder 中缓存的子视图引用,将数据填充到视图中(例如 holder.textView.setText(data.getName()))。这是数据绑定的核心步骤。

4. ViewHolder:视图引用持有者

ViewHolderRecyclerView 性能优化的核心。它的作用是:

  • 缓存视图引用: 在 ViewHolder 的构造函数中,通过 findViewById() 获取所有需要操作的子视图的引用,并将其存储为成员变量。
  • 避免重复查找: 一旦 ViewHolder 被创建并缓存了视图引用,后续无论这个 ViewHolder 被重用多少次,都无需再次调用 findViewById()。直接通过 ViewHolder 内部的成员变量即可访问子视图,大大提高了性能。
  • 封装逻辑: ViewHolder 也可以作为放置与单个列表项相关的事件监听器(如点击事件)和特定UI更新逻辑的好地方。

总结整个流程:

  1. 初始化: RecyclerView 被添加到布局中。
  2. 设置 LayoutManager: RecyclerView 知道如何排列其子项。
  3. 设置 Adapter: RecyclerView 知道如何获取数据并创建/绑定视图。
  4. 初始布局: LayoutManagerAdapter 请求足够多的 ViewHolder (onCreateViewHolder) 并绑定数据 (onBindViewHolder) 来填充屏幕,然后将这些 ViewHolder 的视图附着到 RecyclerView
  5. 滚动时:
    • 当一个 item 滚出屏幕时,LayoutManager 会将其视图从 RecyclerView分离。这个 ViewHolder 可能会进入 Scrap HeapView Cache,最终可能进入 RecycledViewPool
    • 当一个新 item 需要进入屏幕时,LayoutManager 首先尝试从 Scrap Heap 中获取一个可重用的 ViewHolder
    • 如果 Scrap Heap 中没有,它会尝试从 View Cache 中获取。
    • 如果 View Cache 中也没有,它会检查 RecycledViewPool 中是否有指定 viewTypeViewHolder
    • 如果所有缓存中都没有,LayoutManager 会通知 Adapter 调用 onCreateViewHolder() 来创建一个全新的 ViewHolder
    • 一旦获取到 ViewHolder(无论是重用的还是新的),LayoutManager 会通知 Adapter 调用 onBindViewHolder(),将当前位置的数据绑定到 ViewHolder 的视图上。
    • 最后,LayoutManager 将这个绑定好数据的 ViewHolder 的视图附着RecyclerView 中,使其可见。

通过这种精巧的视图回收和职责分离机制,RecyclerView 能够以极高的效率处理动态列表,无论是数量庞大的数据还是复杂的 item 布局,都能提供流畅的用户体验。

LazyColumn

在 Jetpack Compose 中,LazyColumn (以及 LazyRowLazyVerticalGridLazy 布局) 是处理大量列表数据的核心组件。与 Android View 系统中的 RecyclerView 类似,LazyColumn 的显示逻辑也基于按需组合 (Composition on Demand)视图回收 (View Recycling) 的概念,但它的实现方式与 RecyclerView 略有不同,并且更加“Compose 式”。

LazyColumn 是一个 懒加载 (Lazy Loading) 的列表,它只会在列表项进入屏幕可见区域时才会创建和渲染这些项。这意味着,当列表中有大量数据时,它可以显著减少内存占用和渲染性能。

LazyColumn 的核心显示逻辑

LazyColumn 的设计目标是:只组合(Compose)并测量(Measure)当前在屏幕上可见或即将可见的 item,而不是一次性处理所有数据项。

  1. 按需组合 (Composition on Demand):
    • 当你向 LazyColumn 提供一个数据列表时,它并不会立即为列表中的所有数据项创建对应的 Composable 函数实例。
    • 相反,它只会根据当前滚动位置和屏幕尺寸,计算出哪些 item 应该显示在屏幕上。
    • 只有那些位于可见区域内的 item 的 Composable 函数才会被执行 (Compose)。这被称为“按需组合”。
    • 当你滚动列表时,新的 item 进入可见区域,它们的 Composable 函数才会被调用,从而创建其 UI。滚出屏幕的 item 的 Composable 函数会停止执行,其对应的 UI 节点也会被销毁。
  2. 内容插槽 (Content Slotting):
    • LazyColumn 使用了 Compose 的“内容插槽”模式。你不是直接将 Composable 函数传递给 LazyColumn,而是通过 itemsitem DSL(领域特定语言)块来定义每个 item 的内容。
    • LazyColumn { items(myList) { data -> MyItemComposable(data) } }
    • 这里的 MyItemComposable(data) 就是一个内容插槽,LazyColumn 会根据需要来组合这些内容。
  3. 智能回收与重用 (Smart Recycling and Recomposition):
    • 虽然 Compose 不像 RecyclerView 那样有显式的 ViewHolder 概念,但 LazyColumn 内部也实现了高效的回收机制。
    • LazyColumn 组件中,能够被“复用”的主要概念是 Composable 函数的 UI 结构和底层布局对象 (layout objects),而不是像传统 RecyclerView 那样对 View 实例进行回收和重用。
    • 当另一个 item 需要进入屏幕时,如果缓存中存在一个相同类型的 Composable 实例,并且这个实例可以被重用,LazyColumn 会尝试重用它。
    • 重用的核心在于 Recomposition (重组)
      • 如果被重用的 Composable 实例接收到的数据(或状态)与上次相同,那么 Composable 可能会跳过执行(可跳过 Composable 的优化)。
      • 如果数据不同,Compose 会进行智能重组。它会比较新的数据和旧的数据,并只更新 UI 中实际发生变化的部分,而不是重新创建整个 item 的 UI。这种增量更新是 Compose 性能的关键。
  4. LazyListState 和滚动位置管理:
    • LazyColumn 内部维护着一个 LazyListState 对象(通常通过 rememberLazyListState() 创建)。
    • LazyListState 记录了当前列表的滚动位置、第一个可见 item 的索引、可见 item 的偏移量等信息。
    • 当用户滚动时,LazyListState 会更新,并通知 LazyColumn 重新测量和布局可见 item
  5. key 参数的重要性:
    • itemsitem DSL 提供了 key 参数,强烈建议为每个 item 提供一个稳定且唯一的 key
    • LazyColumn 使用 key 来优化重组和识别 item 的移动、添加或删除。
    • 如果不提供 keyLazyColumn 默认使用 item 的索引作为 key。当列表中的 item 顺序发生变化(例如删除或重新排序)时,使用索引作为 key 会导致错误的重用或不必要的重组,甚至可能导致动画效果不佳。
    • 提供了稳定的 key 后,即使数据列表的顺序发生变化,LazyColumn 也能识别出哪些是同一个逻辑 item,从而正确地重用其 Composable 实例并应用正确的动画。
    • 需要注意的是每个item的key应该唯一,如果运行时出现重复的key时会直接报错崩溃。

LazyColumnRecyclerView 的主要区别 (在视图处理层面)

特性RecyclerView (View System)LazyColumn (Jetpack Compose)
视图创建通过 AdapteronCreateViewHolder 方法,使用 XML inflated 创建 View 实例。通过 Composable 函数的执行(Composition),直接生成 UI 节点。
视图回收ViewHolder 模式,通过 Recycler 缓存 View 对象。内部缓存 Composable 实例,通过 Recomposition 机制重用和更新 UI。没有显式的 ViewHolder 类。
数据绑定AdapteronBindViewHolder 方法负责将数据绑定到 View 的各个子组件。数据直接作为参数传递给 item 的 Composable 函数,Compose 自动处理数据变化时的 Recomposition。
布局管理通过独立的 LayoutManager 类 (LinearLayoutManager 等) 管理布局策略。LazyColumn 自身内置了布局逻辑,无需单独的 LayoutManager 类。
动画通过 ItemAnimator 实现增删改动画,通常需要额外配置。内置更平滑的动画支持,得益于 Compose 的 Recomposition 和 Keying 机制。
强制性优化ViewHolder 模式需要开发者手动实现才能获得最佳性能。LazyColumn 内部已经强制并内置了类似的优化,开发者只需关注 item 的 Composable 逻辑。
UI 范式命令式 UI:手动操作 View 对象。声明式 UI:描述 UI 应该是什么样子,Compose 负责如何达到。

总结

LazyColumn 的显示逻辑是基于 Compose 的声明式 UI 特性,它在运行时智能地组合和重组 item 的 Composable 函数,只渲染当前可见的 UI 部分。通过内部的缓存机制和 key 参数的优化,LazyColumn 实现了类似 RecyclerView 的高效列表性能,同时提供了更加简洁和直观的 API。开发者不再需要关心繁琐的 findViewByIdViewHolderAdapter 生命周期管理,只需专注于定义每个 item 的 UI 外观和数据绑定逻辑。

【Android进阶】Android自定义View

【Android进阶】Android自定义View

本文介绍了 View 框架下,自定义复杂View的一些路线

在 Android 开发过程中,我们经常使用官方提供的 TextView、Button 和 RecyclerView 来构建界面。但当面临复杂、个性化或高性能的 UI 需求时,这些标准组件往往力不从心。这时,自定义 View 就显得很必要了。

自定义 View 不仅仅是简单地重写 onDraw(),它更关乎 Android 系统的测量、布局、性能优化,以及触摸事件的精准处理。

继承自ViewGroup

这种方法主要是界面里有多处出现的相同的公共控件,将多个基础子View组合到一起,一起集中至一个 ViewGroup 中。自定义View类往往直接继承自某个特定的ViewGroup(比如LinearLayout),往里面添加可复用的子View:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_button"
    android:orientation="vertical"
    android:padding="10dp">

    <ImageView
        android:id="@+id/iv_count"
        android:layout_width="100dp"
        android:layout_height="200dp"
        android:layout_gravity="center"
        android:background="@drawable/testbg"
        android:textSize="24sp" />


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="首页卡片"
        android:textColor="@color/white"
        android:textSize="20sp" />

</LinearLayout>

在Java或者Kotlin代码中使用这些控件,指定响应交互的方式。

class MainCardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    // 加载布局文件
    private val binding: LayoutMainCardViewBinding =
        LayoutMainCardViewBinding.inflate(LayoutInflater.from(context), this, true)
    
}

效果截图:

直接继承自View

基础绘制类和方法

Paint

Paint 对象,顾名思义是画笔,定义“用什么”来画,决定了你绘制的颜色、样式、粗细、字体等所有外观特性。

核心概念作用描述常用方法(部分)
颜色设置绘制的颜色。setColor(int color)
样式 (Style)决定绘制是描边、填充还是两者都有。setStyle(Paint.Style style)
- FILL (填充)
- STROKE (描边/空心)
- FILL_AND_STROKE (填充加描边)
线宽设置线条或描边(STROKE 模式)的粗细。setStrokeWidth(float width)
抗锯齿让边缘更平滑,图形更美观。setAntiAlias(boolean aa)
着色器 (Shader)实现渐变、纹理等复杂色彩效果。setShader(Shader shader) (如 LinearGradient)
文本相关设置字体、字号、对齐方式。setTextSize(float size), setTypeface(Typeface typeface)
路径效果设置路径的绘制效果(如虚线、点状线)。setPathEffect(PathEffect effect) (如 DashPath)

常见于初始化操作:

textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setColor(Color.BLACK);

每次调用 Canvasdraw... 方法时,都需要传入一个配置好的 Paint 对象。

例如绘制文字:

 public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
    super.drawText(text, x, y, paint);
}

Canvas

Canvas 是一个画布,定义“在哪里”画,它提供了所有绘制图形、文本、位图的方法。所有绘制操作都是基于 Canvas 上的坐标系进行的。

核心概念作用描述常用方法(部分)
绘制方法用于画出各种图形。drawLine(), drawCircle(), drawRect(), drawPath()
坐标系以 View 的左上角为原点 $(0, 0)$,向右为 $x$ 轴正方向,向下为 $y$ 轴正方向。 
变换 (Matrix)对画布进行平移、旋转、缩放等操作,影响后续所有绘制。translate(), rotate(), scale()
图层管理允许创建新的绘图层,用于复杂的叠加和混合效果。save(), restore()
裁剪 (Clip)限制绘制的区域,超出裁剪区域的内容将不会显示。clipRect(), clipPath()

Canvas 提供的 draw 方法只负责“画出”这个动作和位置,至于“画成什么样子”,则完全依赖于传入的 Paint

常用几何图形类

这些类用于存储几何图形的尺寸和形状信息,作为 Canvas 绘制方法的参数。

1. Rect / RectF(矩形)
  • Rect,存储整数坐标的矩形。适用于需要像素对齐的场景。
  • RectF ,存储浮点数坐标的矩形。在大多数绘图场景中更常用,因为浮点数精度更高。

它们都存储四个坐标值:

  • left: 左边界 x 坐标
  • top: 上边界 y 坐标
  • right: 右边界 x 坐标
  • bottom: 下边界 y 坐标

例如:

public class CustomRectView extends View {
    private Paint mRectPaint; 
    private Rect mDrawingRect;

    ...

    private void init() {
        mRectPaint = new Paint();
        mRectPaint.setColor(Color.BLUE); 
        mRectPaint.setStyle(Paint.Style.FILL); 
        mRectPaint.setAntiAlias(true);   
        // 初始化 Rect 对象。注意:这里的坐标通常是临时值或0,
        // 实际的边界值通常在 onSizeChanged 或 onDraw 中根据 View 尺寸设置。
        mDrawingRect = new Rect();
    }

    // 推荐在尺寸确定后设置 Rect 的边界
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 设置矩形的边界坐标:
        // 距离左边 50 像素,距离顶部 50 像素,宽度到 View 宽度减 50,高度到 View 高度减 50
        int left = 50;
        int top = 50;
        int right = w - 50;
        int bottom = h - 50;
        // 利用 Rect 对象的 set() 方法设置四个边界坐标
        mDrawingRect.set(left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 3. 利用 Canvas 的 drawRect 方法和 Rect 对象进行绘制
        // drawRect(Rect r, Paint paint) 是 Canvas 专门用于绘制 Rect 区域的方法
        canvas.drawRect(mDrawingRect, mRectPaint);

        // 提示:也可以直接使用四个坐标值绘制,但使用 Rect 更便于管理和传递区域信息:
        // canvas.drawRect(50, 50, getWidth() - 50, getHeight() - 50, mRectPaint); 
    }
}
2. Path(路径)

Path用于定义任意复杂的几何图形,由直线、曲线等线段组成。

  • moveTo(x, y): 设置起点。
  • lineTo(x, y): 画直线到指定点。
  • quadTo() / cubicTo(): 画二次/三次贝塞尔曲线
  • addCircle() / addRect(): 直接添加圆形/矩形。
  • close(): 闭合路径,将终点连接回起点。

可以绘制心形、五角星等复杂或不规则的图形,平滑的曲线或折线图。

例如利用 Path 绘制一个五角星:

private void calculateStarPath(float centerX, float centerY) {
    mStarPath.reset();
    float currentAngle = (float) (Math.PI / 2.0); // 90度,从上方顶点开始
    // 五角星总共有 5 个外顶点和 5 个内陷点,共 10 个点
    for (int i = 0; i < 10; i++) {
        float radius = (i % 2 == 0) ? mOuterRadius : mInnerRadius;
        // 计算当前点的 X 坐标:centerX + radius * cos(angle)
        float x = (float) (centerX + radius * Math.cos(currentAngle));
        // 计算当前点的 Y 坐标:centerY - radius * sin(angle)。注意 Y 轴方向
        float y = (float) (centerY - radius * Math.sin(currentAngle));
        if (i == 0) {
            // 第一个点作为起始点
            mStarPath.moveTo(x, y);
        } else {
            // 连接到下一个点
            mStarPath.lineTo(x, y);
        }
        // 角度递增:每次增加 36 度(两个相邻顶点/内陷点间的角度)
        currentAngle -= ANGLE_STEP;
    }
    // 闭合路径(虽然 Path 默认会闭合,但最好显式调用)
    mStarPath.close();
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(mStarPath, mPaint);
}

常用绘制方法详解

1. 绘制点和线
方法签名作用
drawPoint()drawPoint(float x, float y, Paint paint)在指定坐标 $(x, y)$ 绘制一个点。点的形状和大小受 PaintsetStrokeCap() (例如设置为圆形) 和 setStrokeWidth() 影响。
drawLine()drawLine(float startX, float startY, float stopX, float stopY, Paint paint)绘制一条直线,从 $(startX, startY)$ 到 $(stopX, stopY)$。线的颜色和粗细由 Paint 决定。
drawLines()drawLines(float[] pts, Paint paint)批量绘制多条线段。数组 pts 中的元素是 $[x_0, y_0, x_1, y_1, x_2, y_2, \dots]$,每四个坐标定义一条线。
2. 绘制圆形
方法签名作用
drawCircle()drawCircle(float cx, float cy, float radius, Paint paint)在中心点 $(cx, cy)$ 处,绘制一个指定半径的圆形。
3. 绘制矩形和椭圆
方法签名作用
drawRect()drawRect(RectF rect, Paint paint)
drawRect(float left, float top, float right, float bottom, Paint paint)
绘制一个矩形。最常用的方式是传入一个 RectF 对象。
drawOval()drawOval(RectF oval, Paint paint)绘制一个椭圆。这个椭圆会内切于传入的 RectF 或矩形边界。如果传入的 RectF 是一个正方形,则会画出圆形。
drawRoundRect()drawRoundRect(RectF rect, float rx, float ry, Paint paint)绘制一个圆角矩形。rxry 分别指定圆角在 $x$ 轴和 $y$ 轴上的半径。
4. 绘制路径
方法签名作用
drawPath()drawPath(Path path, Paint paint)绘制一个由 Path 对象定义的复杂图形。这是绘制不规则图形和贝塞尔曲线的唯一方式。

重要回调方法

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

决定自身期望的尺寸,根据父容器传递的测量规格 (MeasureSpec) 计算并设置 View 最终的宽度和高度,通过调用 setMeasuredDimension(width, height) 来确定。必须实现,确保 View 能够正确地被布局,特别是当尺寸为 wrap_content 时。

onLayout(boolean changed, int left, int top, int right, int bottom)

决定子 View 的位置,对于 ViewGroup 来说,它用于遍历所有子 View 并调用它们的 layout() 方法来定位子 View。对于单个 View 来说,这个方法通常不需要重写(除非你需要特殊的布局逻辑)。主要用于自定义 ViewGroup,用于定位子元素。自定义 View 通常不用重写。

onSizeChanged(int w, int h, int oldw, int oldh)

View 的尺寸确定或发生变化时回调。它提供了 View 最终确定的尺寸,适合在这里进行与尺寸相关的初始化工作,如计算绘制坐标、创建 Bitmap 等。适合作为绘图对象的初始化场所。首次布局和尺寸变化时会回调。

onDraw(Canvas canvas)

执行实际的绘制操作。onDraw 接收一个 Canvas 对象,可以在其上绘制图形、文本、图片等所有可见内容。所有自定义 View 的外观都是在这里完成的。所有自定义绘图代码都放在这里。注意要避免在这里执行耗时操作和对象创建。

onTouchEvent(MotionEvent event)

处理用户的触摸事件,用于接收和响应用户的点击、滑动等输入。你需要在这里通过判断 MotionEvent.getAction() 来处理不同的手势。属于核心交互方法,返回 true 表示你已处理事件,并且希望继续接收后续事件。

computeScroll()

配合 Scroller 实现平滑滚动,这个方法在 View 重绘时被调用,用于查询 Scroller 的当前位置,更新 ViewmScrollX/mScrollY,并通过 postInvalidate() 驱动连续动画。实现平滑滚动(例如 fling 或平移)时必须重写。

总结流程

View 的绘制流程可以简化为以下顺序(通常由系统驱动):

  1. 构造方法:初始化 Paint、属性等不依赖尺寸的对象。
  2. onMeasure():决定 View 的期望尺寸。
  3. onSizeChanged():获取最终尺寸,初始化依赖尺寸的对象。
  4. onDraw():绘制 View 的外观。
  5. onTouchEvent():处理用户交互。
  6. computeScroll():如果在交互中触发了 Scroller,这个方法会驱动滚动动画。

静态View

这一类往往是无动态变化和交互的,一般直接在渲染绘制阶段使用特殊的手段给其加上特定的显示效果。

例如自定义一个可以渐变颜色的TextView:

public class GradientTextView extends View {
    private Paint paint;
    private String text;
    private float textSize;

    public GradientTextView(Context context) {
        super(context);
        init();
    }

    public GradientTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public GradientTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        paint = new Paint();
        text = "Hello, World!";
        // 使用 dp/sp 转换尺寸更好,这里暂时保持 60f
        textSize = 60f;
        paint.setTextSize(textSize);
        // 抗锯齿
        paint.setAntiAlias(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w > 0) {
            // 每次尺寸变化时,根据最新的宽度 w 重新创建 LinearGradient
            // 渐变从 (0, 0) 到 (w, 0)
            Shader textShader = new LinearGradient(0, 0, w, 0,
                    Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
            // 将新的 Shader 设置给 Paint
            paint.setShader(textShader);
            // 确保重新绘制
            invalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 测量文本宽度,使用 paint.measureText(text) 获取文本的精确宽度
        float desiredWidth = paint.measureText(text);
        // 2. 测量文本高度,使用 Paint.FontMetrics 来准确计算文本高度,包括上行空间(ascent)和下行空间(descent)
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        // 文本高度 = descent - ascent
        float desiredHeight = fontMetrics.bottom - fontMetrics.top;
        // 3. 处理 View 的尺寸规格 (MeasureSpec) 根据父布局的限制和我们计算出的期望尺寸来确定最终的尺寸
        // 确定最终宽度
        int width = resolveSize((int) desiredWidth, widthMeasureSpec);
        // 确定最终高度
        int height = resolveSize((int) desiredHeight, heightMeasureSpec);
        // 4. 设置最终测量尺寸
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 计算文本绘制的基线位置。
        // drawText 的 y 坐标是基线位置,而不是顶部。
        // 为了让文本垂直居中(假设 View 高度 h 足够),可以使用 (getHeight() - paint.ascent() - paint.descent()) / 2
        // 但为了简单,暂时保持原始逻辑:从 y=textSize 处开始绘制 (顶部)
        canvas.drawText(text, 0, textSize, paint);
    }
}

效果截图:

可以看到代码中是创建了创建了一个从红色 (Color.RED) 到蓝色 (Color.BLUE) 的水平线性渐变,然后应用到paint画笔上:

textShader = new LinearGradient(0, 0, getWidth(), 0, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);

参数解释:

  • (0, 0): 渐变的起始点坐标(左上角)。
  • getWidth(), 0: 渐变的结束点坐标(右边界,与起始点 y 坐标相同,因此是水平渐变)。
  • Color.RED: 渐变的起始颜色。
  • Color.BLUE: 渐变的结束颜色。
  • Shader.TileMode.CLAMP: 瓷砖模式。CLAMP 的意思是如果渐变区域不够覆盖绘制区域,则在渐变的起止颜色之外的区域,使用渐变的起始色和结束色来延伸填充(在这个例子中,超出部分会被最接近的红色或蓝色填充)。

注意我们是依靠宽度来设置线性简便效果的,还需要重写 onMeasure() 方法,实现自适应的效果,以正确显示渐变。

onMeasure 实现自适应尺寸

包裹内容

上一个例子中,根据文字内容的宽高来重写了onMeasure方法,以达到包裹文字的效果。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 测量文本宽度,使用 paint.measureText(text) 获取文本的精确宽度
    float desiredWidth = paint.measureText(text);
    // 2. 测量文本高度,使用 Paint.FontMetrics 来准确计算文本高度,包括上行空间(ascent)和下行空间(descent)
    Paint.FontMetrics fontMetrics = paint.getFontMetrics();
    // 文本高度 = descent - ascent
    float desiredHeight = fontMetrics.bottom - fontMetrics.top;
    // 3. 处理 View 的尺寸规格 (MeasureSpec) 根据父布局的限制和我们计算出的期望尺寸来确定最终的尺寸
    // 确定最终宽度
    int width = resolveSize((int) desiredWidth, widthMeasureSpec);
    // 确定最终高度
    int height = resolveSize((int) desiredHeight, heightMeasureSpec);
    // 4. 设置最终测量尺寸
    setMeasuredDimension(width, height);
}

固定尺寸

如果是一个图标或者图片类型的自定义View,期望在设置 wrap_content 自适应时,直接用一个预设的固定值,这时候就需要动态判断 measureSpec 这两个参数了。

companion object {
    const val FIXED_WIDTH_DP: Float = 200f
    const val FIXED_HEIGHT_DP: Float = 200f
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // 1. 获取固定的像素值
    val fixedWidthPx: Int = (FIXED_WIDTH_DP).dp2px()
    val fixedHeightPx: Int = (FIXED_HEIGHT_DP).dp2px()
    // 2. 处理宽度
    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val finalWidth: Int = when (widthMode) {
        MeasureSpec.EXACTLY -> {
            // 布局指定了确切尺寸 (match_parent 或固定值)
            widthSize
        }
        MeasureSpec.AT_MOST -> {
            // 布局是 wrap_content,我们在这里返回固定尺寸
            // 但不能超过父布局的限制
            min(fixedWidthPx, widthSize)
        }
        else -> {
            // UNSPCIFIED,通常用于 Adapter View 等,直接返回固定尺寸
            fixedWidthPx
        }
    }
    // 3. 处理高度 (逻辑与宽度相同)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)
    val finalHeight: Int = when (heightMode) {
        MeasureSpec.EXACTLY -> {
            heightSize
        }
        MeasureSpec.AT_MOST -> {
            // 布局是 wrap_content,返回固定尺寸
            min(fixedHeightPx, heightSize)
        }
        else -> {
            fixedHeightPx
        }
    }
    // 4. 设置最终测量尺寸
    setMeasuredDimension(finalWidth, finalHeight)
}

动态View

这种类型的实现方式是可以动态更新的View,比如具体某一个时间点,或者一个其他的什么条件,达成之后,动态地改变内部绘制的参数。

这里举一个时钟表盘的例子:

class ClockView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val calendar = Calendar.getInstance()

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 获取当前时间
        calendar.timeInMillis = System.currentTimeMillis()
        val hour = calendar.get(Calendar.HOUR)
        val minute = calendar.get(Calendar.MINUTE)
        val second = calendar.get(Calendar.SECOND)

        // 绘制表盘
        drawClockFace(canvas)

        // 绘制时针
        drawHand(canvas, hour.toFloat() * 5 + minute / 12f, 0.5f, Color.BLACK)

        // 绘制分针
        drawHand(canvas, minute.toFloat(), 0.7f, Color.BLACK)

        // 绘制秒针
        drawHand(canvas, second.toFloat(), 0.9f, Color.RED)

        // 每秒更新一次
        postInvalidateDelayed(1000)
    }

    private fun drawClockFace(canvas: Canvas) {
        val centerX = width / 2f
        val centerY = height / 2f
        val radius = width.coerceAtMost(height) / 2f * 0.8f

        // 绘制表盘背景
        paint.color = Color.WHITE
        paint.style = Paint.Style.FILL
        canvas.drawCircle(centerX, centerY, radius, paint)

        // 绘制表盘边框
        paint.color = Color.BLACK
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 5f
        canvas.drawCircle(centerX, centerY, radius, paint)

        // 绘制刻度
        paint.strokeWidth = 3f
        for (i in 0..59) {
            val angle = Math.PI * i / 30 - Math.PI / 2
            val startX = centerX + (radius - 20) * cos(angle)
            val startY = centerY + (radius - 20) * sin(angle)
            val stopX = centerX + radius * cos(angle)
            val stopY = centerY + radius * sin(angle)
            canvas.drawLine(
                startX.toFloat(),
                startY.toFloat(),
                stopX.toFloat(),
                stopY.toFloat(),
                paint
            )
        }

        // 绘制数字
        paint.textSize = 40f
        paint.textAlign = Paint.Align.CENTER
        for (i in 1..12) {
            val angle = Math.PI * i / 6 - Math.PI / 2
            val x = centerX + (radius - 60) * cos(angle)
            val y = centerY + (radius - 60) * sin(angle) + paint.textSize / 3
            canvas.drawText(i.toString(), x.toFloat(), y.toFloat(), paint)
        }
    }

    private fun drawHand(canvas: Canvas, position: Float, length: Float, color: Int) {
        val centerX = width / 2f
        val centerY = height / 2f
        val radius = width.coerceAtMost(height) / 2f * 0.8f

        paint.color = color
        paint.strokeWidth = 10f
        paint.strokeCap = Paint.Cap.ROUND

        val angle = Math.PI * position / 30 - Math.PI / 2
        val stopX = centerX + radius * length * cos(angle)
        val stopY = centerY + radius * length * sin(angle)
        canvas.drawLine(centerX, centerY, stopX.toFloat(), stopY.toFloat(), paint)
    }
}

这里为了简单实现,直接在onDraw里进行的重绘消息post,实际应该在外部调用,或者使用一个Handler来循环刷新。

运行截图:

交互式

这类也是需要处理动态刷新的自定义View,更进一步还结合了触摸事件的处理,即需要根据 ACTION_MOVE 事件或者封装的 GestureDetector 手势回调里,判断方向和距离,对内部的组件进行特定的移动,缩放等处理逻辑。

比如一个空调温度选择滑动列表:

public class TemperaturePickerView extends View {

    private static final String TAG = "TemperaturePickerView";

    // 温度范围
    private static final int MIN_TEMPERATURE = 16;
    private static final int MAX_TEMPERATURE = 30;

    // 绘制相关
    private Paint textPaint;
    private Paint selectedLinePaint;

    // 尺寸相关
    private float itemHeight;
    private float centerY;
    private float textBaseline;

    // 滚动相关
    private Scroller scroller;
    private GestureDetector gestureDetector;
    private float currentScrollY = 0;
    private float maxScrollY;

    // 选中监听
    private OnTemperatureChangeListener listener;
    private int currentTemperature = 20;

    // 字体大小配置
    private float maxTextSize;
    private float minTextSize;

    public interface OnTemperatureChangeListener {
        void onTemperatureChanged(int temperature);
    }

    public TemperaturePickerView(Context context) {
        super(context);
        init(context);
    }

    public TemperaturePickerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public TemperaturePickerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        // 初始化文字画笔
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setColor(Color.BLACK);

        // 选中线画笔
        selectedLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        selectedLinePaint.setStyle(Paint.Style.STROKE);
        selectedLinePaint.setStrokeWidth(3);
        selectedLinePaint.setColor(0xFF2196F3);

        // 滚动控制器
        scroller = new Scroller(context, new DecelerateInterpolator());

        // 手势检测
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                // 处理滚动
                scrollByInternal(0, (int) distanceY);
                return true;
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                // 处理快速滑动
                scroller.fling(0, (int) currentScrollY, 0, (int) -velocityY,
                        0, 0, 0, (int) maxScrollY);
                Log.i(TAG, "onFling: velocityY = " + velocityY);
                adjustPosition();
                invalidate();
                return true;
            }
        });

        // 尺寸转换
        maxTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 24, getResources().getDisplayMetrics());
        minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.i(TAG, "onSizeChanged: w = " + w + ", h = " + h);
        centerY = h / 2f;
        itemHeight = h / 8f;

        // 计算文字基线位置
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        textBaseline = centerY - (fontMetrics.descent + fontMetrics.ascent) / 2;

        // 计算最大滚动范围
        int totalItems = MAX_TEMPERATURE - MIN_TEMPERATURE + 1;
        maxScrollY = (totalItems - 1) * itemHeight;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawTemperatures(canvas);
        drawSelectedLine(canvas);
    }

    private void drawSelectedLine(Canvas canvas) {
        float centerX = getWidth() / 2f;
        // 绘制选中线
        canvas.drawLine(centerX - 80, centerY, centerX + 80, centerY, selectedLinePaint);

        // 绘制选中指示器
        selectedLinePaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(centerX - 80, centerY, 8, selectedLinePaint);
        canvas.drawCircle(centerX + 80, centerY, 8, selectedLinePaint);
        selectedLinePaint.setStyle(Paint.Style.STROKE);
    }

    private void drawTemperatures(Canvas canvas) {
        float centerX = getWidth() / 2f;

        // 计算当前可见的温度范围
        int centerIndex = Math.round(currentScrollY / itemHeight);
        int startIndex = centerIndex - 2;
        int endIndex = centerIndex + 2;

        // 确保在有效范围内
        startIndex = Math.max(startIndex, 0);
        endIndex = Math.min(endIndex, MAX_TEMPERATURE - MIN_TEMPERATURE);

        for (int i = startIndex; i <= endIndex; i++) {
            int temperature = MIN_TEMPERATURE + i;
            float y = i * itemHeight - currentScrollY + centerY;

            // 计算距离中心的偏移量
            float offset = Math.abs(y - centerY);
            float scale = Math.max(0, 1 - offset / (2 * itemHeight));

            // 根据距离中心的位置调整字体大小
            float textSize = minTextSize + (maxTextSize - minTextSize) * scale;
            textPaint.setTextSize(textSize);

            // 根据距离调整透明度
            int alpha = (int) (255 * scale);
            textPaint.setAlpha(alpha);

            // 绘制温度文字
            String text = temperature + "°C";
            canvas.drawText(text, centerX, y + textBaseline - centerY, textPaint);

            // 如果是选中项,更新当前温度
            if (Math.abs(y - centerY) < itemHeight / 4) {
                if (currentTemperature != temperature) {
                    currentTemperature = temperature;
                    if (listener != null) {
                        listener.onTemperatureChanged(temperature);
                    }
                }
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 先让手势检测器处理
        if (gestureDetector.onTouchEvent(event)) {
            return true;
        }

        // 处理抬起事件,进行位置修正
        if (event.getAction() == MotionEvent.ACTION_UP) {
            adjustPosition();
            return true;
        }

        return true;
    }

    private void scrollByInternal(float x, float y) {
        float newScrollY = currentScrollY + y;

        // 限制滚动范围
        if (newScrollY < 0) {
            newScrollY = 0;
        } else if (newScrollY > maxScrollY) {
            newScrollY = maxScrollY;
        }

        currentScrollY = newScrollY;
        invalidate();
    }

    private void adjustPosition() {
        // 计算最近的刻度位置
        int targetIndex = Math.round(currentScrollY / itemHeight);
        float targetScrollY = targetIndex * itemHeight;
        Log.i(TAG, "adjustPosition: targetIndex = " + targetIndex + ", targetScrollY = " + targetScrollY);
        // 使用动画平滑滚动到目标位置
        ValueAnimator animator = ValueAnimator.ofFloat(currentScrollY, targetScrollY);
        animator.setDuration(300);
        animator.setInterpolator(new DecelerateInterpolator());
        animator.addUpdateListener(animation -> {
            currentScrollY = (float) animation.getAnimatedValue();
            invalidate();
        });
        animator.start();
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            currentScrollY = scroller.getCurrY();

            // 限制滚动范围
            if (currentScrollY < 0) {
                currentScrollY = 0;
                scroller.forceFinished(true);
            } else if (currentScrollY > maxScrollY) {
                currentScrollY = maxScrollY;
                scroller.forceFinished(true);
            }

            invalidate();
        }
    }

    // 设置温度变化监听器
    public void setOnTemperatureChangeListener(OnTemperatureChangeListener listener) {
        this.listener = listener;
    }

    // 获取当前温度
    public int getCurrentTemperature() {
        return currentTemperature;
    }

    // 设置当前温度
    public void setTemperature(int temperature) {
        if (temperature < MIN_TEMPERATURE || temperature > MAX_TEMPERATURE) {
            return;
        }

        int index = temperature - MIN_TEMPERATURE;
        currentScrollY = index * itemHeight;
        currentTemperature = temperature;
        invalidate();
    }
}

运行截图:

{width=”200” height=”400” loading=”lazy”}

在这个例子中,使用的 gestureDetector 来处理DOWN和MOVE事件,onTouchevent的事件先交由 gestureDetector 来处理,其内部计算具体的手势,是普通的滑动,还是快速的fling,由 computeScroll 回调来驱动滑动 Scroller 计算滑动距离。

onDraw回调里,在距离改变时,第一步计算这个滑动距离下,有哪几个item的文字应该显示到屏幕上,在对每一个温度文字在屏幕上距离显示中心的offset偏移量,根据这个偏移量来计算出文字应该显示多少透明度和字号,最后整体绘制出正确的温度列表。

【Android进阶】APP冷启动流程解析

【Android进阶】APP冷启动流程解析

本文为launcher图标拉起app的全流程解析

从用户手指点击桌面上的应用图标到屏幕上显示出应用主Activity界面而完成应用启动,快的话往往都不需要一秒钟,但是这整个过程却是十分复杂的,其中涉及了Android系统的几乎所有核心知识点。

同时应用的启动速度也绝对是系统的核心用户体验指标之一,多少年来,无论是谷歌或是手机系统厂商们还是各个Android应用开发者,都在为实现应用打开速度更快一点的目标而不断努力。

概要图

cold_start

流程详解

事件传递到launcher图标view

手指按下后,硬件到驱动到系统侧链路暂且不看。

System部分

Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReaderInputDispatcherInputReaderInputDispatcher 是跑在 SystemServer进程中的两个 native 循环线程,负责读取和分发 Input 事件。

  • InputReader 负责从 EventHub 里面把 Input事件 读取出来,然后交给 InputDispatcher 进行事件分发
  • InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装后,寻找并分发到目标窗口;

system_server 的native线程 InputReader 读取到了一个触控事件。它会唤醒 InputDispatcher 去进行事件分发,先放入 InboundQueue 队列中,再去寻找处理事件的窗口,找到窗口后就会放入 OutboundQueue 队列,等待通过socket通信发送到 launcher应用 的窗口中,此时事件处于 WaitQueue 中,等待事件被处理,若5s内没有处理,就会向 systemserver 报ANR异常。

input_event

Launcher部分

Launcher进程接收到之后,通过 enqueueInputEvent 函数放入 “aq” 本地待处理队列中,唤醒 UI线程deliverInputEvent 流程进行事件分发处理,具体交给界面window里的类型来处理。

从View布局树的根节点DecorView开始遍历整个View树上的每一个子View或ViewGroup界面进行事件的分发、拦截、处理的逻辑。

这次的触摸事件被消耗后,Launcher及时调用 finishInputEvent 结束应用的处理逻辑,再通过JNI调用到native层InputConsumer的 sendFinishedSignal 函数通知 InputDispatcher 事件处理完成,及时从 waitqueue 里移除待处理事件,避免ANR异常。

整个处理流程是按照责任链的设计模式进行

Launcher到AMS的binder调用

上一轮 Input事件 传到图标view后,通过一个 ACTION_DOWNTouchEvent 触控事件和多个 ACTION_MOVE 事件,直到最后出现一个 ACTION_UPTouchEvent 事件后,去判断是 click 点击事件。

就开始通过 ActivityManager Binder 调用AMS的 startActivity 服务接口准备启动应用。

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
                IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
                int startFlags, boolean doResume, ActivityOptions options, Task inTask,
                boolean restrictedBgActivity, NeededUriGrants intentGrants) {
        ...
        try {
            ...
            // 添加“startActivityInner”的systrace tag
            Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "startActivityInner");
            // 执行startActivityInner启动应用的逻辑
            result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
                    startFlags, doResume, options, inTask, restrictedBgActivity, intentGrants);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
            ...
        }
        ...
    }

上面AMS的 startActivityUnchecked 函数,开始和结尾都会添加traceTAG记录时间,中间则是调用 startActivityInner 方法来启动应用。这个方法首 先检查当前Activity栈里处于resume状态的Activity ,如果当前 mResumedActivity 不是目标Activity,就通知这个Activity 进入Pause状态

/*frameworks/base/services/core/java/com/android/server/wm/ActivityStack.java*/
private boolean resumeTopActivityInnerLocked(ActivityRecord prev, ActivityOptions options) {
   ...
   // mResumedActivity不为null,说明当前存在处于resume状态的Activity且不是新需要启动的应用
   if (mResumedActivity != null) {
      // 执行startPausingLocked通知桌面应用进入paused状态
      pausing |= startPausingLocked(userLeaving, false /* uiSleeping */, next);
   }
   ...
}

final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping,
            ActivityRecord resuming) {
    ...
    ActivityRecord prev = mResumedActivity;
    ...
    if (prev.attachedToProcess()) {
        try {
             ...
             // 相关执行动作封装事务,binder通知mResumedActivity也就是桌面执行pause动作
             mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
                        prev.appToken, PauseActivityItem.obtain(prev.finishing, userLeaving,
                        prev.configChangeFlags, pauseImmediately));
        } catch (Exception e) {
           ...
        }
     }
     ...
}

Launcher进程把其Activity的 pause 操作执行完毕后,执行 ActivityTaskManager.getService().activityPaused(token) 会将pause完成的结果通知到AMS。

AMS通知Launcher暂停自己的之后,会继续启动应用的逻辑,不等待Launcher进程的pause处理结果。

  • 先判断需要启动应用进程如果存在,调用 realStartActivityLocked
  • 如果进程不存在,就会先调用 startProcessAsync 创建进程。

拉起一个应用进程,具体是AMS通过Socket连接到Zygote进程,后者在开机时会创建好一个服务端。AMS通知Zygote进程去fork一个新进程,即 ZygoteProcess.start(...) 方法。

Zygote开机时就会创建 ZygoteServer 对象,调用 runSelectLoop 进入死循环等待AMS的请求。

/*frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java*/  
   @GuardedBy("this")
    final ProcessRecord startProcessLocked(...) {
        return mProcessList.startProcessLocked(...);
   }
   
   /*frameworks/base/services/core/java/com/android/server/am/ProcessList.java*/
   private Process.ProcessStartResult startProcess(HostingRecord hostingRecord, String entryPoint,
            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags,
            int mountExternal, String seInfo, String requiredAbi, String instructionSet,
            String invokeWith, long startTime) {
        try {
            // 原生标识应用进程创建所加的systrace tag
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
                    app.processName);
            ...
            // 调用Process的start方法创建进程
            startResult = Process.start(...);
            ...
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        }
    }
    
    /*frameworks/base/core/java/android/os/Process.java*/
    public static ProcessStartResult start(...) {
        // 调用ZygoteProcess的start函数
        return ZYGOTE_PROCESS.start(...);
    }
    
    /*frameworks/base/core/java/android/os/ZygoteProcess.java*/
    public final Process.ProcessStartResult start(...){
        try {
            return startViaZygote(...);
        } catch (ZygoteStartFailedEx ex) {
           ...
        }
    }
    
    private Process.ProcessStartResult startViaZygote(...){
        ArrayList<String> argsForZygote = new ArrayList<String>();
        ...
        return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
    }

ZygoteProcess#startViaZygote 函数中,拿到创建进程的参数,返回一个列表,里面含有pid等信息。

startProcess 中会封装相关进程信息请求参数,连接发送到zygote进程的socket服务端最后阻塞等待进程创建的结果。 startProcess 的阻塞工作线程,最终被711线程也就是zygote进程的主线程唤醒。

Zygote进程fork应用进程

fork_process

ZygoteServer 接收到请求后,去fork一个进程,fork采用 copy-on-write技术,这是linux创建进程的标准方法,调用一次,返回两次,返回值有3种类型,父进程里是新的子进程的pid,子进程返回的是0,为负数则表示出错了。

父进程去把pid通过socket发送到AMS,子进程通过调用 handleChildProc 函数,关闭父进程继承来的服务地址,再做一些通用的初始化工作,比如启用Binder机制,执行应用程序的入口函数。

子进程里有三个重要方法:

  1. 应用进程默认的java异常处理机制(可以实现监听、拦截应用进程所有的Java crash的逻辑);
  2. JNI调用启动进程的binder线程池(注意应用进程的binder线程池资源是自己创建的并非从zygote父进程继承的);
  3. 最后通过 RuntimeInit#applicationInit反射创建ActivityThread对象 并调用其“main”入口方法。进入到子进程内部逻辑。
/*frameworks/base/core/java/com/android/internal/os/ZygoteInit.java*/
public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
        // 原生添加名为“ZygoteInit ”的systrace tag以标识进程初始化流程
        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        RuntimeInit.redirectLogStreams();
        // 1.RuntimeInit#commonInit中设置应用进程默认的java异常处理机制
        RuntimeInit.commonInit();
        // 2.ZygoteInit#nativeZygoteInit函数中JNI调用启动进程的binder线程池
        ZygoteInit.nativeZygoteInit();
        // 3.RuntimeInit#applicationInit中反射创建ActivityThread对象并调用其“main”入口方法
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
 }

目标APP的内部逻辑

建立消息机制

ActivityThread 对象的 main() 方法,里面主要分两步。

  • 一是创建主线程并 prepare() 来启动消息循环;
  • 二是通过binder调用AMS的 attachApplication 接口,将自己注册到AMS中。

继续分析主线程消息循环机制的建立, Looper.prepareMainLooper() ,通过prepare,创建MassageQueue队列,准备主线程的Looper,通过ThreadLocal机制实现与主线程的一对一绑定。

/*frameworks/base/core/java/android/app/ActivityThread.java*/
public static void main(String[] args) {
     ...
     // 1.创建Looper、MessageQueue
     Looper.prepareMainLooper();
     ...
     // 2.启动loop消息循环,开始准备接收消息
     Looper.loop();
     ...
}

// 3.创建主线程Handler对象
final H mH = new H();

class H extends Handler {
  ...
}

/*frameworks/base/core/java/android/os/Looper.java*/
public static void prepareMainLooper() {
     // 准备主线程的Looper
     prepare(false);
     synchronized (Looper.class) {
          if (sMainLooper != null) {
              throw new IllegalStateException("The main Looper has already been prepared.");
          }
          sMainLooper = myLooper();
     }
}

private static void prepare(boolean quitAllowed) {
      if (sThreadLocal.get() != null) {
          throw new RuntimeException("Only one Looper may be created per thread");
      }
      // 创建主线程的Looper对象,并通过ThreadLocal机制实现与主线程的一对一绑定
      sThreadLocal.set(new Looper(quitAllowed));
}

private Looper(boolean quitAllowed) {
      // 创建MessageQueue消息队列
      mQueue = new MessageQueue(quitAllowed);
      mThread = Thread.currentThread();
}

以上一切完成后,主线程就有了完整的 Looper、MessageQueue、Handler,此时 ActivityThread 的 Handler 就可以开始处理 Message。

主线程的初始化完成后,主线程就进入阻塞状态,等待 Message,一旦有 Message 发过来,主线程就会被唤醒,处理 Message,处理完成之后,如果没有其他的 Message 需要处理,那么主线程就会进入休眠阻塞状态继续等待。

包括 Application、Activity、ContentProvider、Service、Broadcast 等组件的生命周期函数,都会以 Message 的形式,在主线程按照顺序处理。

Looper循环器,其loop方法开启后,不断地从MessageQueue中获取Message;MessageQueue 就是一个 Message 管理器,队列中是 Message,在没有 Message 的时候,MessageQueue借助Linux的ePoll机制,阻塞休眠等待,直到有Message进入队列将其唤醒。

Message 是传递消息的对象,其内部包含了要传递的内容,最常用的包括 what、arg、callback 等。

将自己的进程注册到AMS

上面是应用内的消息机制建立和初始化,看看AMS怎么处理这个进程的 attach 注册请求的。AMS接收到请求后,通过 oneway类型 的binder调用此进程的 bindApplication 接口,里面会往主线程的消息队列中post一个 BIND_APPLICATION 的消息,触发主线程的 handleBindApplication

/*frameworks/base/core/java/android/app/ActivityThread.java*/
@UnsupportedAppUsage
private void handleBindApplication(AppBindData data) {
    ...
    // 1.创建应用的LoadedApk对象
    data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
    ...
    // 2.创建应用Application的Context、触发Art虚拟机加载应用APK的Dex文件到内存中,并加载应用APK的Resource资源
    final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
    ...
    // 3.调用LoadedApk的makeApplication函数,实现创建应用的Application对象
    app = data.info.makeApplication(data.restrictedBackupMode, null);
    ...
    // 4.执行应用Application#onCreate生命周期函数
    mInstrumentation.onCreate(data.instrumentationArgs);
    ...
}

这个方法里通过AMS发过来的 ApplicationInfo ,创建LoadedApk对象;创建 ApplicationContext ;触发Art虚拟机加载应用APK的Dex文件到内存中;通过 LoadedApk 加载应用的Resource资源;LoadedApkmakeApplication 方法创建 Application 对象;

// /frameworks/base/core/java/android/app/Instrumentation.java
public Application newApplication(ClassLoader cl, String className, Context context)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    Application app = getFactory(context.getPackageName())
            .instantiateApplication(cl, className);
    app.attach(context);
    return app;
}

然后执行 ApplicationattachBaseContext 方法,通过 installContentProviders 创建 ContentProvider ,执行其 onCreate 方法,随后执行 ApplicationonCreate 方法。

Dex文件加载

背景:Java代码在JVM被编译成字节码,再翻译成机器语言来运行。而DVM即Dalvik虚拟机不能和JVM一样能直接运行Java字节码,它只能运行.dex文件。dex文件是由Java的字节码通过Android的dx生成工具来生成的,这个过程就是打包apk的流程。

后面5.0后推出的ART虚拟机,相比Dalvik的JIT实时编译,是在启动时将dex转换成机器码,ART采用了AOT预先编译,在安装apk时就把dex文件转换成可以直接运行的oat文件,其可以支持多dex,大幅提升冷启动速度。缺点是安装速度变慢。

/*frameworks/base/core/java/android/app/ContextImpl.java*/
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
            String opPackageName) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    // 1.创建应用Application的Context对象
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
                0, null, opPackageName);
    // 2.触发加载APK的DEX文件和Resource资源
    context.setResources(packageInfo.getResources());
    context.mIsSystemOrSystemUiContext = isSystemOrSystemUI(context);
    return context;
}

/*frameworks/base/core/java/android/app/LoadedApk.java*/
@UnsupportedAppUsage
public Resources getResources() {
     if (mResources == null) {
         ...
         // 加载APK的Resource资源
         mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader()/*触发加载APK的DEX文件*/, null);
      }
      return mResources;
}

@UnsupportedAppUsage
public ClassLoader getClassLoader() {
     synchronized (this) {
         if (mClassLoader == null) {
             createOrUpdateClassLoaderLocked(null /*addedPaths*/);
          }
          return mClassLoader;
     }
}

private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
     ...
     if (mDefaultClassLoader == null) {
          ...
          // 创建默认的mDefaultClassLoader对象,触发art虚拟机加载dex文件
          mDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(
                    zip, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath,
                    libraryPermittedPath, mBaseClassLoader,
                    mApplicationInfo.classLoaderName, sharedLibraries);
          ...
     }
     ...
     if (mClassLoader == null) {
         // 赋值给mClassLoader对象
         mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader,
                    new ApplicationInfo(mApplicationInfo));
     }
}

/*frameworks/base/core/java/android/app/ApplicationLoaders.java*/
ClassLoader getClassLoaderWithSharedLibraries(...) {
    // For normal usage the cache key used is the same as the zip path.
    return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath,
                              libraryPermittedPath, parent, zip, classLoaderName, sharedLibraries);
}

private ClassLoader getClassLoader(String zip, ...) {
        ...
        synchronized (mLoaders) {
            ...
            if (parent == baseParent) {
                ...
                // 1.创建BootClassLoader加载系统框架类,并增加相应的systrace tag
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
                ClassLoader classloader = ClassLoaderFactory.createClassLoader(
                        zip,  librarySearchPath, libraryPermittedPath, parent,
                        targetSdkVersion, isBundled, classLoaderName, sharedLibraries);
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                ...
                return classloader;
            }
            // 2.创建PathClassLoader加载应用APK的Dex类,并增加相应的systrace tag
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
            ClassLoader loader = ClassLoaderFactory.createClassLoader(
                    zip, null, parent, classLoaderName, sharedLibraries);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            return loader;
        }
}

/*frameworks/base/core/java/com/android/internal/os/ClassLoaderFactory.java*/
public static ClassLoader createClassLoader(...) {
        // 通过new的方式创建ClassLoader对象,最终会触发art虚拟机加载APK的dex文件
        ClassLoader[] arrayOfSharedLibraries = (sharedLibraries == null)
                ? null
                : sharedLibraries.toArray(new ClassLoader[sharedLibraries.size()]);
        if (isPathClassLoaderName(classloaderName)) {
            return new PathClassLoader(dexPath, librarySearchPath, parent, arrayOfSharedLibraries);
        }
        ...
}

上一轮的Context对象创建后,通过 packageInfo.getResources() 去加载加载APK的 Resource 资源赋给 context ,这个方法中需要 getClassLoader 获取类加载器,触发ART虚拟机加载dex文件。

资源文件加载

/*frameworks/base/core/java/android/app/LoadedApk.java*/
@UnsupportedAppUsage
public Resources getResources() {
     if (mResources == null) {
         ...
         // 加载APK的Resource资源
         mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader()/*触发加载APK的DEX文件*/, null);
      }
      return mResources;
}

/*frameworks/base/core/java/android/app/ResourcesManager.java*/
public @Nullable Resources getResources(...) {
      try {
          // 原生Resource资源加载的systrace tag
          Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
          ...
          return createResources(activityToken, key, classLoader, assetsSupplier);
      } finally {
          Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
      }
}

private @Nullable Resources createResources(...) {
      synchronized (this) {
            ...
            // 执行创建Resources资源对象
            ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
            if (resourcesImpl == null) {
                return null;
            }
            ...
     }
}

private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
            @NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
      ...
      impl = createResourcesImpl(key, apkSupplier);
      ...
}

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
            @Nullable ApkAssetsSupplier apkSupplier) {
        ...
        // 创建AssetManager对象,真正实现的APK文件加载解析动作
        final AssetManager assets = createAssetManager(key, apkSupplier);
        ...
}

private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
            @Nullable ApkAssetsSupplier apkSupplier) {
        ...
        for (int i = 0, n = apkKeys.size(); i < n; i++) {
            final ApkKey apkKey = apkKeys.get(i);
            try {
                // 通过loadApkAssets实现应用APK文件的加载
                builder.addApkAssets(
                        (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
            } catch (IOException e) {
                ...
            }
        }
        ...   
}

private @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException {
        ...
        if (key.overlay) {
            ...
        } else {
            // 通过ApkAssets从APK文件所在的路径去加载
            apkAssets = ApkAssets.loadFromPath(key.path,
                    key.sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
        }
        ...
    }

/*frameworks/base/core/java/android/content/res/ApkAssets.java*/
public static @NonNull ApkAssets loadFromPath(@NonNull String path, @PropertyFlags int flags)
            throws IOException {
        return new ApkAssets(FORMAT_APK, path, flags, null /* assets */);
}

private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags,
            @Nullable AssetsProvider assets) throws IOException {
        ...
        // 通过JNI调用Native层的系统system/lib/libandroidfw.so库中的相关C函数实现对APK文件压缩包的解析与加载
        mNativePtr = nativeLoad(format, path, flags, assets);
        ...
}

系统对于应用APK文件资源的加载过程其实就是创建应用进程中的 Resources 资源对象的过程,其中真正实现APK资源文件的I/O解析作,最终是借助于 AssetManager 中通过JNI调用系统Native层的相关C函数实现。

加载应用的 Resource 。上面 getResources() 方法里,创建 ResourcesImpl 时,会调用到 createAssetManager 方法, AssetManager 这是实际加载解析apk的类,通过路径去加载APK文件压缩包。

ApkAssets.loadFromPath(key.path, key.sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0)

通过JNI调用Native层的系统 system/lib/libandroidfw.so 库中的相关C函数实现对APK文件压缩包的解析与加载。

创建Activity

上面 AMS 接收到新进程的 Application 绑定请求之后,反馈其 bindApplication 接口后,立即开始执行启动Activity的流程。简要流程是框架 system_server 进程最终是通过 ActivityStackSupervisor#realStartActivityLocked 函数中,通过 LaunchActivityItemResumeActivityItem 两个类的封装,依次实现 binder调用 通知应用进程这边执行 Activity 的 Launch 和 Resume 动作。

/*frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java*/
@GuardedBy("this")
private boolean attachApplicationLocked(...) {
     ...
     if (app.isolatedEntryPoint != null) {
           ...
     } else if (instr2 != null) {
           // 1.通过oneway异步类型的binder调用应用进程ActivityThread#IApplicationThread#bindApplication接口
           thread.bindApplication(...);
     } else {
           thread.bindApplication(...);
     }
     ...
     // See if the top visible activity is waiting to run in this process...
     if (normalMode) {
          try {
            // 2.继续执行启动应用Activity的流程
            didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
          } catch (Exception e) {
                Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
                badApp = true;
          }
      }
}

/*frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java*/
public boolean attachApplication(WindowProcessController wpc) throws RemoteException {
       synchronized (mGlobalLockWithoutBoost) {
            if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
                // 原生标识attachApplication过程的systrace tag
                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "attachApplication:" + wpc.mName);
            }
            try {
                return mRootWindowContainer.attachApplication(wpc);
            } finally {
                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
            }
       }
}

/*frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java*/
boolean attachApplication(WindowProcessController app) throws RemoteException {
       ...
       final PooledFunction c = PooledLambda.obtainFunction(
                // startActivityForAttachedApplicationIfNeeded执行启动应用Activity流程
                RootWindowContainer::startActivityForAttachedApplicationIfNeeded, this,
                PooledLambda.__(ActivityRecord.class), app,
                rootTask.topRunningActivity());
       ...
}
 
private boolean startActivityForAttachedApplicationIfNeeded(ActivityRecord r,
            WindowProcessController app, ActivityRecord top) {
        ...
        try {
            // ActivityStackSupervisor的realStartActivityLocked真正实现启动应用Activity流程
            if (mStackSupervisor.realStartActivityLocked(r, app,
                    top == r && r.isFocusable() /*andResume*/, true /*checkConfig*/)) {
                ...
            }
        } catch (RemoteException e) {
            ..
        }
}

/*frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java*/
boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
            boolean andResume, boolean checkConfig) throws RemoteException {
         ...
        // 1.先通过LaunchActivityItem封装Binder通知应用进程执行Launch Activity动作       
         clientTransaction.addCallback(LaunchActivityItem.obtain(...));
         // Set desired final state.
         final ActivityLifecycleItem lifecycleItem;
         if (andResume) {
                // 2.再通过ResumeActivityItem封装Binder通知应用进程执行Launch Resume动作        
                lifecycleItem = ResumeActivityItem.obtain(dc.isNextTransitionForward());
         }
         ...
         clientTransaction.setLifecycleStateRequest(lifecycleItem);
         // 执行以上封装的Binder调用
         mService.getLifecycleManager().scheduleTransaction(clientTransaction);
         ...
}

主线程调用到 ActivityThreadhandleLaunchActivity 函数在主线程执行应用 ActivityLaunch 创建动作,这个方法里会执行 performLaunchActivity(r, customIntent) ,其中创建 ActivityContext ,通过反射创建 activity 对象,再调用其 attach 方法,创建 应用窗口PhoneWindow 对象,并配置 WindowManager 。然后通过 mInstrumentation.callActivityOnCreate(activity, r.state) 执行其 onCreate() 周期,在 setContentView 调用中创建DecorView对象。

Activity和窗口创建完成后, ActivityThread 调用 handleResumeActivity 来执行其 onResume() 流程,在 ActivityonResume() 周期回调之后,执行 makeVisible()

然后 WindowManager 执行 addView 动作,开启视图绘制逻辑,创建 ViewRootImpl 对象,并调用其 setView 方法。

/*frameworks/base/core/java/android/app/servertransaction/ResumeActivityItem.java*/
@Override
public void execute(ClientTransactionHandler client, IBinder token,
            PendingTransactionActions pendingActions) {
   // 原生标识Activity Resume的systrace tag
   Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
   client.handleResumeActivity(token, true /* finalStateRequest */, mIsForward,
                "RESUME_ACTIVITY");
   Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

/*frameworks/base/core/java/android/app/ActivityThread.java*/
 @Override
public void handleResumeActivity(...){
    ...
    // 1.执行performResumeActivity流程,执行应用Activity的onResume生命周期函数
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ...
    if (r.window == null && !a.mFinished && willBeVisible) {
            ...
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    ...
                    // 2.执行WindowManager#addView动作开启视图绘制逻辑
                    wm.addView(decor, l);
                } else {
                  ...
                }
            }
     }
    ...
}

public ActivityClientRecord performResumeActivity(...) {
    ...
    // 执行应用Activity的onResume生命周期函数
    r.activity.performResume(r.startsNotResumed, reason);
    ...
}

/*frameworks/base/core/java/android/view/WindowManagerGlobal.java*/
public void addView(...) {
     // 创建ViewRootImpl对象
     root = new ViewRootImpl(view.getContext(), display);
     ...
     try {
         // 执行ViewRootImpl的setView函数
         root.setView(view, wparams, panelParentView, userId);
     } catch (RuntimeException e) {
         ...
     } 
}

setView() 内部会开启硬件加速,调用 requestLayout 来触发界面绘制(measure、layout、draw)动作。通过 Binder 调用 WMSaddView 操作,注册应用窗口,创建 WindowInputEventReceiver 对象,传入本地创建 inputChannel 对象用于后续接收系统的触控事件。最后将 DocorViewparent 设置为自己,所以 ViewRootImpl 不是一个View,但它是所有 View 的顶层 Parent

/*frameworks/base/core/java/android/view/ViewRootImpl.java*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
      synchronized (this) {
         if (mView == null) {
             mView = view;
         }
         ...
         // 开启绘制硬件加速,初始化RenderThread渲染线程运行环境
         enableHardwareAcceleration(attrs);
         ...
         // 1.触发绘制动作
         requestLayout();
         ...
         inputChannel = new InputChannel();
         ...
         // 2.Binder调用访问系统窗口管理服务WMS接口,实现addWindow添加注册应用窗口的操作,并传入inputChannel用于接收触控事件
         res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
         ...
         // 3.创建WindowInputEventReceiver对象,实现应用窗口接收触控事件
         mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
         ...
         // 4.设置DecorView的mParent为ViewRootImpl
         view.assignParent(this);
         ...
      }
}

插入 Activity层级结构和相关概念

activity_window

  • Window是一个抽象类,通过控制DecorView提供了一些标准的UI方案,比如背景、标题、虚拟按键等,而 PhoneWindow 是Window的唯一实现类,在Activity创建后的attach流程中创建,应用启动显示的内容装载到其内部的 mDecor(DecorView)
  • DecorView 是整个界面布局View控件树的根节点,通过它可以遍历访问到整个View控件树上的任意节点;
  • WindowManager 是一个接口,继承自 ViewManager 接口,提供了View的基本操作方法; WindowManagerImp 实现了 WindowManager 接口,内部通过组合方式持有 WindowManagerGlobal,用来操作View; WindowManagerGlobal 是一个全局单例,内部可以通过 ViewRootImplView 添加至窗口中;
  • ViewRootImpl 是所有View的Parent,用来总体管理View的绘制以及与系统WMS窗口管理服务的IPC交互从而实现窗口的开辟; ViewRootImpl 是应用进程运转的发动机,可以看到 ViewRootImpl 内部包含 mView(DecorView)、mSurface、Choregrapher,mView 代表整个控件树,mSurfacce代表画布,应用的UI渲染会直接放到mSurface中,Choregorapher使得应用请求vsync信号,接收信号后开始渲染流程;

View绘制流程

我们的手机屏幕刷新频率有不同的类型,60Hz、120Hz等。60Hz表示屏幕在一秒内刷新60次,也就是每隔16.6ms刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMesure()onLayout()onDraw()这些方法。

同步栏删

requestLayout() 首先进行线程检查,然后给主线程 MessageQueue 队列里增加同步栏删,保证卡住同步消息,只让异步消息通过,直到 VSYNC 信号到来才会执行绘制任务并移除同步屏障。这样可以使绘制消息属于高优先级。

这样在等待 VSYNC 信号的时候主线程什么事都没干?是的。这样的好处是:保证在 VSYNC 信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿。但这样也带来了相对应的代价:

  • 我们的同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行

  • 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息

改善这个问题办法就是:使用异步消息。当我们发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。

可能有读者会觉得,异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?这里我们需要思考一下,什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。

如果异步消息执行时间太长,那不管异步还是同步任务,都会造成界面卡顿。

其次,若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;

原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。所以我们应该注意的一点是:

不可在主线程执行重量级任务,无论异步还是同步。

最后建议的使用方案为,如果需要保证与绘制任务的顺序,使用同步Handler;其他,使用异步Handler。

继续看 requestLayout() 的源码:

/*frameworks/base/core/java/android/view/ViewRootImpl.java*/
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
         // 检查当前UI绘制操作是否发生在主线程,如果发生在子线程则会抛出异常
         checkThread();
         mLayoutRequested = true;
         // 触发绘制操作
         scheduleTraversals();
    }
}

@UnsupportedAppUsage
void scheduleTraversals() {
    if (!mTraversalScheduled) {
         ...
         // 注意此处会往主线程的MessageQueue消息队列中添加同步栏删,因为系统绘制消息属于异步消息,需要更高优先级的处理
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         // 通过Choreographer往主线程消息队列添加CALLBACK_TRAVERSAL绘制类型的待执行消息,用于触发后续UI线程真正实现绘制动作
         mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         ...
     }
}

通过 mChoreographer.postCallback ,往主线程消息队列添加 CALLBACK_TRAVERSAL 绘制类型的待执行消息,用于触发后续UI线程真正实现绘制动作。

Choreographer背景

Choreographer,编舞者,配合系统的VSync垂直同步机制,每次VSync信号到来,就绘制一帧,给app的渲染提供一个稳定的Message处理时机。其在渲染链路中承上启下,统筹处理app的消息和回调,输入事件,动画,Traversal等,到下一次Vsync信号来的时候统一处理,对下他负责接收和请求VSync信号。ViewRootImpl推送待执行的消息之后,Choreographer向系统申请APP的VSync信号,等待信号到来之后,调用到 doTraversal 方法去执行真正的绘制操作。

Vysnc垂直同步

是Android在“黄油计划”中引入的一个重要机制,本质上是为了协调BufferQueue的应用生产者生成UI数据动作和SurfaceFlinger消费者的合成消费动作,避免出现画面撕裂的Tearing现象。Vysnc信号分为两种类型:

  • app类型的Vsync:app类型的Vysnc信号由上层应用中的Choreographer根据绘制需求进行注册和接收,用于控制应用UI绘制上帧的生产节奏。根据第7小结中的分析:应用在UI线程中调用invalidate刷新界面绘制时,需要先透过Choreographer向系统申请注册app类型的Vsync信号,待Vsync信号到来后,才能往主线程的消息队列放入待绘制任务进行真正UI的绘制动作;
  • sf类型的Vsync:sf类型的Vsync是用于控制SurfaceFlinger的合成消费节奏。应用完成界面的绘制渲染后,通过Binder调用queueBuffer接口将缓存数据返还给应用对应的BufferQueue时,会申请sf类型的Vsync,待SurfaceFlinger 在其UI线程中收到 Vsync 信号之后,便开始进行界面的合成操作。

doTraversal绘制操作

/*frameworks/base/core/java/android/view/ViewRootImpl.java*/
void doTraversal() {
     if (mTraversalScheduled) {
         mTraversalScheduled = false;
         // 调用removeSyncBarrier及时移除主线程MessageQueue中的Barrier同步栏删,以避免主线程发生“假死”
         mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
         ...
         // 执行具体的绘制任务
         performTraversals();
         ...
    }
}

private void performTraversals() {
     ...
     // 1.从DecorView根节点出发,遍历整个View控件树,完成整个View控件树的measure测量操作
     windowSizeMayChange |= measureHierarchy(...);
     ...
     if (mFirst...) {
    // 2.第一次执行traversals绘制任务时,Binder调用访问系统窗口管理服务WMS的relayoutWindow接口,实现WMS计算应用窗口尺寸并向系统surfaceflinger正式申请Surface“画布”操作
         relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
     }
     ...
     // 3.从DecorView根节点出发,遍历整个View控件树,完成整个View控件树的layout测量操作
     performLayout(lp, mWidth, mHeight);
     ...
     // 4.从DecorView根节点出发,遍历整个View控件树,完成整个View控件树的draw测量操作
     performDraw();
     ...
}

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
        ...
        // 通过Binder IPC访问系统WMS服务的relayout接口,申请Surface“画布”操作
        int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mTmpFrame, mTmpRect, mTmpRect, mTmpRect, mPendingBackDropFrame,
                mPendingDisplayCutout, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
                mTempControls, mSurfaceSize, mBlastSurfaceControl);
        if (mSurfaceControl.isValid()) {
            if (!useBLAST()) {
                // 本地Surface对象获取指向远端分配的Surface的引用
                mSurface.copyFrom(mSurfaceControl);
            } else {
               ...
            }
        }
        ...
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        ...
        // 原生标识View树的measure测量过程的trace tag
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            // 从mView指向的View控件树的根节点DecorView出发,遍历访问整个View树,并完成整个布局View树的测量工作
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
}

private void performDraw() {
     ...
     boolean canUseAsync = draw(fullRedrawNeeded);
     ...
}

private boolean draw(boolean fullRedrawNeeded) {
    ...
    if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
        ...
        // 如果开启并支持硬件绘制加速,则走硬件绘制的流程(从Android 4.+开始,默认情况下都是支持跟开启了硬件加速的)
        mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
    } else {
        // 否则走drawSoftware软件绘制的流程
        if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
                    return false;
         }
    }
}

View绘制三大步,测量,布局,绘制。

首先需要移除同步栏删,removeSyncBarrier,避免主线程接受不了同步消息假死。

然后再执行具体绘制,从DecorView根节点遍历整个View树,完成measure操作。首次执行traversal操作时,通过Binder调用WMS的relayout接口,实现WMS计算窗口尺寸,向系统的surfaceflinger申请Surface画布操作,再由本地surface获取远端分配的surface的引用。画布有了准备进行layout布局,同样从DecorView根节点遍历,完成布局操作。最后的绘制如果开启了硬件加速,则走GPU硬件绘制,否则走CPU软件绘制。

绘制三大步

Measure 测量流程

在Measure测量的时候,会用到一个 MeasureSpec 类,这个类内部的一个32位的int值,其中高2位代表了 SpecMode ,低30位则代表 SpecSizeSpecMode 指的是测量模式, SpecSize 指的是测量大小。通过位运算来给这个常量的高2位赋值, SpecMode 有三种情况:

  • 00—UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
  • 11—- AT_MOST:最大模式,对应于 wrap_content` 属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
  • 01—–EXACTLY:精确模式,对应于 match_parent 属性和具体的数值,父容器测量出 View所需要的大小,也就是SpecSize的值。

每一个普通View都有一个MeasureSpec属性来对其进行测量。而对于DecorView来说,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定。

View 根据传入的 MeasureSpec 约束和自身的 layout_width/layout_height 属性,计算出自己理想的尺寸。最后必须调用 setMeasuredDimension(int measuredWidth, int measuredHeight) 来保存最终确定的测量结果。

performMeasure 这个方法里,会对一众的 子ViewGroup子View 进行测量。 View的onMeasure方法,实际是看 getDefaultSize() 来解析其宽高的,注意对于View基类来说,为了扩展性,它的两个MeasureSpec,AT_MOST和EXACTLY处理是一样的,即其宽高直接取决于所设置的specSize,所以自定义View直接继承于View的情况下,要想实现 wrap_content 属性,就需要重写onMeasure方法,自己设置一个默认宽高值。

ViewGroup的onMeasure

每个View的本身的onMeasure并不复杂,只需要关注好本身的尺寸就好了。

ViewGroup的Measure方法,它没有onMeasure,有一个 measureChildre() 方法:简单来说就是根据自身的 MeasureSpec和子元素的的 LayoutParams 属性来得出的子元素的 MeasureSpec 属性。它除了需要测量自己的宽与高之外,还需要逐个遍历子 view 以 measure 子 view。

如果 ViewGroup 自身是 EACTLY 的,那么 onMeasure 过程就会简单不少,因为它自身的宽与高是确定的,只需要挨个 measure 子View就可了,而且子View并不影响它本身。当然,要把 paddingmargin 考虑进来。

最为复杂的就是 AT_MOSTViewGroup 自身的宽与高是由其所有子View决定的,这才是最复杂的,也是各个ViewGroup子类布局器需要重点解决的,而且过程各不相同,因为每个布局器的特点不一样,所以过程并不相同,下面来各自讨论一下。

LinearLayout 的测量

它的方向只有两个,可以只分析一个方向,另外一个方向是差不多的,我们就看看垂直布局的measureVertical。

  • 当height mode是EXACTLY的时候,这个时候LinearLayout布局本身的高度是已知的,挨个遍历子view然后measure一下就可以。

  • 比较复杂的情况,是AT_MOST时,这其实也还好,理论上高度就是所有子view的高度之和。

  • 最为复杂的情况是处理weight,这需要很多复杂处理,要把剩余所有的空间按weight来分配,具体比较复杂,有兴趣的可以具体去看源码。这也说明了,为何在线性布局中使用weight会影响性能,代码中就可以看出当有weight要处理的时候,至少多遍历一遍子view以进行相关的计算。

虽然方向是 VERTICAL 时,重点只处理垂直方向,但是width也是需要计算的,但width的处理就要简单得多,如果其是EXACTLY的,那么就已知了;如果是AT_MOST的,就要找子view中width的最大值。

FrameLayout 的测量

其实是最简单的一个布局管理器,因为它对子view是没有约束的,无论水平方向还是垂直方向,对子view都是没有约束,所以它的measure过程最简单。

  • 如果是EXACTLY的,它本身的高度与宽度是确定的,那么就遍历子view,measure一下就可以了,最后再把margin和padding加一下就完事了。

  • 如果是AT_MOST的,那么也不难,遍历子View并measure,然后取子view中最大宽为它的宽度,取最大的高为其高度,再加上margin和padding,基本上就做完了。

因为,FrameLayout的measure过程最为简单,因此系统里很多地方默认用的就是FrameLayout,比如窗口里的root view。

RelativeLayout

这个是最为复杂的,从设计的目的来看,RelativeLayout要解决的问题也是提供了长与宽两个维度来约束子view。

总体过的过程就是要 分别从vertical方向和horizontal方向,来进行两遍的measure ,同时还要计算具体的坐标,实际上RelativeLayout的measure过程是把measure和layout一起做了。

Layout 布局流程

onLayout() 这是 ViewGroup 需要重写的核心方法,对于 View 不需要重写 onLayout()。ViewGroup中的 layout() 方法用来确定子元素的位置,View中的 layout() 方法则用来确定自身的位置。所以是ViewGroup来计算子View的参数,并调用子控件的layout方法。

layout() 方法接收四个参数:左边界 (l)、上边界 (t)、右边界 (r)、下边界 (b)。这些坐标都是相对于 父 View 的坐标系 而言的。

View的layout方法,其中分别传入 left,top,right,bottom 四个参数,表示其距离父布局的四个距离,再走到setFrame,最后到onLayout,这是一个空方法,由继承的类自己实现。

LinearLayout

依然是两个方向,因为LinearLayout的目的就是在某一个方向上对子view进行约束。看layoutVertical就可以了,水平方向上逻辑是一样的。

遍历一次子View即可,从父布局的left, top起始,考虑子view的height 以及上下的padding和margin,依次排列就可以了。需要注意的是,对于left的处理,理论上子view的left就应该等于父布局,因为这毕竟是vertical的,水平上是没有约束的,但是也要考虑Gravity,当然也要把padding和margin考虑进来。最后通过setChildFrame把排列好的坐标设置给子view。

总体来看,线性布局的layout过程比其measure过程要简单不少。

FrameLayout

FrameLayout对子view的排列其实是没有约束的,所以layout过程也不复杂,遍历子view,子view的left和top初始均为父布局,依据其Gravity来做一下排布即可,比如如果Gravity是right,那么子view就要从父布局的右侧开始计算,childRight=parentRight-margin-padding,childLeft=childRight-childWidth,以次类推,还是比较好理解的。

RelativeLayout

前面提到过RelativeLayout是在measure的时候就把坐标都计算好了,它的layout就是把坐标设置给子view,其余啥也没有。

热门八股问题:为什么onResume获取不到View的宽高,而View.post就可以拿到?
  1. onCreate和onResume中无法获取View的宽高,是因为还没执行View的绘制流程。
  2. view.post之所以能够拿到宽高,是因为在绘制之前,会将获取宽高的任务放到Handler的消息队列,等到View的绘制结束之后,便会执行。

Draw 绘制流程

触发逻辑

从上面的讨论中可以看出draw的触发逻辑有两条路:

一是,没有启用硬件加速时,走的软件draw流程,也是一条比较好理解的简单流程: performTraversal->performDraw->draw->drawSoftware->View#draw

二是,启用了硬件加速时,走的是 performTraversal->performDraw->draw->ThreadedRenderer#draw ,到这里就走进了硬件加速相关的逻辑了。

硬件加速的绘制流程

遍历 DecorView 树,递归调用每个子View节点的 updateDisplayListIfDirty 函数,最终完成绘制树的创建。再通过JNI调用到Native层的RenderThread渲染线程,并唤醒渲染线程利用OpenGL执行渲染任务。

软件绘制

ViewRootImpl 是直接调用根节点的 draw() 方法,那么这里便是整个view tree的入口。可先从 View#draw(canvas) 方法看起。

主要分为四步:

  1. 画背景 drawBackground()
  2. 画自己的内容通过onDraw来委派,具体的内容是在onDraw里面做的;
  3. 画子view,通过 dispatchDraw() 方法;
  4. 画其他的东西,如scroll bar或者focus highlight等。

可以重点关注一下这些操作的顺序,先画背景,然后画自己,然后画子view,最后画scroll bar和focus之类的东西。

重点来看看dispatchDraw方法,因为其他几个都相对非常好理解,这个方法主要要靠ViewGroup来实现,因为在View里面它是空的,节点自己只需要管自己就可以了,只有父节点才需要关注如何画子View。

ViewGroup#dispatchDraw

这个方法做一些准备工作,如把 padding 考虑进来并进行clip,后会遍历子View,针对 每个子view调用 drawChild 方法,这实际上就 是调用回了 View#draw(canvas,parent,drawingTime) 方法,注意这个方法是package scope的,也就是说只能供view框架内部调用。这个方法并没有做具体的渲染工作(因为每个View的具体渲染都是在onDraw里面做的),这个方法里面做了大量与动画相关的各种变换。

Canvas对象是从哪里来的

View的渲染过程其实大都是 GUI框架内部的逻辑流程控制 ,真正涉及graphics方面的具体的图形如何画出来,其实都是由Canvas对象来做的,比如如何画点,如何画线,如何画文字,如何画图片等等。

一个Canvas对象从ViewRootImpl传给View tree,就在view tree中一层一层的传递,每个view都把其想要展示的内容渲染到Canvas对象中去。

那么,这个Canvas对象又是从何而来的呢?从view tree的一些方法中可以看到,都是从外面传进来的,view tree的各个方法(draw, dipsatchDraw和drawChild)都只接收Canvas对象,但并不创建它。

从上面的逻辑可以看到Canvas对象有二个来源:

  • 一是在 ViewRootImpl 中创建的,当走软件渲染时,会用 Surface 创建出一个 Canvas 对象,然后传给view tree。从 ViewRootImpl 的代码来看,它本身就会持有一个 Surface 对象,大概的逻辑就是每一个Window对象内,都会有一个用来渲染的 Surface
  • 另外一个来源就是走硬件加速时,会由 hwui 创建出Canvas对象。

合成送显

这三大步走完之后,应用界面的内容用户依然还不可见,需要由 RenderThread 线程的渲染处理,渲染完成后,还需要通过Binder调用 “上帧” 交给 surfaceflinger 进程中进行合成后送显才能最终显示到屏幕上。

总结,应用在UI线程中从根节点DecorView出发,递归遍历每个子View节点,搜集其drawXXX绘制动作并转换成DisplayListOp命令,将其记录到DisplayListData并填充到RenderNode中,最终完成整个View绘制命令树的构建。从此UI线程的绘制任务就完成了。

C++层面的RenderThread 渲染流程

syncFrameState中遍历View树上每一个RenderNode,执行prepareTreeImpl函数,实现同步绘制命令树的操作;调用OpenGL库API使用GPU,按照构建好的绘制命令完成界面的渲染,将前面已经绘制渲染好的图形缓冲区Binder上帧给SurfaceFlinger合成和显示。

SurfaceFlinger作为系统中独立运行的一个Native进程,借用Android官网的描述,其为承上启下的角色,就是通过Surface与不同的应用进程建立联系,接收它们写入Surface中的绘制缓冲数据,对它们进行统一合成。然后对下层,通过屏幕的后缓存区与屏幕建立联系,发送合成好的数据到屏幕显示设备。

图形载体为Buffer,Surface为Buffer封装,管理了多个Buffer,内部是通过BufferQueue来管理的。这是一个生产者消费者模型, 应用进程为生产者,SurfaceFlinger为消费者 。应用进程开始界面渲染之前,通过Binder向 SurfaceFlinger 申请一张可用的buffer,使用CPU或者GPU渲染之后,将缓存数据返回给进程对应的 BufferQueue ,等其可用时申请sf类型的VSync信号,通知 SurfaceFlinger 去消费合成。 SurfaceFlinger 拿取buffer合成结束之后,再度将其置为free状态,返回对应 BufferQueue 中。

Pagination