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

本文记录了Android平台上Window打断动画优化的记录
在Android系统中,绝大多数应用的界面逻辑承载于Activity组件之中,有一些需要在特殊层级显覆盖示的界面,需要使用WindowManager来添加某些特殊type的Window,显示页面内容。
并且,Activity应用的页面进场出场动画,不指定的情况下,都有系统默认的Transition处理。而使用WindowManager来添加和移除的页面,是没有默认动画效果的。

本文记录了Android平台上Window打断动画优化的记录
在Android系统中,绝大多数应用的界面逻辑承载于Activity组件之中,有一些需要在特殊层级显覆盖示的界面,需要使用WindowManager来添加某些特殊type的Window,显示页面内容。
并且,Activity应用的页面进场出场动画,不指定的情况下,都有系统默认的Transition处理。而使用WindowManager来添加和移除的页面,是没有默认动画效果的。

本文介绍了RecyclerView的优化原理,和Compose中的LazyColumn组件的实现原理。
最初,要在Android界面中显示一个列表,使用的组件是 ListView ,但是由于 ListView 的性能问题,在Android 5.0之后,Google引入了 RecyclerView 组件。 RecyclerView 提供一个高度可定制的列表视图,同时保持了良好的性能和用户体验。
RecyclerView 用于在有限的屏幕空间内显示大量数据列表或网格。它是 ListView 和 GridView 的升级版,提供了更好的性能和灵活性。
核心理念:视图回收 (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,通常需要以下几个关键组件协同工作:
RecyclerView 本身 (The ViewGroup):RecyclerView 是一个 ViewGroup,它负责承载和管理列表中的所有视图。ViewHolder (视图持有者):ViewHolder 对象进行定义。ViewHolder 的作用是持有并提供对单个列表项布局中所有视图的引用(例如 TextView、ImageView 等)。ViewHolder 时,它还没有任何关联的数据。RecyclerView 会在需要时将其绑定到其数据。RecyclerView.ViewHolder 来定义自己的 ViewHolder 类。Adapter (适配器):Adapter 负责将你的数据与 ViewHolder 绑定,并管理列表项的创建和更新。RecyclerView 通过在 Adapter 中调用方法来请求视图并将视图绑定到其数据。RecyclerView.Adapter 来定义自己的 Adapter 类。Adapter 主要有三个重要方法:onCreateViewHolder(): 当 RecyclerView 需要一个新的 ViewHolder 来表示列表项时,会调用此方法。你在这里创建 ViewHolder 及其关联的视图布局。onBindViewHolder(): 当 RecyclerView 准备好将数据绑定到 ViewHolder 时,会调用此方法。你在这里获取特定位置的数据,并将其填充到 ViewHolder 的视图中。getItemCount(): 返回列表中项的总数。LayoutManager (布局管理器):LayoutManager 负责在 RecyclerView 中定位和排列列表中的各个元素,并决定何时回收和重用不再可见的项视图。RecyclerView 库提供了几种开箱即用的 LayoutManager:LinearLayoutManager: 将项排列成一维列表(垂直或水平滚动)。GridLayoutManager: 将项排列成二维网格。StaggeredGridLayoutManager: 将项排列成错列的二维网格,每列稍微偏移。LayoutManager 不符合你的需求,你也可以通过扩展 RecyclerView.LayoutManager 抽象类来创建自定义的布局管理器。RecyclerView 在处理每个子项视图时,采用了一套高度优化和解耦的机制,旨在实现高性能的列表滚动,尤其是在处理大量数据时。核心是视图回收 (View Recycling) 和职责分离 (Separation of Concerns)。
下面详细介绍 RecyclerView 是如何处理每个子项视图的:
LayoutManager:布局与可见性管理LayoutManager 是 RecyclerView 处理子项视图的第一个关键参与者。它的主要职责包括:
RecyclerView 中的排列方式,例如垂直线性、水平线性、网格或瀑布流等。它负责测量和放置每个可见的子视图。LayoutManager 会将其附着 (attach) 到 RecyclerView;当视图离开屏幕时,它会将其分离 (detach)。这里的分离并不是销毁,而是将其从 RecyclerView 的视图层级中移除,但保留在缓存中。LayoutManager 会与 Recycler 合作,决定何时回收视图(当视图离开屏幕)以及何时重用视图(当需要显示新项时)。当用户滚动 RecyclerView 时,LayoutManager 会不断计算哪些数据项应该可见。对于这些可见的数据项:
LayoutManager 会尝试使用它。LayoutManager 会通知 Adapter 创建一个新的视图。Recycler (缓存机制):视图回收池RecyclerView 内部有一个强大的 Recycler 机制,它维护了 多个视图缓存池 ,以高效地管理视图的回收和重用:
notifyItemChanged() 操作时,视图可能只是暂时离开屏幕,然后又回来。unbound),因此不需要再次调用 onBindViewHolder()。LayoutManager 会优先从这里查找可重用的视图。ViewHolder,它们已经被从 RecyclerView 中分离。ViewHolder 从 Scrap Heap 无法被重用时,LayoutManager 会尝试从 View Cache 中获取。View Cache 中的 ViewHolder 仍然持有视图引用,但它们可能已经与之前的数据解绑,需要通过 onBindViewHolder() 重新绑定新数据。ViewHolder。ViewHolder 离开 View Cache 或 LayoutManager 明确将其回收时,它会进入 RecycledViewPool。ViewHolder 是按视图类型 (view type) 进行分类存储的。如果你的 RecyclerView 有多种不同的 item 布局,它们会分别存储在各自的池中。RecycledViewPool 取出的 ViewHolder 必须重新绑定数据,即总是会调用 onBindViewHolder()。RecyclerView 实例之间共享的(例如在嵌套 RecyclerView 中),进一步提高了效率。RecyclerView 中的视图。它们没有被回收,也没有进入任何缓存池。Adapter:数据与视图的桥梁Adapter 是数据和视图之间的桥梁,它与 LayoutManager 和 ViewHolder 紧密协作,负责以下工作:
getItemCount(): 告诉 RecyclerView 总共有多少个数据项。getItemViewType(int position):item 布局(例如,一个列表项是图片,另一个是文字),你需要重写这个方法,返回一个唯一的整数来标识不同类型。RecyclerView 会根据 viewType 从 RecycledViewPool 中查找相应类型的 ViewHolder 进行重用,避免混淆不同布局的视图。onCreateViewHolder(ViewGroup parent, int viewType):LayoutManager 需要一个新的 ViewHolder 时(即 Scrap Heap 和 View 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()))。这是数据绑定的核心步骤。ViewHolder:视图引用持有者ViewHolder 是 RecyclerView 性能优化的核心。它的作用是:
ViewHolder 的构造函数中,通过 findViewById() 获取所有需要操作的子视图的引用,并将其存储为成员变量。ViewHolder 被创建并缓存了视图引用,后续无论这个 ViewHolder 被重用多少次,都无需再次调用 findViewById()。直接通过 ViewHolder 内部的成员变量即可访问子视图,大大提高了性能。ViewHolder 也可以作为放置与单个列表项相关的事件监听器(如点击事件)和特定UI更新逻辑的好地方。RecyclerView 被添加到布局中。LayoutManager: RecyclerView 知道如何排列其子项。Adapter: RecyclerView 知道如何获取数据并创建/绑定视图。LayoutManager 向 Adapter 请求足够多的 ViewHolder (onCreateViewHolder) 并绑定数据 (onBindViewHolder) 来填充屏幕,然后将这些 ViewHolder 的视图附着到 RecyclerView。item 滚出屏幕时,LayoutManager 会将其视图从 RecyclerView 中分离。这个 ViewHolder 可能会进入 Scrap Heap 或 View Cache,最终可能进入 RecycledViewPool。item 需要进入屏幕时,LayoutManager 首先尝试从 Scrap Heap 中获取一个可重用的 ViewHolder。Scrap Heap 中没有,它会尝试从 View Cache 中获取。View Cache 中也没有,它会检查 RecycledViewPool 中是否有指定 viewType 的 ViewHolder。LayoutManager 会通知 Adapter 调用 onCreateViewHolder() 来创建一个全新的 ViewHolder。ViewHolder(无论是重用的还是新的),LayoutManager 会通知 Adapter 调用 onBindViewHolder(),将当前位置的数据绑定到 ViewHolder 的视图上。LayoutManager 将这个绑定好数据的 ViewHolder 的视图附着到 RecyclerView 中,使其可见。通过这种精巧的视图回收和职责分离机制,RecyclerView 能够以极高的效率处理动态列表,无论是数量庞大的数据还是复杂的 item 布局,都能提供流畅的用户体验。
在 Jetpack Compose 中,LazyColumn (以及 LazyRow、LazyVerticalGrid 等 Lazy 布局) 是处理大量列表数据的核心组件。与 Android View 系统中的 RecyclerView 类似,LazyColumn 的显示逻辑也基于按需组合 (Composition on Demand) 和视图回收 (View Recycling) 的概念,但它的实现方式与 RecyclerView 略有不同,并且更加“Compose 式”。
LazyColumn 是一个 懒加载 (Lazy Loading) 的列表,它只会在列表项进入屏幕可见区域时才会创建和渲染这些项。这意味着,当列表中有大量数据时,它可以显著减少内存占用和渲染性能。
LazyColumn 的核心显示逻辑LazyColumn 的设计目标是:只组合(Compose)并测量(Measure)当前在屏幕上可见或即将可见的 item,而不是一次性处理所有数据项。
LazyColumn 提供一个数据列表时,它并不会立即为列表中的所有数据项创建对应的 Composable 函数实例。item 应该显示在屏幕上。item 的 Composable 函数才会被执行 (Compose)。这被称为“按需组合”。item 进入可见区域,它们的 Composable 函数才会被调用,从而创建其 UI。滚出屏幕的 item 的 Composable 函数会停止执行,其对应的 UI 节点也会被销毁。LazyColumn 使用了 Compose 的“内容插槽”模式。你不是直接将 Composable 函数传递给 LazyColumn,而是通过 items 或 item DSL(领域特定语言)块来定义每个 item 的内容。LazyColumn { items(myList) { data -> MyItemComposable(data) } }MyItemComposable(data) 就是一个内容插槽,LazyColumn 会根据需要来组合这些内容。RecyclerView 那样有显式的 ViewHolder 概念,但 LazyColumn 内部也实现了高效的回收机制。LazyColumn 组件中,能够被“复用”的主要概念是 Composable 函数的 UI 结构和底层布局对象 (layout objects),而不是像传统 RecyclerView 那样对 View 实例进行回收和重用。item 需要进入屏幕时,如果缓存中存在一个相同类型的 Composable 实例,并且这个实例可以被重用,LazyColumn 会尝试重用它。item 的 UI。这种增量更新是 Compose 性能的关键。LazyListState 和滚动位置管理:LazyColumn 内部维护着一个 LazyListState 对象(通常通过 rememberLazyListState() 创建)。LazyListState 记录了当前列表的滚动位置、第一个可见 item 的索引、可见 item 的偏移量等信息。LazyListState 会更新,并通知 LazyColumn 重新测量和布局可见 item。key 参数的重要性:items 和 item DSL 提供了 key 参数,强烈建议为每个 item 提供一个稳定且唯一的 key。LazyColumn 使用 key 来优化重组和识别 item 的移动、添加或删除。key,LazyColumn 默认使用 item 的索引作为 key。当列表中的 item 顺序发生变化(例如删除或重新排序)时,使用索引作为 key 会导致错误的重用或不必要的重组,甚至可能导致动画效果不佳。key 后,即使数据列表的顺序发生变化,LazyColumn 也能识别出哪些是同一个逻辑 item,从而正确地重用其 Composable 实例并应用正确的动画。LazyColumn 与 RecyclerView 的主要区别 (在视图处理层面)| 特性 | RecyclerView (View System) | LazyColumn (Jetpack Compose) |
|---|---|---|
| 视图创建 | 通过 Adapter 的 onCreateViewHolder 方法,使用 XML inflated 创建 View 实例。 | 通过 Composable 函数的执行(Composition),直接生成 UI 节点。 |
| 视图回收 | ViewHolder 模式,通过 Recycler 缓存 View 对象。 | 内部缓存 Composable 实例,通过 Recomposition 机制重用和更新 UI。没有显式的 ViewHolder 类。 |
| 数据绑定 | Adapter 的 onBindViewHolder 方法负责将数据绑定到 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。开发者不再需要关心繁琐的 findViewById、ViewHolder 和 Adapter 生命周期管理,只需专注于定义每个 item 的 UI 外观和数据绑定逻辑。

本文介绍了 View 框架下,自定义复杂View的一些路线
在 Android 开发过程中,我们经常使用官方提供的 TextView、Button 和 RecyclerView 来构建界面。但当面临复杂、个性化或高性能的 UI 需求时,这些标准组件往往力不从心。这时,自定义 View 就显得很必要了。
自定义 View 不仅仅是简单地重写 onDraw(),它更关乎 Android 系统的测量、布局、性能优化,以及触摸事件的精准处理。
这种方法主要是界面里有多处出现的相同的公共控件,将多个基础子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)
}
效果截图:

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);
每次调用 Canvas 的 draw... 方法时,都需要传入一个配置好的 Paint 对象。
例如绘制文字:
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
super.drawText(text, x, y, paint);
}
Canvas 是一个画布,定义“在哪里”画,它提供了所有绘制图形、文本、位图的方法。所有绘制操作都是基于 Canvas 上的坐标系进行的。
| 核心概念 | 作用描述 | 常用方法(部分) |
|---|---|---|
| 绘制方法 | 用于画出各种图形。 | drawLine(), drawCircle(), drawRect(), drawPath() 等 |
| 坐标系 | 以 View 的左上角为原点 $(0, 0)$,向右为 $x$ 轴正方向,向下为 $y$ 轴正方向。 | |
| 变换 (Matrix) | 对画布进行平移、旋转、缩放等操作,影响后续所有绘制。 | translate(), rotate(), scale() |
| 图层管理 | 允许创建新的绘图层,用于复杂的叠加和混合效果。 | save(), restore() |
| 裁剪 (Clip) | 限制绘制的区域,超出裁剪区域的内容将不会显示。 | clipRect(), clipPath() |
Canvas 提供的 draw 方法只负责“画出”这个动作和位置,至于“画成什么样子”,则完全依赖于传入的 Paint。
这些类用于存储几何图形的尺寸和形状信息,作为 Canvas 绘制方法的参数。
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);
}
}
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);
}
| 方法 | 签名 | 作用 |
|---|---|---|
drawPoint() | drawPoint(float x, float y, Paint paint) | 在指定坐标 $(x, y)$ 绘制一个点。点的形状和大小受 Paint 的 setStrokeCap() (例如设置为圆形) 和 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]$,每四个坐标定义一条线。 |
| 方法 | 签名 | 作用 |
|---|---|---|
drawCircle() | drawCircle(float cx, float cy, float radius, Paint paint) | 在中心点 $(cx, cy)$ 处,绘制一个指定半径的圆形。 |
| 方法 | 签名 | 作用 |
|---|---|---|
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) | 绘制一个圆角矩形。rx 和 ry 分别指定圆角在 $x$ 轴和 $y$ 轴上的半径。 |
| 方法 | 签名 | 作用 |
|---|---|---|
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 的当前位置,更新 View 的 mScrollX/mScrollY,并通过 postInvalidate() 驱动连续动画。实现平滑滚动(例如 fling 或平移)时必须重写。
View 的绘制流程可以简化为以下顺序(通常由系统驱动):
Paint、属性等不依赖尺寸的对象。onMeasure():决定 View 的期望尺寸。onSizeChanged():获取最终尺寸,初始化依赖尺寸的对象。onDraw():绘制 View 的外观。onTouchEvent():处理用户交互。computeScroll():如果在交互中触发了 Scroller,这个方法会驱动滚动动画。这一类往往是无动态变化和交互的,一般直接在渲染绘制阶段使用特殊的手段给其加上特定的显示效果。
例如自定义一个可以渐变颜色的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方法,以达到包裹文字的效果。
@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,比如具体某一个时间点,或者一个其他的什么条件,达成之后,动态地改变内部绘制的参数。
这里举一个时钟表盘的例子:
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偏移量,根据这个偏移量来计算出文字应该显示多少透明度和字号,最后整体绘制出正确的温度列表。

本文为launcher图标拉起app的全流程解析
从用户手指点击桌面上的应用图标到屏幕上显示出应用主Activity界面而完成应用启动,快的话往往都不需要一秒钟,但是这整个过程却是十分复杂的,其中涉及了Android系统的几乎所有核心知识点。
同时应用的启动速度也绝对是系统的核心用户体验指标之一,多少年来,无论是谷歌或是手机系统厂商们还是各个Android应用开发者,都在为实现应用打开速度更快一点的目标而不断努力。

手指按下后,硬件到驱动到系统侧链路暂且不看。
Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReader 和 InputDispatcher 。 InputReader 和 InputDispatcher 是跑在 SystemServer进程中的两个 native 循环线程,负责读取和分发 Input 事件。
InputReader 负责从 EventHub 里面把 Input事件 读取出来,然后交给 InputDispatcher 进行事件分发;InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装后,寻找并分发到目标窗口;system_server 的native线程 InputReader 读取到了一个触控事件。它会唤醒 InputDispatcher 去进行事件分发,先放入 InboundQueue 队列中,再去寻找处理事件的窗口,找到窗口后就会放入 OutboundQueue 队列,等待通过socket通信发送到 launcher应用 的窗口中,此时事件处于 WaitQueue 中,等待事件被处理,若5s内没有处理,就会向 systemserver 报ANR异常。

Launcher进程接收到之后,通过 enqueueInputEvent 函数放入 “aq” 本地待处理队列中,唤醒 UI线程 的 deliverInputEvent 流程进行事件分发处理,具体交给界面window里的类型来处理。
从View布局树的根节点DecorView开始遍历整个View树上的每一个子View或ViewGroup界面进行事件的分发、拦截、处理的逻辑。
这次的触摸事件被消耗后,Launcher及时调用 finishInputEvent 结束应用的处理逻辑,再通过JNI调用到native层InputConsumer的 sendFinishedSignal 函数通知 InputDispatcher 事件处理完成,及时从 waitqueue 里移除待处理事件,避免ANR异常。
整个处理流程是按照责任链的设计模式进行
上一轮 Input事件 传到图标view后,通过一个 ACTION_DOWN 的 TouchEvent 触控事件和多个 ACTION_MOVE 事件,直到最后出现一个 ACTION_UP 的 TouchEvent 事件后,去判断是 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处理结果。
realStartActivityLockedstartProcessAsync 创建进程。拉起一个应用进程,具体是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进程的主线程唤醒。

ZygoteServer 接收到请求后,去fork一个进程,fork采用 copy-on-write技术,这是linux创建进程的标准方法,调用一次,返回两次,返回值有3种类型,父进程里是新的子进程的pid,子进程返回的是0,为负数则表示出错了。
父进程去把pid通过socket发送到AMS,子进程通过调用 handleChildProc 函数,关闭父进程继承来的服务地址,再做一些通用的初始化工作,比如启用Binder机制,执行应用程序的入口函数。
子进程里有三个重要方法:
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);
}
ActivityThread 对象的 main() 方法,里面主要分两步。
prepare() 来启动消息循环;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怎么处理这个进程的 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对象;创建 Application 的 Context ;触发Art虚拟机加载应用APK的Dex文件到内存中;通过 LoadedApk 加载应用的Resource资源;LoadedApk 的 makeApplication 方法创建 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;
}
然后执行 Application 的 attachBaseContext 方法,通过 installContentProviders 创建 ContentProvider ,执行其 onCreate 方法,随后执行 Application 的 onCreate 方法。
背景: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文件压缩包的解析与加载。
上面 AMS 接收到新进程的 Application 绑定请求之后,反馈其 bindApplication 接口后,立即开始执行启动Activity的流程。简要流程是框架 system_server 进程最终是通过 ActivityStackSupervisor#realStartActivityLocked 函数中,通过 LaunchActivityItem 和 ResumeActivityItem 两个类的封装,依次实现 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);
...
}
主线程调用到 ActivityThread 的 handleLaunchActivity 函数在主线程执行应用 Activity 的 Launch 创建动作,这个方法里会执行 performLaunchActivity(r, customIntent) ,其中创建 Activity 的 Context ,通过反射创建 activity 对象,再调用其 attach 方法,创建 应用窗口 的 PhoneWindow 对象,并配置 WindowManager 。然后通过 mInstrumentation.callActivityOnCreate(activity, r.state) 执行其 onCreate() 周期,在 setContentView 调用中创建DecorView对象。
Activity和窗口创建完成后, ActivityThread 调用 handleResumeActivity 来执行其 onResume() 流程,在 Activity 的 onResume() 周期回调之后,执行 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 调用 WMS 的 addView 操作,注册应用窗口,创建 WindowInputEventReceiver 对象,传入本地创建 inputChannel 对象用于后续接收系统的触控事件。最后将 DocorView 的 parent 设置为自己,所以 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);
...
}
}

PhoneWindow 是Window的唯一实现类,在Activity创建后的attach流程中创建,应用启动显示的内容装载到其内部的 mDecor(DecorView) ;DecorView 是整个界面布局View控件树的根节点,通过它可以遍历访问到整个View控件树上的任意节点;WindowManager 是一个接口,继承自 ViewManager 接口,提供了View的基本操作方法; WindowManagerImp 实现了 WindowManager 接口,内部通过组合方式持有 WindowManagerGlobal,用来操作View; WindowManagerGlobal 是一个全局单例,内部可以通过 ViewRootImpl 将 View 添加至窗口中;ViewRootImpl 是所有View的Parent,用来总体管理View的绘制以及与系统WMS窗口管理服务的IPC交互从而实现窗口的开辟; ViewRootImpl 是应用进程运转的发动机,可以看到 ViewRootImpl 内部包含 mView(DecorView)、mSurface、Choregrapher,mView 代表整个控件树,mSurfacce代表画布,应用的UI渲染会直接放到mSurface中,Choregorapher使得应用请求vsync信号,接收信号后开始渲染流程;我们的手机屏幕刷新频率有不同的类型,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,编舞者,配合系统的VSync垂直同步机制,每次VSync信号到来,就绘制一帧,给app的渲染提供一个稳定的Message处理时机。其在渲染链路中承上启下,统筹处理app的消息和回调,输入事件,动画,Traversal等,到下一次Vsync信号来的时候统一处理,对下他负责接收和请求VSync信号。ViewRootImpl推送待执行的消息之后,Choreographer向系统申请APP的VSync信号,等待信号到来之后,调用到 doTraversal 方法去执行真正的绘制操作。
是Android在“黄油计划”中引入的一个重要机制,本质上是为了协调BufferQueue的应用生产者生成UI数据动作和SurfaceFlinger消费者的合成消费动作,避免出现画面撕裂的Tearing现象。Vysnc信号分为两种类型:
/*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测量的时候,会用到一个 MeasureSpec 类,这个类内部的一个32位的int值,其中高2位代表了 SpecMode ,低30位则代表 SpecSize 。 SpecMode 指的是测量模式, SpecSize 指的是测量大小。通过位运算来给这个常量的高2位赋值, SpecMode 有三种情况:
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方法,自己设置一个默认宽高值。
每个View的本身的onMeasure并不复杂,只需要关注好本身的尺寸就好了。
ViewGroup的Measure方法,它没有onMeasure,有一个 measureChildre() 方法:简单来说就是根据自身的 MeasureSpec和子元素的的 LayoutParams 属性来得出的子元素的 MeasureSpec 属性。它除了需要测量自己的宽与高之外,还需要逐个遍历子 view 以 measure 子 view。
如果 ViewGroup 自身是 EACTLY 的,那么 onMeasure 过程就会简单不少,因为它自身的宽与高是确定的,只需要挨个 measure 子View就可了,而且子View并不影响它本身。当然,要把 padding 和 margin 考虑进来。
最为复杂的就是 AT_MOST , ViewGroup 自身的宽与高是由其所有子View决定的,这才是最复杂的,也是各个ViewGroup子类布局器需要重点解决的,而且过程各不相同,因为每个布局器的特点不一样,所以过程并不相同,下面来各自讨论一下。
它的方向只有两个,可以只分析一个方向,另外一个方向是差不多的,我们就看看垂直布局的measureVertical。
当height mode是EXACTLY的时候,这个时候LinearLayout布局本身的高度是已知的,挨个遍历子view然后measure一下就可以。
比较复杂的情况,是AT_MOST时,这其实也还好,理论上高度就是所有子view的高度之和。
最为复杂的情况是处理weight,这需要很多复杂处理,要把剩余所有的空间按weight来分配,具体比较复杂,有兴趣的可以具体去看源码。这也说明了,为何在线性布局中使用weight会影响性能,代码中就可以看出当有weight要处理的时候,至少多遍历一遍子view以进行相关的计算。
虽然方向是 VERTICAL 时,重点只处理垂直方向,但是width也是需要计算的,但width的处理就要简单得多,如果其是EXACTLY的,那么就已知了;如果是AT_MOST的,就要找子view中width的最大值。
其实是最简单的一个布局管理器,因为它对子view是没有约束的,无论水平方向还是垂直方向,对子view都是没有约束,所以它的measure过程最简单。
如果是EXACTLY的,它本身的高度与宽度是确定的,那么就遍历子view,measure一下就可以了,最后再把margin和padding加一下就完事了。
如果是AT_MOST的,那么也不难,遍历子View并measure,然后取子view中最大宽为它的宽度,取最大的高为其高度,再加上margin和padding,基本上就做完了。
因为,FrameLayout的measure过程最为简单,因此系统里很多地方默认用的就是FrameLayout,比如窗口里的root view。
这个是最为复杂的,从设计的目的来看,RelativeLayout要解决的问题也是提供了长与宽两个维度来约束子view。
总体过的过程就是要 分别从vertical方向和horizontal方向,来进行两遍的measure ,同时还要计算具体的坐标,实际上RelativeLayout的measure过程是把measure和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的目的就是在某一个方向上对子view进行约束。看layoutVertical就可以了,水平方向上逻辑是一样的。
遍历一次子View即可,从父布局的left, top起始,考虑子view的height 以及上下的padding和margin,依次排列就可以了。需要注意的是,对于left的处理,理论上子view的left就应该等于父布局,因为这毕竟是vertical的,水平上是没有约束的,但是也要考虑Gravity,当然也要把padding和margin考虑进来。最后通过setChildFrame把排列好的坐标设置给子view。
总体来看,线性布局的layout过程比其measure过程要简单不少。
FrameLayout对子view的排列其实是没有约束的,所以layout过程也不复杂,遍历子view,子view的left和top初始均为父布局,依据其Gravity来做一下排布即可,比如如果Gravity是right,那么子view就要从父布局的右侧开始计算,childRight=parentRight-margin-padding,childLeft=childRight-childWidth,以次类推,还是比较好理解的。
前面提到过RelativeLayout是在measure的时候就把坐标都计算好了,它的layout就是把坐标设置给子view,其余啥也没有。
从上面的讨论中可以看出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) 方法看起。
主要分为四步:
drawBackground();dispatchDraw() 方法;可以重点关注一下这些操作的顺序,先画背景,然后画自己,然后画子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里面做的),这个方法里面做了大量与动画相关的各种变换。
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线程的绘制任务就完成了。
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 中。

本文介绍了JVM虚拟机,Dalvik虚拟机还有ART虚拟机三者之间不同特点的对比
去年通读了深入理解Java虚拟机,对JVM的一系列特性有了系统的了解,然而作为Android开发,却还没有对Android平台特有的两代虚拟机做更为细致的学习,属实有点说不过去。在车机Android系统app的开发中,几乎涉及不到apk的动态安装卸载,一般是参与系统集成编译完就发布镜像,烧写后一直运行。所以对此专题,了解甚少。
JVM跨平台特性的原理是 字节码 ,字节码是一种中间代码,它是一种 面向虚拟机 的代码,而不是面向硬件的代码。每个JRE编译的时候针对每个平台编译,因此下载JRE(JVM、Java核心类库和支持文件)的时候是分平台的,JVM的作用是把平台无关的.class里面的字节码翻译成平台相关的机器码,来实现跨平台。
从问题寻找答案,总是一个有效的学习方法,为什么Android平台当年不直接使用JVM作为硬件抽象之上的虚拟机供应用运行呢?
在早期(Android 4.4之前),Android使用的是Dalvik虚拟机,这是谷歌为安卓系统专门设计和开发的一款虚拟机,主要用于在资源受限的移动设备上高效运行应用程序。不直接使用JVM,主要有以下几个原因:
事实证明,Android使用Dalvik虚拟机的选择是正确的,如果直接使用JVM,在以上这些因素的影响下,Android系统可能真的发展不起来。
JVM虚拟机里面,解释运行的是class文件,而在Dalvik虚拟机里面,解释运行的是dex(即“Dalvik Executable”)文件。
回顾一下class文件的组成:
为了减小执行文件的体积,安卓的Dalvik虚拟机选择dex文件来替代class文件。
class文件合并为dex文件这个过程,主要由 Android SDK 中的dx工具(在较新的Android Gradle插件中被d8工具替代)完成。通过这个转换过程,多个class文件被合并转换成一个更紧凑、更适合移动设备的dex文件,从而减小了应用的大小,提高了加载和执行效率。
class文件转换成dex文件的过程称为”dexing”,主要通过以下步骤完成:
二者格式对比如下:

每个Java线程都有自己的 Java虚拟机栈 ,用于存储方法调用的信息,包括局部变量、部分结果和方法调用/返回等。
当一个方法被调用时,会在栈上创建一个新的 栈帧(Stack Frame) 。栈帧包含局部变量表,操作数栈,指向运行时常量池的引用,方法返回地址等信息。
JVM使用基于栈的指令集,这意味着大多数操作都是通过压栈和出栈来完成的。例如:
int foo(int a, int b) {
return (a + b) * (a - b);
}
转换后的字节码指令为:
int foo(int, int);
Code:
0: iload_1 // 将局部变量a压入栈
1: iload_2 // 将局部变量b压入栈
2: iadd // 弹出栈顶的两个元素,相加,结果压入栈
3: iload_1 // 将局部变量a压入栈
4: iload_2 // 将局部变量b压入栈
5: isub // 弹出栈顶的两个元素,相减,结果压入栈
6: imul // 弹出栈顶的两个元素,相乘,结果压入栈
7: ireturn // 返回栈顶元素作为方法结果
在Dalvik虚拟机中,寄存器是虚拟的,不同于物理CPU中的硬件寄存器。这些虚拟寄存器实际上是存储在内存中的,用于存储局部变量、方法参数、中间计算结果等。
Dalvik虚拟机使用基于寄存器的指令集,这意味着大多数操作都是通过寄存器来完成的。还是上面同样的计算方法:
int foo(int a, int b) {
return (a + b) * (a - b);
}
转换后的字节码指令
0000: add-int v0, v3, v4
0002: sub-int v1, v3, v4
0004: mul-int/2addr v0, v1
0005: return v0
add-int是一个需要两个操作数的指令,其指令格式是:add-int vAA, vBB, vCC。其指令的运算过程,是将后面两个寄存器中的值进行(加)运算,然后将结果放在(第一个)目标寄存器中。其余类似
计算流程如下图:

一个JVM进程所处的Java虚拟机的内存可以用下面这张热门图片概括:

Dalvik虚拟机的内存模型略有不同。
Dalvik虚拟机用来分配对象的堆划分为两部分,一部分叫做Active Heap,另一部分叫做Zygote Heap。
Android系统启动后,会有一个Zygote进程创建第一个Dalvik虚拟机,它只维护了一个堆。以后启动的所有应用程序进程是被Zygote进程fork出来的,并都持有一个自己的Dalvik虚拟机。在创建应用程序的过程中,Dalvik虚拟机采用 COW策略 复制Zygote进程的地址空间。
COW策略,即Copy-On-Write,一开始的时候(未复制Zygote进程的地址空间的时候),应用程序进程和Zygote进程共享了同一个用来分配对象的堆。当Zygote进程或者应用程序进程对该堆 进行写操作时,内核就会执行真正的拷贝操作 ,使得Zygote进程和应用程序进程分别拥有自己的一份拷贝,这就是所谓的COW。因为copy是十分耗时的,所以必须尽量避免copy或者尽量少的copy。
为了实现这个目的,当创建第一个应用程序进程时,会将已经使用了的那部分堆内存划分为一部分作为 Zygote堆 ,还没有使用的堆内存划分为另外一部分称为 Active堆 。
Zygote Heap 堆内存中, 有一部分区域的内存是只读的, 如系统相关的库, 共享库, 预置库, 这些内存数据所有应用公用。这些预加载的类、资源和对象可以在Zygote进程和应用程序进程中做到 长期共享 。这样既能减少拷贝操作,还能减少对内存的需求。
这样只需把zygote堆中的内容复制给应用程序进程就可以了。以后无论是Zygote进程,还是应用程序进程,当 它们需要分配对象的时候,都在Active堆上进行 。这样就可以使得Zygote堆尽可能少地被执行写操作,因而就可以减少执行写时拷贝的操作。
DVM特点内存模型图

Card Table:用于 DVM Concurrtent GC,当第一次进行垃圾标记后,记录垃圾信息。 Heap Bitmap:分为两个,Live Bitmap 用来记录上次 GC 存活的对象,Mark Bitmap 用来记录这次 GC 存活的对象。 Mark Stack:在 GC 的标记阶段使用的,用来遍历存活的对象。
Dalvik垃圾收集主要是 mark-sweep 算法实现的。 mark-sweep 算法分为两个阶段,即mark阶段和sweep阶段。Dalvik的GC过程,可以大致归纳为如下过程:
这里涉及到的一个核心概念就是我们怎么标记对象有没有被引用的,换句说就是通过什么数据结构来描述对象有没有被引用。
事实上,总共使用了两个bitmap, 一个是Live bitmap,一个是Mark bitmap。 这两个bitmap里的每一位对应一个对象,某个对象被引用了,就标1,没引用就标0。
Livebitmap主要用来标记上一次GC时被引用的对象,也就是那些没有被回收的对象,而markbitmap主要用来标记本轮 当前GC有被引用 的对象。因此那些 在Live bitmap中标为1,而在mark bitmap中标为0 的对象,就是需要回收的对象。mark bitmap实际上就是live bitmap的一个子集。
标记的STW现象
在mark阶段,要求除了GC线程外,其他的线程都需要停止,否则就可能不能正确的标记每个对象,因为可能刚标记完又被修改引用等等情况的发生。这种现象叫stop the world,会导致该应用程序中止执行,
在整个mark开始时,GC会先不得不中止一次程序的运行,从而对堆地址空间进行一次遍历,这次遍历可以获得每一个应用程序分配的对象,就能确认每个对象在内存堆中的大小、起始地址等等信息。 这个停顿在dalvik里是不得不做的事情 ,每次GC都会必须触发一次堆地址空间的遍历引起的停顿。
为了减少stop the world带来的负面影响,dalvik的GC采用了分阶段并行标记Concurrent的方案。分为了两个子阶段:
所谓根集对象,就是指在GC线程开始的时候,那些 被全局变量、栈变量和寄存器等引用的对象 。通过这些根集变量,可以顺着它们找到其余的被引用变量,其实这就是可达性分析。比如说,假如发现了一个栈变量引用一个对象,而这个对象又引用了另外一个对象,那这个被引用的对象也会被标记为正在使用。这个标记“被根集对象引用的对象”的过程就是第二个子阶段。
并行策略很容易想到的一个问题,就是在第二个阶段执行的过程中,如果某个运行的线程修改了一个对象了内容,由于很有可能引用了新的对象,所以这个对象也必须要记录起来。否则会发生被引用对象还在使用却被回收。
这种情况出现在只进行部分GC的情况,这时候 Card Table 的作用就是用来记录非GC堆对象对GC的堆对象的引用。
Dalvik的堆空间,分为zygote heap 和 active heap。前者主要存放一些在zygote时就分配的对象,后者主要用于之后新分配的空间,
Dalvik虚拟机进行部分垃圾收集时,实际上就是 只收集在Active heap上分配的对象 。Card Table就是用来记录在Zygote heap上分配的对象 在GC执行过程中 对在Active heap上分配的对象的引用。
Card table由一系列card组成,一个card实际上就是一个字节,它的值分为clean和dirty两种。如果一个Card的值是 CLEAN ,就表示与它对应的对象在Mark第二个子阶段 没有被程序修改过 。如果一个Card的值是 DIRTY ,就意味着 被程序修改过 。对于这些被修改过的对象。需要在Mark第二子阶段结束之后,再次禁止GC线程之外的其它线程执行,以便GC线程再次根据Card Table记录的信息对被修改过的对象引用的其它对象 进行重新标记 。这个二次标记的过程就是非并行的,确保本次Mark流程的标记都是准确的。
由于Mark 第二子阶段被修改的对象不会很多 ,这样就可以保证第二次子阶段结束后,再次执行标记对象的过程是很快的,因而此时对程序造成的停顿非常小。
在mark阶段,主要是标记的第二个子阶段,dalvik是采用递归的方式来遍历标记对象。但是这个递归并不是像一般的递归一样借助一个递归函数来实现,而是使用一个叫做mark stack的栈空间实现。大致过程是:一个被引用的对象在标记的过程中,先被标记,然后放在栈中,等该对象的父对象全部被标记完成后,依次弹出栈中的每一个对象,并标记其引用,然后把其引用再丢到栈中。
采用mark stack栈而不是函数递归的好处是:首先可以 采用并行的方式 来做,将栈进行分段,多个线程共同将栈中的数据递归标记。其次,可以 避免函数堆栈占用较深 。
至此,差不多介绍了dalvik的GC的mark阶段的过程。我们可以发现,在mark阶段,一共有3次停顿:
这3次停顿的时间直接影响了android上应用程序的表现,尤其是 卡顿现象 ,因此ART在这块有重点改进,等会介绍ART上的过程。
其实sweep阶段就很简单了,在mark阶段已经提到过,GC时回收的是在live bitmap里标为1而在mark bitmap里标为0的对象。
而这个mark bitmap实际上就是live bitmap的子集,因此在sweep阶段只需要处理二者的差集即可,在回收掉相应的对象后,只需要 再把live bitmap和mark bitmap的指针调换一下 ,即这次的mark bitmap作为下一次GC时的live bitmap,然后清空live bitmap,等到下一次GC流程开始,用来标记下一次的可回收对象。
Sweep的过程,在ART里没什么太大变化,而由于在android 5.0源码中已经去掉了dalvik,这环节的具体解释就在ART部分分析,但是实际上在sweep阶段dalvik和ART二者没有太大区别,因为主要只是处理相应的bitmap的对应的对象的内存,ART也没有什么优化的地方。
Art虚拟机是在Android 4.4就引入了,这个版本上留了一个开关供调试选择,其实就是一个标记位,手动开关更改这个值,重启系统之后就是使用上次选择的新的虚拟机方案。5.0版本之后,彻底移除了Dalvik虚拟机,完全使用ART虚拟机。
Dalvik之所以要被ART替代包含下面几个原因:
先接上文介绍下Art的垃圾回收策略,再介绍JIT,AOT等编译流程。
ART同样采用了自动GC的策略,并且同样不可避免的使用到了经典的mark-sweep算法。
Art虚拟机的标记清除垃圾回收,根据轻重程度不同,分为三类, sticky,partial,full 。可以看到,ART里的GC的改进,首先就是收集器的多样化。 而根据GC时是否暂停所有的线程分类并行和非并行两类。所以在ART中一共定义了6个mark-sweep收集器。参看art/runtime/gc/heap.cc可见。
根据不同情况,ART会选择不同的GC collector进行GC工作。其实最复杂的就是 Concurrent Mark Sweep 收集器 。如果理解了最复杂的Concurrent Mark Sweep算法,其他5种GC收集器的工作原理就也理解了。同样的,垃圾回收工作从整体上可以划分两个大的阶段:Mark 和 Sweep。
最重要的提升就是这个阶段 只暂停线程一次。将dalvik的三次缩短到一次 ,得到了较大的优化。和dalvik一样,标记阶段完成的工作也是完成从根集对象出发,进行递归遍历标记被引用的对象的整个过程。用到的主要的数据结构也是同样的 live bitmap和mark bitmap,以及card table和在递归遍历标记时用到的mark stack。
一个被引用的对象在标记的过程中先被标记,然后存入mark stack中,等待该对象的父对象全部被标记完成,再依次弹出栈中每一个对象然后,标记这个对象的引用,再把引用存入mark stack,重复这个过程直至整个栈为空。这个过程对mark stack的操作使用以及递归的方法和dalvik的递归过程是一样的。
但是在dalvik小节里提到了,在标记时mark阶段划分成了两个阶段,第一小阶段是禁止其他线程执行的,在mark两个阶段完成后处理card table时也是禁止其他线程执行的。
在ART里做出了改变,即 先Concurrent标记两遍 ,也就是说两个子阶段都可以允许其他线程运行了。然后 再Non-Concurrent标记一遍 。这样就大大缩短了dalvik里的第二次停顿的带来的卡顿时间。这个改进非常重要。
allocation stack
在标记开始阶段,有别于dalvik的要暂停所有线程 进行堆地址空间的遍历 来确定所有的对象的大小,地址信息。ART去掉了这个过程,替代的是 增加了一个叫作allocation stack结构 ,所有新分配的对象的信息会记录到allocation stack。
然后在Mark的时候,再在Live Bitmap中打上live的标记。Allocation stack和live stack其实是一个工作栈和备份栈。当在GC的时候,需要处理allocation stack,那么会把两个stack互换。新分配的对象会压到备份栈中,这个时候备份栈就当作新的工作栈。之前的allocation stack就当作本轮gc的 所有堆对象的记录栈 。这样一来,dalvik在每一次GC时产生的第一次停顿就完全消除了,从而产生了巨大的性能提升。
Live Stack和Live Bitmap名称很像,但其实是完全不同的东西。Live Bitmap用于记录当前VM进程中所有存在的对象,包括未标记和已标记的对象。与Mark Bitmap配合用于确定可回收的垃圾对象。Live Stack是allocation stack的备份栈,主要用于优化GC性能,快速访问新分配对象。
关于card table,和dalvik依旧类似,每个card用一个字节来描述。ART里多了一个结构ModUnionTable,是和card table配合使用的。
前面在ConCurrent的情况下,经过了两轮的递归遍历,基本上已经标记扫描的差不多了。但由于应用程序主线程是在一直运行的,不可避免地会修改之前已经mark过的bitmap。因此,需要 第三遍扫描,这次就需要在stop the world的情况下进行遍历 ,主要过程也就是上文提到的对card table的操作等等。
这次遍历扫的时候,除了重新标记根集以外,还需要扫描card table中Dirty Card的部分。关于live bitmap和mark bitmap的使用,ART和dalvik在这一块没有多少区别。Live Bitmap记录了当前存在于VM进程中所有的未标记的对象和标记过的对象。Mark Bitmap经过了Mark 的过程,记录了当前VM进程中所有被引用的object。Live Bitmap和Mark Bitmap中间的差集,便是所有为被系统引用的object,即是可以回收的垃圾了。
经过了前面3次扫描以及Mark,我们的mark bitmap已经很完整了。但是值得注意的是,由于Sweep的操作是对应于live bitmap,即那些在live bitmap中标记过,却在mark bitmap中没有标记的对象。也就是说,mark bitmap中标记的对象是live bitmap中标记对象的子集。
但目前为止live bitmap标记的对象还不是最全,因为前文有提到过,为了消除dalvik的第一次停顿,在扫描期间,Art计入了allocation stack中的对象,还没有标记。Allocation stack先“搁置”起来不让后面的主线程使用,启用备份的的live stack。
void Heap::SwapStacks() {
allocation_stack_.swap(live_stack_);
}
即在下一次垃圾回收开始时,将allocation stack和live stack进行交换。确保本轮处理的对象信息是最新的。
在完成了mark阶段后,对应已经标好的live bitmap和mark bitmap,需要进入sweep来回收相应的垃圾。Sweep阶段就是把那些二者的差集所占用的内存回收掉。
不同于Dalvik,ART中可以归纳为有一个第三个阶段,就是类似的一个finish阶段。
void MarkSweep::FinishPhase() {
base::TimingLogger::ScopedSplit split("FinishPhase", &timings_);
// Can't enqueue references if we hold the mutator lock.
Object* cleared_references = GetClearedReferences();
Heap* heap = GetHeap();
timings_.NewSplit("EnqueueClearedReferences");
heap->EnqueueClearedReferences(&cleared_references);
......
}
因为之前说过mark bitmap是live bitmap的一个子集,而mark bitmap中包含了所有的正在被引用的的非垃圾的对象,因此需要交换mark bitmap和live bitmap的指针,使mark bitmap作为下一次GC的live bitmap,并且重置新的mark bitmap。
//Clear all of the spaces' mark bitmaps.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() != space::kGcRetentionPolicyNeverCollect) {
space->GetMarkBitmap()->Clear();
}
}
mark_stack_->Reset();
另外,需要指出的是,由于mark stack的目的是为了方便标记的递归,所以在Finish阶段,也需要把mark stack给清空,至于实现可以看以上代码行。
其实sticky mark sweep的主要步骤也是和mark sweep的过程大致一样,主要完成三次并发的mark阶段,然后进行一个stop the world的非并发进行一次对堆对象的遍历。
void StickyMarkSweep::BindBitmaps() {
PartialMarkSweep::BindBitmaps();
WriterMutexLock mu(Thread::Current(), *Locks::heap_bitmap_lock_);
// For sticky GC, we want to bind the bitmaps of all spaces as the allocation stack lets us
// know what was allocated since the last GC. A side-effect of binding the allocation space mark
// and live bitmap is that marking the objects will place them in the live bitmap.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() == space::kGcRetentionPolicyAlwaysCollect) {
BindLiveToMarkBitmap(space);
}
}
GetHeap()->GetLargeObjectsSpace()->CopyLiveToMarked();
}
但是可以通过实现方法发现,有一个 getGcRetenionPolicy ,获取的是一个枚举。
Sticky Mark Sweep 和 Full Mark Sweep 的主要区别为,Full Mark Sweep:回收整个堆内存空间。Sticky Mark Sweep:只回收 自上次 GC 以来新分配的对象和被修改过的对象所在的区域 。
只有符合总是收集的条件的,就把live bitmap和mark bitmap绑定起来。其余的过程和full是一样的。Sticky Mark sweep只扫描自从上次GC后被修改过的堆空间,并且也只回收自从上次GC后分配的空间。Sticky是只回收kGcRetentionPolicyAlwaysCollect的space。不回收其他两个,因此sticky的回收的力度是最小的。作为最全面的full mark sweep, 上面的三个策略都是会回收的。
这是mark sweep收集器里使用的最少的GC收集策略,回收部分堆空间,通常包括年轻代和部分老年代。
按照官方文档,一般是使用sticky mark sweep较多。这里有一个概念就是吞吐率,即一次GC释放的字节数和GC持续的时间(秒)的比值。由于一般是sticky mark sweep进行GC,所以当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。但是在partial执行一次GC后,就仍然会恢复到stick mark sweep收集器。
阅读源码发现,partial重写了父类的成员函数。
其实分析这些可以发现,从full mark sweep到partial mark sweep到stick mark sweep,GC的力度是越来越小的,因为 可以回收的越来越少 。之所以说回收力度大,就是指可以回收的space多,比如上图的partial, 是不回收 kGcRetentionPolicyFullCollect ,但是是会回收 kGcRetentionPolicyAlwaysCollect 的space的。
kGcRetentionPolicyFullCollect 表示该内存空间只在完全(Full)垃圾回收时才会被收集。kGcRetentionPolicyAlwaysCollect 表示该内存空间在每次垃圾回收时都会被收集,无论是哪种类型的 GC。
因此partial mark sweep每次执行一次GC后,就会自动切换到sticky策略,这样才能使系统更流畅得进行GC,并减少了GC带来的消耗。。
其实观察space目录的文件可以发现,有一个新的概念就是large object space。事实上,ART还引入了一个新的的方法就是 大对象存储空间(large object space,LOS) ,这个空间与堆是相互独立的,但是仍然是驻留在应用程序的内存空间中。方便让ART可以更好的管理较大的对象,比如android里的bitmap。在dalvik中,在对堆空间进行分段时,占用空间较大的对象会带来一些问题。例如,在堆里分配一个 bitmap大对象时,由于占用空间较大,可能引起GC的启动次数也会增加,从而增加了开销 。有了LOS,GC收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。
老罗的博客里做的一些实验对比,证明了ART的GC的性能确实比dalvik的要好。
可以看到,在dalvik模式下刚启动支付宝的几秒内,触发了28次GC事件,总共停顿耗时4657ms。而在ART模式下,可以看到一共触发了2次GC事件,共耗时231.099ms。
在dalvik模式下刚启动百度地图的几秒内,触发了26次GC事件,总共停顿耗时5371ms。而在ART模式下,可以看到一共触发了4次GC事件,共耗时497.162ms。
对比可以看到,ART下的GC的性能明显提升了,几乎可以说是 提升了十倍左右 ,这是一个数量级的提升,GC环节带来的性能提升还是非常明显。
在垃圾回收中,复制算法和标记-清除算法的性能优劣取决于具体场景,两者的速度差异主要与堆内存状态、对象存活率以及硬件条件相关。
复制算法,对于 对象存活率低时(如新生代垃圾回收),只需复制少量存活对象,效率极高。复制后堆空间紧凑,无碎片,分配新对象只需移动指针。适合对延迟敏感的场景(如Young GC)。主要缺点有两个,一是内存浪费,需保留一半空间作为空闲区(空间利用率50%)。二是存活率高时性能骤降,若大部分对象存活(如老年代),复制开销比较大。
标记-清除算法,在对象存活率高时,只需标记并清理少量垃圾,无需复制存活对象。内存方面,无需预留空间,适合堆内存紧张的场景。缺点就是内存碎片化,可能导致分配大对象时触发压缩。还有两次遍历开销**:标记和清除阶段需扫描整个堆,存活对象多时较慢。
现代GC通常混合使用多种算法以平衡吞吐量、延迟和空间开销。
若堆内存充足且对象存活率低,复制算法更快。若存活率高或内存有限,标记-清除更高效。
JIT的全称是Just In Time,即 即时编译 ,它”即时”地进行编译,而不是在程序执行前完成所有编译工作。JIT是在运行时进行字节码到本地机器码的编译,这也是为什么Java普遍被认为效率比C++差的原因。无论是解释器的解释还是运行过程中即时编译,都比C++编译出的本地机器码执行多了一个耗费时间的过程。
AOT,即Ahead of Time,即 提前编译 。当APK 在安装的时候 ,系统会通过一个名称为dex2oat的工具 将APK中的dex文件编译成包含本地机器码的oat文件存放下来 。这样做之后,在程序执行的时候,就可以直接使用已经编译好的机器码以加快效率。
下图描述了Dalvik虚拟机与(Android 5.0上的)ART虚拟机在安装APK时的区别:

从这幅图中我们看到:
应用程序编译生成的OAT文件会引用Framework中的代码。一旦系统发生升级,Framework中的实现发生变化,就需要重新修正所有应用程序的OAT文件,使得它们的引用是正确的,这就需要 重新编译所有的应用 。
在应用安装的时候也是一样的问题,AOT会在安装时将应用的dex文件编译为oat文件存下来,让安装时间对比Dalvik虚拟机也更长。
第三个问题是,编译生成的Oat文件中,既包含了原先的Dex文件,又包含了编译后的机器代码。而实际上,对于用户来说,并非会用到应用程序中的所有功能,因此很多时候编译生成的机器码是一直用不到的。一份数据存在两份结果(尽管它们的格式是不一样的)显然是一种存储空间的浪费。
在 Android 7.0 中,Google又为Android添加了即时 (JIT) 编译器。JIT和AOT的配合,是取两者之长,避两者之短:在APK安装时,并不是一次性将所有代码全部编译成机器码。而是在实际运行过程中,对代码进行分析,将热点代码编译成机器码,让它可以在应用运行时持续提升 Android 应用的性能。
JIT编译器补充了ART当前的预先(AOT)编译器的功能,有助于提高运行时性能,节省存储空间,以及加快应用及系统更新速度。相较于 AOT编译器,JIT编译器的优势也更为明显,因为它不会在应用自动更新期间或重新编译应用(在无线下载 (OTA) 更新期间)时拖慢系统速度。
尽管JIT和AOT使用相同的编译器,它们所进行的一系列优化也较为相似,但它们生成的代码可能会有所不同。JIT会利用运行时类型信息,可以更高效地进行内联,并可让堆栈替换 (On Stack Replacement) 编译成为可能,而这一切都会使其生成的代码略有不同。
系统不再像纯AOT时代那样对整个APK进行完全编译。取而代之的是,系统会进行一些轻量级的处理:
这个阶段不会生成大量的本地机器码,从而加快了安装速度。
应用以解释模式开始运行。 JIT编译器开始工作:
编译后的代码存储在JIT代码缓存中,供后续快速访问。同时,系统开始收集应用使用的配置文件信息。
JIT编译器持续工作,不断优化热点代码。系统继续收集和更新应用的使用配置文件。随着时间推移,更多的代码可能被JIT编译。
系统触发”编译守护进程”(compilation daemon)。基于收集的配置文件信息,对应用进行部分AOT编译。这个过程被称为”配置文件引导编译”(profile-guided compilation)。编译结果保存为本地机器码,存储在设备上。
应用启动时,系统会加载之前AOT编译的代码。对于未编译的部分,继续使用解释执行和JIT编译。JIT编译器继续工作,可能会编译之前未被AOT编译的代码。
系统会定期(通常在设备空闲充电时)重新评估和更新AOT编译的代码。这个过程会考虑最新的使用模式,可能会编译新的热点代码,或者放弃编译不再频繁使用的代码。
当系统更新时(例如Android版本升级),之前的AOT编译结果可能会被丢弃。 应用会重新经历上述过程,以适应新的系统环境。
总的来说,这种JIT和AOT的混合策略充分利用了两种方法的优点,在 安装速度、启动时间、运行性能和存储空间使用之间取得了很好的平衡 ,显著提升了Android系统的整体用户体验。

本文介绍了Android,Linux,Java相结合的一些IO模型和底层原理
Java 的 I/O 模型 指的是 Java 如何处理输入和输出(Input/Output)操作的方式,尤其是在涉及文件、网络、控制台等数据流时的读写机制。不同的 I/O 模型在性能、阻塞行为、线程使用等方面有显著差异。
Java 的 I/O 模型经历了几个发展阶段:传统的阻塞式 I/O(java.io)、NIO(java.nio)和 NIO.2(java.nio.file)。
Java 传统的 I/O 基于流的概念,将数据视为连续的字节序列或字符序列。
字节流 ,InputStream 和 OutputStream 是所有字节流的抽象基类,用于处理原始字节数据(例如图片、音频、视频文件)。常见的实现类有 FileInputStream, FileOutputStream, BufferedInputStream, BufferedOutputStream, DataInputStream, DataOutputStream, ObjectInputStream, ObjectOutputStream 等。
字符流 ,Reader 和 Writer 是所有字符流的抽象基类,用于处理字符数据(例如文本文件)。它们处理字符编码,能够将字节流转换为字符流。常见的实现类有 FileReader, FileWriter, BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter 等。
传统的 I/O 是 阻塞式 (Blocking) 的。当一个线程执行 I/O 操作(如读取文件或网络数据)时,它会 一直等待直到操作完成 ,期间该线程无法执行其他任务。这在并发场景下会导致性能问题,因为每个连接可能需要一个独立的线程来处理。适用于客户端数量较少、连接时间较短的场景,例如简单的文件读写。
NIO 在 Java 1.4 中引入,旨在解决传统 I/O 的阻塞问题,提供更高效、可伸缩的 I/O 操作。
基于 通道 (Channel),表示到实体(如文件、网络套接字)的连接,通过通道读写数据。它与传统 I/O 的流不同,通道是双向的,可以同时进行读写操作。
常见实现类有 FileChannel, SocketChannel, ServerSocketChannel, DatagramChannel 等。所有数据都 通过缓冲区读写 。数据从通道读取到缓冲区,或者从缓冲区写入通道。缓冲区提供了对数据的结构化访问,支持在内存中对数据进行高效操作。最常用的是 ByteBuffer,此外还有 CharBuffer, IntBuffer 等。
NIO模型还允许 单个线程管理多个通道 。通过选择器,一个线程可以监控多个通道上的 I/O 事件(如连接就绪、读就绪、写就绪),从而实现非阻塞 I/O。这大大减少了线程创建和切换的开销,提高了服务器的并发处理能力。
NIO 是 非阻塞式 (Non-blocking) 的。当 I/O 操作无法立即完成时,线程不会被阻塞,而是可以去执行其他任务。当 I/O 准备就绪时,选择器会通知线程。相较于传统I/O,其特点有:
适用于高并发、大量连接的场景,如网络服务器。
AIO,也叫 NIO.2 在 Java 7 中引入,主要增加了异步 I/O (Asynchronous I/O) 功能,以及对文件系统操作的增强 (java.nio.file 包)。
异步通道允许在 I/O 操作完成后,通过回调函数或 Future 对象来处理结果,而不需要显式地等待。例如 AsynchronousFileChannel, AsynchronousSocketChannel, AsynchronousServerSocketChannel。还定义了 CompletionHandler 作为I/O 操作完成时调用的回调接口,可以处理成功或失败的情况。
Path/Files,提供了更现代、功能更丰富的文件系统 API,解决了 java.io.File 类的一些局限性,例如更好的异常处理、符号链接支持、元数据访问等。
AIO 是异步式 (Asynchronous) 的。I/O 操作由操作系统执行,当操作完成时,操作系统会通知应用,应用可以注册回调函数来处理结果。这进一步减少了应用层面的线程管理负担。
适用于需要极致性能和扩展性的大型应用,例如高并发网络服务器、大数据处理。
阻塞IO是指在执行IO操作时,如果没有数据可用或者IO操作还没有完成,那么当前线程会被挂起,直到数据准备好或者IO操作完成。这种方式会导致线程阻塞,无法执行其他任务,适用于需要等待IO操作完成的场景。
非阻塞IO是指在执行IO操作时,如果没有数据可用或者IO操作还没有完成,当前线程不会被阻塞,而是会立即返回一个状态码或者错误码,告诉调用者当前IO操作还没有完成。这种方式不会导致线程阻塞,适用于需要同时处理多个IO操作的场景。
Java 中的 NIO 是非阻塞 IO,当用户发起读写的时候,线程不会阻塞,之后,用户可以通过轮询或者接受通知的方式,获取当前 IO 调度的结果。
非直接IO也称为缓冲IO,是大多数操作系统默认的文件访问方式。数据先被 复制到内核缓冲区 ,然后再 从内核缓冲区复制到用户空间 。可以减少实际磁盘操作次数,利用预读(read-ahead)和延迟写(write-behind)优化性能。但是数据需要在内核和用户空间之间 多次拷贝 ,在某些场景下可能增加延迟。适合小文件或随机访问
直接IO绕过内核缓冲区,直接在用户空间和存储设备之间传输数据。数据直接在用户空间和设备间传输,可以 减少数据拷贝次数 。这种方式,每次IO操作都是实际的设备操作。同时要求IO大小和内存对齐符合设备要求。适合大文件传输或已知访问模式的应用
缓冲是针对标准库的
Linux 标准库定义了很多操作系统的基础服务,比如输入/输出、字符串处理等等。Android 操作系统的标准库是 Bionic ,它可是应用层联系内核的桥梁,我们也可以通过 NDK 访问 Bionic。
使用标准库进行 IO 我们称为缓冲 IO,我们读文件的时候,经常遇到,读完一行才会让输出,在 Android 内部也做了类似的处理。
直接是针对内核的
使用 Binder 跨进程传递数据的时候,需要 将数据从用户空间传递到内核空间 ,非直接 IO 也这样,内核空间会多做一层页缓存,如果做直接 IO,应用程序会直接调用文件系统。
Android 的 Binder 通信 既不是 直接 I/O,也不是 非直接 I/O ,而是一种 进程间通信 机制。它主要依赖 内存映射(mmap) 和 内核缓冲区 来实现高效的数据传输,而不是传统的文件 I/O 方式。Binder 使用 mmap() 在内核和用户空间之间建立 共享内存区域,避免数据在用户态和内核态之间的多次拷贝。发送方(Client)和接收方(Server)通过这块共享内存进行数据交换。Binder 驱动维护一个 内核缓冲区,用于临时存储跨进程传递的数据。数据先写入内核缓冲区,再通过共享内存传递给目标进程。Binder 通过 mmap 实现 一次拷贝(用户空间 → 内核缓冲区 → 目标进程用户空间),提高效率。Binder 不属于传统IO,因为它 不涉及磁盘读写,而是纯内存操作 (进程间通信)。
缓冲和非直接 IO 就像 IO 调度的一级和二级缓存,为什么要做这么多缓存呢?因为操作磁盘本身就是消耗资源的,不加缓存频繁 IO 不仅会耗费资源也会耗时。
在 Android 平台上,IO(输入/输出)操作是应用与外部世界(如文件系统、网络、硬件设备)进行交互的基础。
首先确定一个原则,Android 平台的任何耗时操作,都应该在后台线程中进行,主线程需要尽量保持不出现耗时大于16ms的非UI任务,避免出现卡顿现象。主线程的事件循环如果被卡顿5s以上,还会出现ANR的问题。
以下是 Android 平台上常见的 IO 模型及其特点:
这是最简单、最直观的 IO 模型。当一个线程执行 IO 操作时(例如,从文件中读取数据,或者向网络发送数据),该线程会被阻塞,直到 IO 操作完成。编程模型简单,易于理解和实现。
需要注意的是,如果在 Android 的主线程(UI 线程)上执行阻塞 IO 操作,会导致应用无响应(ANR - Application Not Responding)错误,给用户带来糟糕的体验。在高并发场景下,每个连接都需要一个线程,线程切换的开销很大。
所以,仅适用于 非常小的、不频繁的 IO 操作,或者在专门的后台线程中进行。例如应用启动时,需要从 res/raw 或 assets 目录读取一个非常小的、固定的配置字符串,比如一个 API Key 或者一个版本号,并且这个读取操作是在一个 独立的后台线程中 进行的。
线程在进行 IO 操作时,如果数据尚未准备好,IO 调用会立即返回一个状态码,而不是阻塞线程。开发者需要通过轮询(polling)来检查 IO 操作是否完成。避免了线程阻塞,提高了线程的利用率。
这种非阻塞的模式,需要不断轮询,增加了编程的复杂性。而且频繁的轮询会消耗大量的 CPU 资源。
在 Android 开发中,直接使用纯非阻塞 IO 的场景相对较少。例如要自定义一个 网络服务器框架的底层 ,网络通信库或服务器框架(例如,一个模拟 Netty 行为的 Android 本地服务器),你可能会利用 Java NIO 的 SocketChannel 在非阻塞模式下读写数据。这种模式的编程难度很高,需要开发者手动管理缓冲区、检查返回状态码,并且处理各种边缘情况。
这种模型允许单个线程同时监听多个 IO 流(或文件描述符)的事件。当任何一个 IO 流准备好读写时,系统会通知该线程,然后线程可以对相应的 IO 流进行操作。常见的实现包括 select、poll 和 epoll(在 Linux 内核中)。
一个线程可以处理多个连接,避免了大量线程创建和切换的开销。可以避免不必要的阻塞,线程只在有 IO 事件发生时才被唤醒。同样的,编程模型相对复杂,需要对底层系统调用有一定了解。
例如在 Android 应用中,如果需要实现一个轻量级的 本地服务器或者 P2P 连接 ,多路复用 IO 是一种高效的选择。
还有 异步网络请求库的底层 ,许多流行的网络请求库(如 OkHttp)底层都可能利用了多路复用 IO 的思想来管理并发网络连接。
线程发起 IO 操作后,立即返回,无需等待 IO 完成。当 IO 操作真正完成时,系统会通知发起者(通常通过回调函数、事件或 Future/Promise 模式)。
线程无需等待 IO 完成,可以立即执行其他任务,进一步提高了并发性。相对于轮询, 回调函数 的方式可以简化编程逻辑。如果是 RxJava 这种框架,当嵌套的回调过多,可能导致代码难以阅读和维护(Callback Hell)。
使用上,例如 Retrofit + OkHttp + Coroutines 推荐组合。Retrofit 定义接口,OkHttp 执行实际的网络请求,Coroutines (suspend 函数和 Dispatchers.IO) 负责在后台线程执行请求并在请求完成后通知 UI 线程。
还有像大文件和频繁的读写。
suspend fun saveLargeFile(data: ByteArray, fileName: String) {
withContext(Dispatchers.IO) {
val file = File(context.filesDir, fileName)
FileOutputStream(file).use { fos ->
fos.write(data)
}
}
}
一个系统调用的大致工作流程:
绝大部分的非直接IO都会通过系统调用这层桥梁来进行,应用进程不会直接联系内核。
在 Android 平台上,虚拟文件系统 (Virtual File System, VFS) 是一个至关重要的概念,它为应用程序提供了一个统一的文件访问接口,而无需关心底层存储的实际物理结构和文件系统类型。VFS 位于文件系统之上,抽象了不同文件系统之间的差异,使得应用程序可以以相同的方式处理各种文件和目录。
常常有下列的对象(C语言中的结构体)构成:

不过,光有这些对象可不行,VFS 还得知道如何操作它们,所以,每个对象中还存在对应的操作对象:
大伙最熟悉的应该是文件,这是我们能够在进程中实实在在能够操作的,比如,在文件的 file_operation 中,就有我们熟悉的读、写、拷贝、打开、写入磁盘等方法。
超级块和索引节点存在于内存和磁盘,而目录项和文件只存在于内存。
我的理解是对于磁盘,索引节点已经足够记录文件信息,并不需要目录项再来记录层级关系;而对于内存来说,为了节省内存,只会把需要用到的文件和目录项所用到的索引节点加入内存,文件系统只有被挂载的时候超级块才会被加入到内存中。
VFS中的缓存
结合本文中的第一张图,我们会发现,VFS 有目录项缓存、索引节点缓存和页缓存,目录项和索引节点我们都知道什么意思,那页缓存呢?
页缓存是由 RAM 中的物理页组成的,对应着 ROM 上的物理地址。我们都知道,现在主流 Android 的 RAM 访问速度高达是 8.5 GB/S,而 ROM 的访问速度最高只有 6400 MB/S,所以访问 RAM 的速度要远远快于 ROM,页缓存的目的也在于此。
当发起一个读操作的时候,内核会首先检查需要的数据是否在页缓存,如果在,直接从内存中读取,我们称之为缓存命中;如果不在,那么内核在读取数据的时候,将读到的数据放入页缓存,需要注意的是,页缓存可以存入全部文件内容,也可以仅仅存几页。
文件系统
VFS 定义了文件系统的统一接口,具体的实现了交给了文件系统,超级块里面的数据如何组织、目录和索引结构如何设计、怎么分配和清理数据,这都是设计一个文件系统必须考虑的!
说白了,文件系统就是用来管理磁盘里的持久化的数据的,对于 Android 来说,最常见的就是 ext4 和 f2fs。
Ext2 (Second Extended File System) 是 Linux 内核最初使用的文件系统之一,由法国软件开发者 Rémy Card 设计。它取代了早期的 Ext 和 Minix 文件系统,解决了它们在文件大小、文件名长度等方面的限制。尽管现在更常用的是 Ext3 和 Ext4(它们在 Ext2 的基础上增加了日志功能),但理解 Ext2 的基本结构对于理解现代 Linux 文件系统仍然非常重要。
Ext2 文件系统结构
Ext2 文件系统将磁盘空间划分为逻辑块 (Blocks),然后将这些块进一步组织成 块组 (Block Groups)。这种设计旨在减少磁盘碎片,并最小化磁头移动,从而提高性能。
每个块组通常包含以下几个部分:
引导块 (Boot Block):位于磁盘的起始位置,包含用于系统引导的信息。Ext2 文件系统本身并不使用这部分空间。
当应用程序请求读取一个文件时,Ext2 文件系统通常会经历以下 I/O 流程:
open())传入文件路径名。read() 系统调用返回,应用程序可以处理文件数据。虽然大部分的文件系统也都有超级块、索引节点和数据块,但是各个文件系统的实现却大不相同,这就导致了他们的侧重点也不一样。拿 ext4 和 f2fs 来讲,ext4连续读取大文件更强,占用的空间更小;而f2fs随机 IO 更快
说白了,也就是它们对于空闲空间分配和已有的数据管理方式不一致,不同的数据结构和算法导致了不同的结果。
Linux 下面有两大基本设备类型:
这两个设备的区别就是是否能够随机访问。拿属于字符设备的键盘来说,当我们输入 Hello World 的时候,系统肯定不可以先得到得到 eholl wrodl,这样的话,输出就乱套了。而对于闪存来说,常常是看完这个这些数据库组成的图片,又要读间隔很远的数组块的小说内容,所以读取的块在磁盘上肯定不是连续的。
因为内核管理块设备实在太复杂了,所以就出现了管理块设备的子系统,就是上面说的文件系统。
块设备结构
块设备中常用的数据管理单位:
因为 Linux 中常常用的硬盘,这里我有点疑问,这里的管理单位是否和下面闪存管理单位一致?
IO过程
如果当前有 IO 操作,内核会建立一个 bio 结构体的基本容器,它是由多个片段组成,每一个片段都是一小块连续的内存缓冲区。
之后,内核会将这些 IO 请求保存在一个 request_queue 的请求队列中。
如果按照 IO 请求产生的顺序发向块设备,性能肯定难以接受,所以内核会按照磁盘地址对进入队列之前提交的 IO 请求做合并与排序的预操作。
移动设备中常用的持久化存储是 Nand 闪存,UFS 又是 Nand 闪存中的佼佼者,其特点是速度更快、体积小和更省电。当今 Android 旗舰机基本上标配 UFS 3.1,它们只是一块儿很小的芯片。
闪存是一种非易失性存储器,即使掉电了,数据也不会丢。闪存的存储单元从小到大有:
到Cell这一层,再往下就是MOS管了,通过电压控制电子是否进入存储单元。


本文介绍了Android平台上的补间动画,属性动画,帧动画,扩展的mp4,pag,lottie,kanzi,unity
Android 平台上公认的系统层主要的动画类型有以下三种:属性动画,补间动画,帧动画
Android 平台最早的动画系统之一,只能对 View对象 进行动画,也称为视图动画。
且只能对 View 的基本变换属性进行动画,包括:
需要注意的是,补间动画 改变的是绘制位置,而非View的实际位置。 实际上并没有改变 View 的实际位置、大小等属性,它只是改变了 View 的绘制方式。这意味着即使 View 被动画移动了,它的点击事件响应区域仍在原来的位置。
对于简单的变换动画,补间动画的使用相对简单,可以通过 XML 或代码定义。适用于简单的视图变换,例如按钮点击后的放大缩小效果、图片渐入渐出效果等。对性能要求不高的简单 UI 动画。
首先确认好动画的效果,是平移或是透明度等。在res文件等anim文件夹内,建立一个 translate_anim.xml
<!-- 平移动画 -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:fromXDelta="0%"
android:toXDelta="300%"
android:interpolator="@android:anim/linear_interpolator"/>
在代码中读取这个anim对象,再调用view的 startAnimation 方法。
binding.apply {
tvViewAnimation.setOnClickListener {
val anim =
AnimationUtils.loadAnimation(this@ViewAnimationActivity, R.anim.translate_anim)
tvViewAnimation.startAnimation(anim)
}
}
我们就可以看到这个 textView 会平移到其三倍宽度的地方, XDelta 属性即为这个 View 自身宽度的尺度。动画完结view的显示会回到原来的位置,如果想要停在目标位置,可以设置 anim.fillAfter = true 。
在运动过程中,点击View原来的位置,可以看到动画重新开始了,可以说明View的实际位置没有改变。
Android 系统会不断调用 View 的 onDraw() 方法,在onDraw()方法中,动画会根据当前时间计算出View应该处于的变换状态,然后应用这些变换(通过Canvas的translate()、scale()、rotate()、alpha()等方法),最后绘制View的内容。不断的重绘就形成了动画效果。
而View的实际位置是在 Layout 布局阶段确定的,补间动画并没有改变View的上下左右边界,所以触摸事件的生效区域还在最初 布局 阶段确定的原位置。
属性动画系统是 Android 3.0 (API level 11) 引入的,它允许你对 任何对象的任何属性 进行动画。这意味着你不仅可以动画视图的属性(如位置、大小、透明度、旋转),还可以动画自定义对象的属性,甚至那些不直接绘制到屏幕上的属性。
属性动画的核心是改变属性的值。你可以指定动画的起始值和结束值,系统会根据时间插值器 (TimeInterpolator) 计算中间值,并通过一个估值器 (TypeEvaluator) 将计算出的值应用到目标对象的属性上。
属性动画不局限于 View,可以对任何 Java 对象进行动画,只要该对象有一个 setter 方法来设置要动画的属性。
有丰富的 API 来控制动画的各个方面,包括:
属性动画的使用相对复杂,需要创建一个 Animator 对象,并指定要动画的属性和目标值。然后,将这个 Animator 对象应用到目标对象上。
针对View的属性动画
tvPropAnimation.apply {
setOnClickListener {
val animator =
ObjectAnimator.ofFloat(this, "translationX", 0f, this.width * 3f)
animator.duration = 2000
animator.interpolator = LinearInterpolator()
animator.start()
}
}
这里我们可以看到,我们创建了一个属性动画,这个动画会让这个 textView 从自身的X轴方向平移到自身宽度三倍的位置。和上面补间动画一样的效果。
针对变量的属性动画
使用ValueAnimator来创建属性动画,ValueAnimator会根据时间插值器计算出中间值,并通过一个估值器将计算出的值应用到目标对象的属性上。
tvVarPropAnimation.apply {
setOnClickListener {
val valueAnimator = ValueAnimator.ofInt(this.width, this.width * 3)
valueAnimator.duration = 2000
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.addUpdateListener { animation ->
this.width = animation.animatedValue as Int
}
valueAnimator.start()
}
}
这个动画会把TextView的宽度在2s内扩大3倍。
帧动画是指通过按顺序显示一系列 Drawable 资源来创建的动画,类似于电影胶片的原理。
由于需要加载多张图片,如果图片数量过多或分辨率过高,可能会占用较多的内存资源。通常在 XML 文件中定义,指定每个帧的 Drawable 资源和显示时长。
播放序列帧图片,例如加载动画、游戏中的角色跳跃动画、爆炸效果等。精确控制每一帧显示内容的场景。
在 drawable 文件夹内,建立一个 frame_anim.xml 的文件。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true">
<item
android:drawable="@drawable/fragnance_open_00"
android:duration="50" />
<item
android:drawable="@drawable/fragnance_open_01"
android:duration="50" />
<item
android:drawable="@drawable/fragnance_open_02"
android:duration="50" />
<item
android:drawable="@drawable/fragnance_open_03"
android:duration="50" />
</animation-list>
在代码中读取这个anim对象,一般会使用一个ImageView来承载,触发后,调用 ImageView 的 setBackgroundResource 方法。
binding.apply {
ivFrameAnimation.setOnClickListener {
ivFrameAnimation.setBackgroundResource(R.drawable.frame_anim)
(ivFrameAnimation.background as AnimationDrawable).start()
}
}
帧动画的原理是通过不断地切换 Drawable 资源来实现的。系统会根据每个 Drawable 的显示时长,依次显示每个 Drawable,形成动画效果。
在现代 Android 开发中,属性动画 是推荐的首选动画方式,因为它提供了无与伦比的灵活性和强大的控制能力。视图动画和帧动画在某些特定场景下仍有其用武之地,但通常属性动画能够实现它们的功能,并且效果更好。
此外,对于 Jetpack Compose 这种声明式 UI 框架,也有其独特的动画 API,如 animateFloatAsState、AnimatedVisibility 等,它们在底层也是基于属性动画的原理实现的,提供了更简洁的动画开发体验。
Lottie 是一个由 Airbnb 开发的开源动画库,它的核心理念是将 AE 中创建的动画导出为轻量级的 JSON 文件,然后 Lottie 库可以在各种平台上(包括 Android, iOS, Web, React Native, Windows, HarmonyOS 等)原生渲染这些动画。
Lottie 动画的优势
JSON 文件解析:
原生渲染
Canvas 进行 2D 渲染,并结合原生的 ValueAnimator 等动画组件来实现动画效果。Canvas 的绘图方法(如 drawPath, drawCircle, drawRect 等)将其绘制出来。添加依赖:
在 build.gradle (Module: app) 中添加 Lottie 库的依赖。
dependencies {
implementation 'com.airbnb.android:lottie:6.0.0' // 替换为最新版本
}
放置 JSON 文件
将导出的 .json 动画文件放置在 src/main/assets 目录下。
在布局中添加 LottieAnimationView:
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_fileName="your_animation.json"
app:lottie_autoPlay="true"
app:lottie_loop="true" />
app:lottie_fileName:指定位于assets目录下的 JSON 文件名。app:lottie_autoPlay:是否自动播放。app:lottie_loop:是否循环播放。
在代码中控制 你也可以通过代码来加载和控制动画。
LottieAnimationView animationView = findViewById(R.id.animation_view);
// 从 assets 加载
animationView.setAnimation("your_animation.json");
// 从 raw 资源加载
// animationView.setAnimation(R.raw.your_animation);
// 从 URL 加载
// animationView.setAnimationFromUrl("https://example.com/your_animation.json");
animationView.playAnimation(); // 播放
animationView.pauseAnimation(); // 暂停
animationView.cancelAnimation(); // 取消
animationView.loop(true); // 设置循环
animationView.setSpeed(1.5f); // 设置播放速度
animationView.setProgress(0.5f); // 设置播放进度
// ... 更多控制 API
Lottie 是现代移动和 Web 应用中实现高质量动画的强大工具,极大地简化了设计师和开发者之间的协作流程,并提供了出色的性能和灵活性。
PAG动画 是由腾讯开发的一种高效的动画渲染解决方案,全称为 Portable Animated Graphics。它主要用于在移动端、Web 和桌面应用中实现高性能的矢量动画播放,广泛应用于社交、广告、游戏和短视频等领域。
PAG动画的核心特点:
.pag 文件),比JSON格式的Lottie(.json)更紧凑,加载更快。PAG动画的使用相对简单,主要分为以下几个步骤:
导包
tencent-libpag = { module = "com.tencent.tav:libpag", version.ref = "tencentLibpag" }
放置源文件
将设计师所提供的 .pag动画 文件导入到项目中,一般放置于assets文件夹。
xml布局添加承载的pagView
<com.tencent.libpag.PAGView
android:id="@+id/pagView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
代码中加载动画
// 调用
playPAGView(pagView, "anim_file.pag")
//开始播放动效
fun playPAGView(pagView: PAGView?, fileName: String) {
LogUtils.i(TAG, "playPAGView: fileName:$fileName")
if (pagView != null) {
if (pagView.isPlaying) {
pagView.stop()
}
pagView.visibility = View.VISIBLE
val pf = PAGFile.Load(appContext.assets, fileName)
pagView.composition = pf
pagView.setRepeatCount(-1)
pagView.play()
}
}
PAG动画凭借其高性能和灵活性,逐渐成为替代Lottie的主流方案,特别适合需要复杂动效和实时编辑的场景。
整体架构如下:
┌───────────────────────┐
│ Android App │
├───────────────────────┤
│ PAG SDK (Java/Kotlin)│
├───────────┬───────────┤
│ 文件解析层 │ 渲染引擎层 │
├───────────┴───────────┤
│ OpenGL ES/Skia │
└───────────────────────┘
(1) 文件解析与数据解码
.pag 文件格式:采用二进制编码(而非 Lottie 的 JSON),体积更小,解析更快。解析时仅解码当前帧所需的数据,减少内存占用。采用了多线程解码,在后台线程解析动画数据,避免阻塞 UI 线程。
(2) 渲染引擎(Skia + OpenGL ES)
PAG 的渲染核心依赖: Skia:处理矢量路径(Path)、颜色渐变、遮罩等 2D 图形操作。 OpenGL ES:实现硬件加速,通过 GPU 高效合成图层(类似游戏渲染)。
渲染流程:
图层树构建:解析 .pag 文件后生成图层树(类似 AE 的图层结构)。 帧数据计算:根据当前时间戳计算各图层的变换(位移、旋转、透明度等)。
GPU 绘制:
矢量路径 → 由 Skia 转换为 GPU 可识别的网格(Mesh)。 位图/图片 → 使用 OpenGL 纹理(Texture)渲染。 特效(模糊、渐变)→ 通过 GLSL 着色器实现。
(3) 动画控制与动态修改
时间轴驱动:基于 ValueAnimator 或 Choreographer 同步帧率(通常 60FPS)。
动态替换:
文本替换:运行时修改 PAGTextLayer 的内容。 图片替换:通过 PAGImageLayer 绑定 Bitmap 对象。 播放控制:支持播放、暂停、循环、进度跳转等。
简单来说,Lottie动画的文件简单小巧,文件主要基于JSON打包,跨平台特性比较好。
而PAG动画可实现的效果更多,同时文件采用了二进制压缩,二进制格式通常具有更高的压缩率和解析速度,但可读性较差,不易直接修改。
选择 Lottie 还是 PAG 取决于你的具体需求:
这两个动画比较详细的介绍,此前已经有一篇记录。

本文介绍了Android平台上的一些常用的硬件设备相关的api总结。
纲要转载自掘金老哥的一篇文章,实机测试及扩展而来。 【掘金原文】
Android中没有提供特定的方法来判断设备是手机还是平板,只能通过别的方式来间接判断,比如通过判断屏幕尺寸。
infoText.text = checkIsTablet()
private fun checkIsTablet(): String {
val metrics = resources.displayMetrics
val widthInches = metrics.widthPixels / metrics.xdpi
val heightInches = metrics.heightPixels / metrics.ydpi
val diagonalInches = sqrt(widthInches.pow(2.0f) + heightInches.pow(2.0f))
return if (diagonalInches >= 7.0) {
"手机还是平板:平板"
} else {
"手机还是平板:手机"
}
}
其实在折叠屏没出现的时候,判断手机或者是平板使用上述方法还是够用的,但是在折叠屏面前就显得信心不足了,折叠屏一展开,那就是一个长着平板脸的手机,为了识别折叠屏,Android10出来了一个新的感应器类型TYPE_HINGE_ANGLE,可以通过是否存在这种感应器来识别折叠屏。
private fun checkIsFoldScreen(): String {
val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val hingeAngleSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
return if (hingeAngleSensor == null) {
"是否折叠屏:否"
} else {
"是否折叠屏: 是"
}
}
如果说想要具体拿到折叠屏的状态,比如是全展开还是半展开,或者收起状态,就要使用Jetpack WindowManager这个库了,分别可以通过以下api拿到不同的状态。

这两个值相信基本每个项目都会用到,屏幕密度一般用来判断屏幕适配,加载不同的图片资源,密度比例一般用来单位换算,这俩值都可以通过DisplayMetrics来获得
infoText.text = checkScreenDpiAndDensity()
private fun checkScreenDpiAndDensity(): String {
val displayMetric = resources.displayMetrics
val dpi = displayMetric.densityDpi
val density = displayMetric.density
return "屏幕密度:${dpi} 密度比例:${density}"
}
private fun checkScreenPixel(): String {
val displayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display?.getRealMetrics(displayMetrics)
}else{
windowManager.defaultDisplay.getRealMetrics(displayMetrics);
}
val wPixel = displayMetrics.widthPixels
val hPixel = displayMetrics.heightPixels
return "像素(宽):${wPixel} 像素(高):${hPixel}"
}
物理尺寸在安卓上单位是英寸,它表示一个屏幕对角线的长度,至于如何计算对角线,就要用到上学时候用到的勾股定理,x,y分别是屏幕的宽高,注意的是由于单位是英寸,所以也要把上面计算出来的像素转换成英寸,具体代码如下
private fun checkPhysicalSize(): String {
val displayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display?.getRealMetrics(displayMetrics)
}else{
windowManager.defaultDisplay.getRealMetrics(displayMetrics);
}
val widthInches = displayMetrics.widthPixels / displayMetrics.xdpi
val heightInches = displayMetrics.heightPixels / displayMetrics.ydpi
val diagonalInches = sqrt(
widthInches.pow(2.0f) + heightInches.pow(2.0f)
)
return "物理尺寸 $diagonalInches"
}
刷新率一般就是指Android屏幕上每秒更新画面的频率,单位是赫兹,正常来讲,普通设备的刷新率都为60赫兹,获取刷新率的代码如下
private fun checkRefreshRate(): String {
val mDisplay = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
}else{
windowManager.defaultDisplay
}
return "刷新率 ${mDisplay?.refreshRate}"
}
有的设备支持广色域,有的设备仅仅支持标准色域,广色域的意思是屏幕可以显示比标准色域(sRGB)更加丰富的颜色范围,判断一个设备是否支持广色域的方式如下
private fun checkColorGamut(): String {
val config: Configuration = resources.configuration
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val isWideColorGamut: Boolean = config.isScreenWideColorGamut
val support = if(isWideColorGamut) "支持" else "不支持"
return "是否支持广色域模式:${support}"
}
return "不支持广色域"
}
一般来讲应用想获取内存信息,用的最多的就是通过Runtime来获取,可以通过它获取应用最大可用内存,当前分配的内存,当前空闲内存,已使用内存
infoText.text = checkMemoryRuntime()
private fun checkMemoryRuntime(): String {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory() // 应用最大可用内存
val totalMemory = runtime.totalMemory() // 当前分配的内存
val freeMemory = runtime.freeMemory() // 当前空闲内存
val usedMemory = totalMemory - freeMemory // 已使用内存
return "最大可用内存:${maxMemory} 当前分配的内存:${totalMemory} 当前空闲内存:${freeMemory} 已使用内存${usedMemory}"
}
还有一种方式就是通过获取MemoryInfo来拿到内存信息,比如总内存,当前空闲内存以及判断内存是否过低.
infoText.text = checkMemoryMemoInfo()
private fun checkMemoryMemoInfo(): String {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
val totalMem = memoryInfo.totalMem
val availMem = memoryInfo.availMem
val lowMemory = memoryInfo.lowMemory
return "总内存:${totalMem} 当前空闲内存:${availMem} 内存是否过低${lowMemory}"
}
可以通过StatFs来获取外部存储以及内存存储容量
//外部存储
infoText.text = checkExternalStorageInfo()
private fun checkExternalStorageInfo(): String {
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
val path = Environment.getExternalStorageDirectory() // 外部存储根目录
val stat = StatFs(path.path)
val blockSize = stat.blockSizeLong
val totalBlocks = stat.blockCountLong
val availableBlocks = stat.availableBlocksLong
val totalSize = blockSize * totalBlocks
val availableSize = blockSize * availableBlocks
val usedSize = totalSize - availableSize
return "外部存储总容量:${totalSize} 外部存储可用容量:${availableSize} 外部存储已用容量:${usedSize}"
}
return ""
}
//内部存储
infoText.text = checkInternalStorageInfo()
private fun checkInternalStorageInfo(): String {
val path = Environment.getDataDirectory() // 内部存储根目录
val stat = StatFs(path.path)
val blockSize = stat.blockSizeLong // 每个block的大小
val totalBlocks = stat.blockCountLong // 总block数
val availableBlocks = stat.availableBlocksLong // 可用block数
val totalSize = blockSize * totalBlocks // 总容量
val availableSize = blockSize * availableBlocks // 可用容量
val usedSize = totalSize - availableSize // 已用容量
return "内部存储总容量:${totalSize} 内部存储可用容量:${availableSize} 内部存储已用容量:${usedSize}"
}
获取CPU的内核数量很简单,Runtime类中有现成的方法
infoText.text = checkCPUcoreNumber()
private fun checkCPUcoreNumber() =
"cpu核心数 : ${Runtime.getRuntime().availableProcessors()}"
infoText.text = checkCPUArchitecture()
private fun checkCPUArchitecture() =
"cpu架构:${Build.SUPPORTED_ABIS[0]}"
fun getCpuFreq(): String? {
try {
val reader =
BufferedReader(FileReader("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq"))
val freq = reader.readLine()
reader.close()
// 将频率从 kHz 转换为 MHz
return "${(freq.toLong() / 1000)} MHz"
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
infoText.text = checkCPUHardware()
private fun checkCPUHardware() =
"硬件信息:${Build.HARDWARE}"
同样的没有任何api可以直接去判断设备是否有root权限,我们只能从以下几个方式去判断: 判断检查是否存在相关root文件
var fileRooted = false
val paths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin/su"
)
for (path in paths) {
if (File(path).exists()) {
fileRooted = true
}
}
检查是否存在su命令
var suCmdExest = false
var process: Process? = null
try {
process = Runtime.getRuntime().exec(arrayOf("which", "su"))
val reader = BufferedReader(InputStreamReader(process.inputStream))
suCmdExest = reader.readLine() != null
} catch (e: Exception) {
suCmdExest = false
} finally {
process?.destroy()
}
检查 Build.TAGS 里面是否存在 test-keys
```kotlinvar testKeys = false val buildTags = Build.TAGS testKeys = buildTags != null && buildTags.contains(“test-keys”)
**执行su命令**
```kotlin
var suCmdExecute = false
var suprocess: Process? = null
try {
suprocess = Runtime.getRuntime().exec("su")
val out = suprocess.outputStream
out.write("exit\n".toByteArray())
out.flush()
out.close()
suCmdExecute = suprocess.waitFor() == 0
} catch (e: java.lang.Exception) {
suCmdExecute = false
} finally {
suprocess?.destroy()
}
Magisk 文件是否存在
var giskFile = false
val magiskPaths = arrayOf(
"/sbin/.magisk",
"/sbin/magisk",
"/cache/.disable_magisk",
"/cache/magisk.log",
"/data/adb/magisk",
"/data/adb/modules",
"/data/magisk",
"/data/magisk.img"
)
for (path in magiskPaths) {
if (File(path).exists()) {
giskFile = true
}
}
所有条件综合判断:
val gotRoot = fileRooted || suCmdExest || testKeys || suCmdExecute || giskFile
这个也是在应用当中经常会用到的一个属性,判断设备是连接的是wifi,还是连接的是2,3,4,5G网络,首先通过获取 NetworkCapabilities 来判断是否连接的是wifi还是移动网络。
private fun checkNetworkType(): String {
var net = ""
val cManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities: NetworkCapabilities? =
cManager.getNetworkCapabilities(cManager.activeNetwork)
capabilities?.let { cb ->
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
net = "WIFI"
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
net = getMobileNetworkType(cManager)
}
}
return "网络情况:${net}"
}
当判断出非wifi网络的时候,再通过getMobileNetworkType函数来得出具体的网络类型
private fun getMobileNetworkType(cManager: ConnectivityManager): String {
val networkInfo = cManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)
if (networkInfo != null) {
val networkType = networkInfo.subtype
return when (networkType) {
TelephonyManager.NETWORK_TYPE_GPRS, TelephonyManager.NETWORK_TYPE_EDGE -> "2G"
TelephonyManager.NETWORK_TYPE_UMTS, TelephonyManager.NETWORK_TYPE_HSPA -> "3G"
TelephonyManager.NETWORK_TYPE_LTE -> "4G"
TelephonyManager.NETWORK_TYPE_NR -> "5G"
else -> "UNKNOWN"
}
}
return "UNKNOWN"
}

本文介绍了车载通信架构中的 SOA(Service Oriented Architecture,面向服务的架构)通信协议相关内容。
服务导向架构(Service-Oriented Architecture,SOA)是一种软件设计方法论,它强调将应用程序构建为一系列松散耦合(loosely coupled)、可独立部署(independently deployable) 且通过 定义良好的接口(well-defined APIs) 进行通信的 服务(services) 。
在 SOA 中,每个服务都执行一个特定的业务功能,并且可以被其他服务或应用程序调用和重用。它不是一种技术,而是一种架构风格,可以通过各种技术(如 SOAP、REST、消息队列等)实现。
传统的汽车电子电气(E/E)架构是高度分散的,由数百个独立的电子控制单元(ECU)组成,每个 ECU 负责一个或几个特定功能(如发动机控制、车窗控制、信息娱乐等),它们之间通过 CAN、LIN 等低速总线进行通信。这种单片式(monolithic)或信号导向(signal-oriented)的架构随着车辆功能(如自动驾驶、智能座舱、车联网)的爆炸式增长,暴露出以下局限性:
为了应对这些挑战,汽车行业正在向软件定义汽车(Software-Defined Vehicle, SDV)的方向发展,而 SOA 正是实现 SDV 的关键使能技术。
在这些新的 E/E 架构中,高速以太网(Ethernet)逐渐取代传统的总线作为主要通信骨干网,为 SOA 提供必要的带宽和低延迟。相比于分布式架构,会有一个处理器作为 中央网关 ,来综合管理其他各个服务的订阅,数据分发。
在车载 SOA 中,车辆的各项功能被抽象为可独立调用和组合的“服务”。例如:
SOME/IP,全称 Scalable Service-Oriented Middleware over IP (基于 IP 的可伸缩面向服务中间件),它是一种专为汽车域设计的高效通信协议。
起源与目的: 随着现代汽车电子架构的复杂性日益增加,传统的车载网络协议(如 CAN、LIN、FlexRay)在 带宽、灵活性和可伸缩性 方面逐渐遇到瓶颈。为了满足信息娱乐、高级驾驶辅助系统 (ADAS) 等对带宽和实时性要求更高的新功能,汽车行业开始引入以太网。SOME/IP 应运而生,旨在为基于 IP 的汽车网络提供一种面向服务的通信机制。
AUTOSAR 整合: SOME/IP 是 AUTOSAR (AUTomotive Open System ARchitecture) 标准的关键组成部分。AUTOSAR 致力于统一汽车软件架构,SOME/IP 作为其通信层的一部分,确保了不同供应商和制造商之间的软件组件兼容性和互操作性。
服务发现的通信机制是通过SOME/IP-SD协议实现的,主要是为了实现在车载以太网中告知客户端当前服务实例的可用性及访问方式,可通过Find Service 和Offer Service来实现。
SOME/IP 服务发现流程可以分为以下三大基本步骤:

远程进程调用主要可分为四种通信模式:
Request-Response模型作为一种最为常见的通信方式,其主要任务就是客户端发送请求信息,服务端接收到请求,进行相关处理之后进行相应的响应。
该通信模型的主要任务就是客户端向服务端发送请求,服务端无需进行任何响应,有点类似诊断服务中的抑制正响应。
该通信模式主要描述了发布 /订阅消息内容,主要任务就是为了实现客户端向服务端订阅相关的事件组,当服务端的事件组发生或者值发生变化时,就需要向已订阅该事件组的客户端发布更新的内容。
访问进程通信机制主要是为了实现针对对应用程序的数据获取与更改,主要任务就是实现客户端通过Getter获取Server的值,通过Setter设置Server的值。
Field就可理解为一个Service的基本属性,可包含Getter,Setter,Notifier三种方式。其中Getter就是读取Field中某个值的方法,Setter就是一种改变Field值的方法,而Notifier则是一种当Field中的值发生变化的触发事件,发生变化时就通知Client。
AUTOSAR为了更为高效的定位到通讯过程中的问题所在,制定了一套检查SOME/IP协议格式内容的错误处理机制。比如版本信息检查,服务ID等,其他故障信息可以在Payload中进行详细定义。目前SOME/IP支持以下两种错误处理机制,这两种uowu处理机制可以根据配置进行选择。
消息类型0x80,Response信息,即可以通过Response Message中的Return Code来定位到问题所在;
消息类型0x81,显式的错误信息;
主要是校验协议首部结构,对Message Type和Return code进行赋值,通知对端。

本文介绍了车载通信架构中的CAN(Controller Area Network)通信协议相关内容。
车载通信中的CAN(Controller Area Network,控制器局域网)协议是汽车电子系统中应用最广泛的串行通信协议之一,一种专门为恶劣环境设计的串行通信协议。它的老家是德国,由博世公司在1986年正式发布,后来被写进了ISO11898-1标准,定义了OSI模型的数据链路层和物理层。
其设计初衷是为了解决汽车内部日益复杂的电子控制单元(ECU)之间的通信需求,替代传统的点对点布线方式,以减少线束重量、降低成本并提高通信可靠性。
现在,它已广泛应用于汽车、工业自动化、医疗设备等领域,作为一种高效、可靠的通信方式。
一种典型架构

CAN 总线是一种消息导向(message-based)的通信协议,而不是地址导向(address-based)。这意味着总线上的所有设备(称为节点或 ECU - Electronic Control Unit)都能“听到”所有传输的消息,每个消息都包含一个标识符(ID),而不是一个目标地址。节点会根据这个 ID 来决定是否接收和处理该消息。
CAN的诞生初衷是为了解决汽车内部电子控制单元(ECU)之间通信的麻烦。以前,ECU之间需要一大堆线缆连接,布线复杂得像蜘蛛网。CAN总线的出现让这一切变得简单:只需一对差分信号线,就能让所有ECU愉快地聊天。
CAN总线有以下几个特点:
CAN协议分为物理层和数据链路层(逻辑链路控制子层LLC和介质访问控制子层MAC),其中物理层和MAC子层由CAN标准(ISO 11898)定义,LLC子层由用户自定义。
一个标准CAN消息帧包含以下几个关键部分:

后来,CAN升级了,推出了扩展CAN,把标识符从11位扩展到29位,消息ID数量暴增到2的29次方,满足更复杂的应用场景。扩展帧在11位ID后加了个替代远程请求(SRR)位,IDE位变成隐性,表示后面还有18位ID。其他部分和标准帧差不多。
CAN总线支持四种消息类型:
简单来说,就是解决多个节点同时想发消息时的优先级问题。
为满足更高数据传输需求,CAN协议衍生出多个扩展版本:
CAN总线在汽车电子和工业控制领域简直无处不在。
比如新能源汽车的BMS(电池管理系统),通过CAN总线实时监控电池状态,SOC、SOH、温度、电压等数据飞速在ECU间传递。
工业领域,像是Modbus或DeviceNet这样的协议,底层也靠CAN总线撑腰。
相比传统的点对点连接,CAN总线的多主架构让系统扩展性强到爆,甚至随手加个节点都随随便便。
想象一下现代汽车,发动机控制、ABS、仪表盘、空调系统……每个模块都是一个ECU,它们通过CAN总线组成一个高效的通信网络。
就像一群人在群聊里实时交流,消息井然有序,互不干扰。比如你踩油门,发动机ECU立马收到指令,调整喷油量,整个过程非常顺畅。

CAN协议凭借其高可靠性、实时性和低成本优势,成为汽车电子通信的基石。随着汽车智能化与电动化的发展,CAN FD和CAN XL等扩展版本进一步提升了带宽与数据处理能力,而CAN与其他车载网络(如以太网)的协同也将成为未来车载通信架构的核心趋势。
分析 CAN 总线通信通常涉及到捕获总线上的原始数据,然后对这些数据进行解码和解释,以理解各个消息的含义。对于 Android 开发者来说,这可能涉及到通过蓝牙 OBD-II 适配器与车辆 CAN 总线交互,或者在嵌入式系统中直接与 CAN 控制器通信。
python-can 库), Wireshark (结合 CAN 插件) 等工具进行数据处理、可视化和模式识别。0x1A0 ID 的消息中解析出“发动机转速”为 2500 RPM)。下一篇将介绍基于中央服务的SOA架构,和CAN总线的分布式架构做对比。