【Android进阶】车载Android常用View控件交互总结

【Android进阶】车载Android常用View控件交互总结

本文介绍了车载Android里的常用的View控件交互总结

模块名词解释

一种常见架构

blogs_view_car_net

VIU

汽车电子系统中的一个重要组成部分,它是一种称为“车辆信息单元”的设备,也被称为“车辆智能单元”,是车辆智能化的核心部件之一。VIU是英文Vehicle Information Unit的缩写,其主要功能是收集车辆的各种信息并进行处理。

VIU由众多不同的传感器、执行器和微控制器组成,它们单独或联合工作,从不同的方面监测和分析车辆的各项数据。传感器可以感知发动机状态、温度、湿度、油压、燃油消耗量等各类参数,执行器则能够控制发动机、座椅、车门等车辆各部分,微控制器则负责控制各种数据从传输到处理。

S32G

在域控制器中,网关处理器的作用不容忽视,其作为域控制器的中心枢纽,负责安全和功能域(如动力传动、底盘与安全、车身控制、信息娱乐和ADAS等)之间互联并处理异构车载网络中的数据。S32G采用M核+A核多核异构架构,兼顾实时应用以及高算力应用场景,并且具有ASIL D的功能安全等级,集成了低延迟通信引擎LLCE,数据包转发引擎PFE,硬件安全引擎HSE等独立内核,非常适合作为主控制器,整合传统网关,BCM,VCU等多个ECU控制逻辑。

S32G M核基于AUTOSAR CP, 可以处理CAN/LIN信号,系统启动,电源管理,健康管理,车控等对实时性要求较高的应用,以及多种安全策略和功能处理策略。 S32G A核运行Linux或者QNX,满足对于处理多路千兆以太网、大数据收集与分析、整车OTA、数据存储、远程诊断等功能,可部署多种通信协议和相关学习算法。M核和A核之间既可以采用核间通信IPCF,交换延迟要求非常低的数据,也可以通过PFE共享以太网接口,实现高吞吐量数据交换需求。

其内部有如下模块:

  1. 整车域控制组件:集成网关、车窗控制、灯光控制等功能模块;
  2. 动力域控制组件:集成整车控制器、电池管理系统、热管理系统等功能模块;
  3. 底盘域控制组件:集成智能控制悬挂、电子驻车单元等系统功能模块;
  4. 车载中央计算机:可协调各个域控单元组件有序工作;
  5. 更多应用场景可根据客户实际需求进行功能组件的组合。

可以说其为整车的核心控制器,身兼中央网关信号中转和重要逻辑处理等多个角色。

TDA4

TDA4是德州仪器推出的一款高性能、超异构的多核SoC,拥有ARM Cortex-R5F、ARM Cortex-A72、C66以及C71内核,可以部署AUTOSAR CP系统、HLOS(Linux或QNX)、图像处理以及深度学习等功能模块,从而满足ADAS对实时性、高运算能力、环境感知及深度学习等方面的需求。

TDA4凭借着出色的运算能力、有竞争性的价格,赢得了越来越多汽车主机厂以及零部件供应商的青睐。

这款智能驾驶处理芯片,计算效率高,工具链成熟,但是算力低,行泊一体将导致其想要完善的能力所带来的要求也极高。

8155

相当于手机的高通855芯片,属于高端旗舰级别,事实上诞生于2019年的高通8155其实就是基于855手机芯片“魔改”而来的,当时高通855芯片在业内也算是领先水平,该芯片多应用在各品牌的旗舰级手机上,因此基于855打造的车规级8155性能也不会差。2019年所发布的8155芯片,至今除了特斯拉的AMD主机级锐龙芯片以外,仍然是天花板级别的存在,其稳定性、可靠性也能够经得起时间的考验,这也是为什么如今众多车企都选择高通骁龙8155。业内一般方案为quick unix系统上套安卓虚拟机的方案,以更稳定的qnx系统来作为硬件直接交互的角色,并且仪表显示等重要模块是运行在qnx系统上的应用,而Android系统由于其不稳定性,更适合作为系统和车控设置项、娱乐信息屏幕的承载系统。

不同电器架构上层共性

一个车控功能,链路从对应的底层控制器到座舱控制器的网络拓扑随着项目的电器架构而变化,一般分为两种情况:

  • 分布式架构,各个控制器彼此独立,使用CAN总线进行通信;
  • 集中式架构,一般有一个域控制器作为中心网络中转的角色,各个控制器都通过域控制器进行通信。

不管底层架构如何,当信号到了座舱域的8155或者8295控制器之后,信号的链路就是一致的了,网络层到硬件抽象层,再到系统FW的CarService层,最后通过Binder接口给到应用层。

信号的上下行流程

车控信号的上下行流程一般分为以下几个步骤:

  1. 用户手动操作控件之后,应用下发对应的setter接口,调用request信号。
  2. 座舱域的控制器将信号转发出去;
  3. 目的控制器接收之后,做出对应处理,将操作的结果通过另一个广播信号或者setter的同一个信号返回上去
  4. 座舱域的控制器将底层的反馈,回调给应用层。

而应用需要做的一般有下面几件事:

  1. 界面初始化根据获取的初始值来刷新界面;
  2. 点击控件可以下发信号;
  3. 操作完毕之后,要根据反馈的信号来刷新页面;
  4. 用户离手一段时间后(2s或者3s),需要主动获取一次开关的状态,刷新开关状态。即回弹逻辑,防止实际执行失败,却传达了执行成功的信息。

不同UI控件信号处理规范

Switch开关类

开关一般用于各个设置项,比如氛围灯,智驾功能,蓝牙,网络开关等。

blogs_view_switch

Switch有切换时下发指令和显示开关状态的需求。

  • 下发指令只能通过用户手动点击触发,不可自行发送信号;
  • 状态显示有主动获取与被动接收通知之分,主动获取常见的策略是在点击之后若干秒后,去获取当前的功能信号状态,进而刷新开关状态。被动接收通知为长期监听。

底层因某些错误发出置灰信号,或者功能在某种条件下自动打开或关闭,都需要及时地反馈到界面的switch上。注意这种主动和被动的更新常常是冲突的,处理不好会导致开关快速闪动。

变种类switch,严格来说是Button,具有非此即彼,互斥状态的控件。比如可能设计成一个高亮色块,通过不同颜色图标来表示开关此时的状态,往往还需要更改开关的文字描述,例:

blogs_view_button

下游执行快

若座舱域的下游控制器可以做到快速切换,只是信号链路传输慢,可以在用户每次点击开关后,都直接往下发设置信号。单次点击一般没有问题,主要分析快速多次点击这种容易出问题的场景。 快速点击期间都移除掉信号监听removeCallbacks,也不进行状态的主动获取刷新

在快速点击时期结束,用户手指最后一次点击若干秒后,主动获取一次开关状态,这种情况下,开关状态一般和最后一次点击下发的值是同步的,不会有回弹现象。

并且在主动获取之后,重新加上开关的被动监听刷新机制。即可以支持快速接收并执行指令的,上层芯片可以只管发,处理好自己的UI就行。

下游执行慢

如果该功能是控制器执行速度慢,可以在开关的快速点击期间,只跟随用户操作变动Switch的UI,不往下发setter信号,在最后一次点击结束后,过几百ms,再往下游控制器发送setter信号,快速点击期间,车控信号被动的监听也是移除的,在主动获取状态刷新UI之后,再加上状态监听。

例如下面是500ms防抖的设置方法,两次点击间隔500ms以内,会移除掉上一次的逻辑,只有快速点击完成后,过500ms,才会继续执行逻辑。

  switch.setOnCheckedChangeListener { buttonView, isChecked ->
            mHandler.removeCallbacks(signalRunnable)
            mHandler.postDelayed(signalRunnable, 500L)
            SignalUtil.sendSignal()
        }

即不可快速自由切换的功能的,上层芯片需要过滤点击事件,尽量将一次抖动流程里只发最后一帧信号。

以上两种是用户体验较好的方案,可以支持快速点击,只取最后一次点击使其生效。

加入点击限制

除了防抖,还可以使用点击限制,在点击后的若干时间内,直接使开关除能,不再接收点击事件,在此期间加入置灰或者加载loading的样式提示此开关暂时不可用。

这种纯粹的点击限制,在用户体验上不是特别好,最好加入说明文案等。适合比较复杂的功能,像底层ECU执行时间特别长(2s以上),并且多次频繁下发值有可能导致其出错的场景。

val switchEnableRunnable = Runnable { switch.isEnabled = true }

switch.setOnCheckedChangeListener { buttonView, isChecked ->
    switch.isEnabled = false
    mHandler.removeCallbacks(switchEnableRunnable)
    mHandler.postDelayed(switchEnableRunnable, 2000L)
    SignalUtil.sendSignal()
}

RadioGroup 类控件

这类控件组,往往是同一个功能,走同一个信号接口,有两个以上的待选项,可以选取不同参数的功能。比如 驾驶模式 选择的控件。

blogs_view_radiogroup

这类控件的处理方式和上述 switch 开关类控件类似。

  1. 执行时间长的需要限制点击下发,某段时间内只允许一次点击;
  2. 信号链路长的功能,可以不限制点击,只限制回调刷新UI。在快速点击过程中,信号是即点即发,直到快速点击后的若干秒内,UI控件不响应回调数据的变化,在防抖动流程结束后,主动获取一次状态,并重新添加上回调监听更新UI的逻辑。

    防止刷新时的循环设置

    RadioGroup加入 checkchanged 监听,可以监听开关项变化。但是由信号被动刷新时,也会触发这个回调,如果在这个里面设置的信号下发和埋点计算逻辑,就会重复计算。

甚至有时候时间差恰到好处的话,会导致两个开关项之间循环设置,不断跳动。

mRgMainBlowFace.setOnCheckedChangeListener((group, checkedId) -> {
    // 发送set信号
    SignalUtil.sendSignal();
    // 埋点计算
    ReportUtil.report();
});

可以对RadioGroup进行封装,对OnCheckedChangeListener加入一个本地变量来保存,加入一个 updateChecked 方法替代原来的刷新方案,在这个 update 方法里,先把 checkListener 给移除掉,再改变选中项的状态,操作完毕再把回调加回去。

public class RadioGroupEx extends RadioGroup {
    private RadioGroup.OnCheckedChangeListener mCheckedChangeListener;

    public RadioGroupEx(Context context) {
        super(context);
    }

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

    public void setOnCheckedChangeListener(@Nullable RadioGroup.OnCheckedChangeListener listener) {
        this.mCheckedChangeListener = listener;
        super.setOnCheckedChangeListener(listener);
    }

    public void updateChecked(int checkedId) {
        super.setOnCheckedChangeListener((RadioGroup.OnCheckedChangeListener)null);
        this.check(checkedId);
        super.setOnCheckedChangeListener(this.mCheckedChangeListener);
    }
}

持续调节自定义View类

首先重温一下点击事件分发与消耗机制:

当一个点击事件产生后,它的传递过程遵循如下顺序: Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。

顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的 onTouchEvent 返回false,那么它的父容器的onTouchEvent将会被调用,依此类推。

如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的 onTouchEvent 方法会被调用。

这种长按持续调节的交互方式,需要手动实现控件的ontouch方法,并监听手势滑动轨迹,在 Action MOVE 回调方法里实时更新自己的UI,并且持续性地发送信号。

插入,ontouch方法和onTouchEvent方法:

boolean onTouch(View v, MotionVent event)
触摸事件发送到视图时调用(v:视图,event:触摸事件)
返回true:事件被完全消耗(即,从down事件开始,触发move,up所有的事件)
返回fasle:事件未被完全消耗(即,只会消耗掉down事件)

boolean onTouchEvent(MotionEvent event)
触摸屏幕时调用
返回值,同上

注意:
1、onTouch优先级比onTouchEvent高
2、如果button设置了onTouchListener监听,onTouch方法返回了true,就不会调用这个button的Click事件 

下面是一个复写 OnTouchListener 的例子:

mCushionTouch.setOnTouchListener((view, motionEvent) -> {
    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {

    }
    if (motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
        // DO YOUR WORK
        // UPDATE UI  &  SEND SIGNAL
        return true;
    }
});

这种控件,在车控领域,一般用在座椅位置和空调风向的调节上,需要其既能响应用户手动滑动,来更新界面UI样式,又可以根据底层反馈更新。

在手动调节时,同样的,为了避免界面显示错乱,需要在touch调节时移除回调更新UI的逻辑,以用户手动调节的位置为最高优先级,调节完成后若干时间后,获取状态更新UI,再重新添加上回调去监听更新的逻辑。注意界面首次调节的起始点必须是当前的位置,不可出现跳动现象。

滑动条SeekBar类

这一类控件常被用来作为 进度条 展示,也具有 手动调节 的功能。

blogs_view_seekbar

一般是亮度,音量,充放电电量等具有一定调节范围的设置项。它有三个回调方法,分别是onProgressChanged,onStartTrackingTouch,onStopTrackingTouch,代表调节时,按下时,抬起时。

其中 onProgressChanged 的回调相当之快,除非有动态变化显示的需求,否则不建议在这里处理逻辑,或者在这里的逻辑加上防抖限制,一定时间内只调用一次。曾经我在这里调用埋点方法,利用系统服务处理网络上传请求,导致系统崩溃黑屏。后续改到停止调节时上传,一次touch操作只会传一次。

这类调节条的更新逻辑与其他控件类似,在下发方案上主要分两类:

  1. 需求上实时调节的,比如氛围灯颜色,音量,亮度,在跟手时硬件即响应变化,用户体验比较好,这种需要在onProgressChanged回调里进行信号的发送。
  2. 不需要实时调节的,是那种无法直观观察到变化的设置项,比如车辆充放电截止电量,能动回收百分比,各种灵敏度等,这就推荐在手指抬起或者按下时调用一次,不处理变化中的逻辑。

以上两种方案有一个共同的更新逻辑,就是在快速调节(滑动or快速点击)中,不响应底层数据反馈,避免进度条乱跳,在设置后一段时间内,主动获取状态,并重新加上数据监听被动更新。

signalSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int i, boolean b) {

    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {

    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {

    }
}); 

有步长的Seekbar体验优化

比如设置某个信号,底层只能接受像5,15,25等5的倍数的信号值。而将seekbar的步长设置为5,在滑动时会有明显的卡顿感。

blogs_view_seekbar_step

所以为了丝滑调节,可以设置默认步长1,采用整除回乘的算法来将区间数据处理成需要的数据,比如32整除5为6,再乘5就是30。

至于快速调节过程中可能出现的回调闪烁问题,则采用防抖或者节流算法来减少频次,再控制一下回调刷新UI的逻辑,即可实现体验最优的seekbar滑动调节信号收发。

【Android进阶】Coil图片加载库介绍

【Android进阶】Coil图片加载库介绍

本文介绍了Android平台的图片显示库Coil,其优化点和基本使用方式

Android图片加载体系

在 Android 中,加载并显示一张图片文件(如 JPEG、PNG)到屏幕上,核心机制是 Bitmap -> Drawable -> ImageView 的配合使用。

Bitmap 存储图片在内存中的实际像素数据(如 RGB 值)。Drawable 属于抽象层,代表可绘制对象,是所有可绘制内容的抽象基类,作为 Bitmap 与 View 之间的桥梁,管理 Bitmap 的绘制状态和尺寸信息。ImageView负责将 Drawable 对象的内容绘制到屏幕上,并处理用户交互。

配合加载一张图片文件的完整流程

当一张图片文件(例如 image.jpg)从磁盘或网络被加载,直到最终显示在 ImageView 中,主要分为以下三个步骤:

步骤 1: 图片数据解码成 Bitmap(数据准备)

图片文件本身是经过压缩的(如 JPEG),不能直接显示。这一步的任务是将压缩数据解压并解码成原始像素数据

  1. 解码: 使用 BitmapFactoryImageDecoder 等 API,将 image.jpg 文件读取为字节流。
  2. 生成 Bitmap: 解码器根据字节流,在内存(RAM)中开辟一块空间,将图片的像素数据填充进去,创建出 Bitmap 对象。
  3. 内存占用: 此时 Bitmap 占用的内存大小 = 图片像素宽 × 图片像素高 × 每个像素占用的字节数(如 ARGB_8888 模式下占 4 字节)。

关键代码: BitmapFactory.decodeFile(path)ImageDecoder.decodeBitmap(source)

步骤 2: Bitmap 封装成 Drawable(状态管理)

Bitmap 纯粹是像素数据,而 Android 的 View 系统需要一个可绘制对象 (Drawable) 来进行绘制和状态管理。

  1. 封装: Bitmap 对象会被封装到一个具体的 Drawable 子类中,最常见的是 BitmapDrawable
  2. 提供信息: BitmapDrawable 获得了 Bitmap 像素信息后,还添加了绘制所需的额外信息,比如:
    • 固有的宽高 (getIntrinsicWidth/getIntrinsicHeight): 来源于 Bitmap 的像素尺寸。
    • 不透明度、颜色过滤、状态(选中/按下等): 允许在绘制时对 Bitmap 进行调整和控制。

关键代码(底层): BitmapDrawable drawable = new BitmapDrawable(resources, bitmap);

步骤 3: ImageView 绘制 Drawable(视图呈现)

ImageView 是最终的显示容器,它负责将 Drawable 的内容呈现在屏幕上。

  1. 设置 Drawable: 通过 imageView.setImageDrawable(drawable)imageView.setImageBitmap(bitmap)(内部会自动封装成 BitmapDrawable)将 Drawable 对象交给 ImageView
  2. 计算尺寸: ImageView 会根据自身的布局参数(如 layout_widthlayout_height)和 scaleType(如 centerCropfitCenter)来决定如何缩放和裁剪内部的 Drawable
  3. 触发绘制:ImageViewonDraw() 方法中,它会调用 Drawable.draw(canvas) 方法,让 Drawable 将其内部的 Bitmap 绘制到 ImageView 的画布(Canvas)上,最终呈现在屏幕上。

关键代码(上层): imageView.setImageBitmap(bitmap) 或在 XML 中使用 android:src="@drawable/..."

Coil

Coil (全称 Coroutine Image Loader) 是一个专为 Android 打造的现代化图片加载库,它之所以被认为是高效的,主要得益于其现代化的架构和多项针对性的优化。

以下是 Coil 优化的主要方面:

1. 核心架构优化 (基于 Kotlin 协程)

这是 Coil 最核心的优化点。Coil 的名字就来源于此。它完全基于 Kotlin 协程 (Coroutines) 来执行所有的异步操作(如网络请求、磁盘I/O、图片解码)。

相比于传统的线程池或 AsyncTask,协程更加轻量级。它们可以挂起 (suspend) 而不阻塞线程,从而用更少的线程处理大量的并发请求。这减少了线程切换的开销,提高了吞吐量,并且能非常简单地实现请求的取消和管理。

2. 内存优化

Coil 在内存管理上做了大量工作,以避免 OutOfMemoryError 并保持应用流畅:

  • 图像降采样 (Downsampling): Coil的第一次解码只读取图片的原始宽高(不分配内存)。根据原始宽高和目标 ImageView 的宽高,计算出最佳的inSampleSize采样率。再带着计算出的inSampleSize进行第二次解码,将缩小的 Bitmap 加载到内存。
  • Bitmap 池化 (Bitmap Pooling): Coil 会复用 Bitmap 对象。当一个 Bitmap 不再需要显示时,它会被放回一个“池”中,而不是立即被垃圾回收 (GC)。当需要加载新图片时,Coil 会尝试从池中获取一个可复用的 Bitmap,而不是重新分配内存。这大大减少了 GC 的频率,从而减少了 UI 卡顿。
  • 内存缓存 (Memory Cache): 使用 LruCache(最近最少使用算法)在内存中快速缓存已经加载的图片。如果同一张图片被再次请求,Coil 会直接从内存中读取,实现即时加载。
  • 自动大小调整 (Automatic Sizing for Compose): 在 Jetpack Compose 中使用时,AsyncImage 能够自动检测 Composable 的约束(大小),并请求一个最优尺寸的图片。

3. 网络与磁盘 I/O 优化

Coil 将网络请求和磁盘缓存完全委托给了 OkHttp,将文件I/O委托给了 Okio。

OkHttp 是目前 Android 上最高效、最主流的网络库,它内置了连接池、gzip压缩、HTTP/2 支持和强大的磁盘缓存系统。Okio 则提供了非常高效的缓冲I/O操作。Coil 无需“重复造轮子”,直接站在了巨人的肩膀上。

利用 OkHttp 的磁盘缓存,实现网络图片的持久化存储,下次请求时(即使应用重启)也能快速加载。新版本的 Coil 支持遵循服务器的 Cache-Control 响应头,实现更智能的网络缓存策略。

4. 请求与生命周期管理

Coil 自动与 androidx.lifecycle 库集成。它会观察 Activity、Fragment 或 Composable 的生命周期。当组件进入 onStoponDestroy 状态时,Coil 会自动取消相关的图片加载请求,这避免了无效的计算、内存占用和潜在的内存泄漏。

如果短时间内有多个地方请求同一张图片(例如在 RecyclerView 中),Coil 只会发起一次加载任务,并将结果分发给所有请求方。

支持设置图片加载的优先级,确保关键图片(如屏幕内的图片)优先于非关键图片(如屏幕外的图片)加载。

5. 轻量级与现代 API

相比于 Glide 和 Fresco,Coil 的库体积和方法数都更小,有助于减小 APK 大小。其API 设计简洁易用,充分利用了 Kotlin 的语言特性(如扩展函数、lambda 等)。与 Glide 不同,Coil 不使用注解处理器,这可以轻微加快应用的编译速度。

总结来说,Coil 的最大优化在于它全面拥抱了 Kotlin 协程和 OkHttp/Okio 这一现代化技术栈,并在此基础上实现了一套高效的内存管理(降采样、Bitmap池化)和智能的请求生命周期控制。

【Android进阶】Android平台主流依赖注入方案对比

【Android进阶】Android平台主流依赖注入方案对比

本文介绍了Android 平台的主流依赖注入方案对比,主要涉及Dagger,Hilt,Koin三种

当初学者熟悉软件在该平台上的运行机制,开始大量写代码之后,在软件项目的架构设计上应该符合 SOLID 原则。

SOLID 是 Robert C. Martin(“Uncle Bob”) 提出的一组五个基本原则的首字母缩写,旨在帮助开发者设计更易于理解、维护和扩展的软件系统:

Single Responsibility Principle (单一职责原则)

Open/Closed Principle (开闭原则)

Liskov Substitution Principle (里氏替换原则)

Interface Segregation Principle (接口隔离原则)

Dependency Inversion Principle (依赖反转原则)

其中的依赖反转原则是指:

高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。

简单来说,就是我们在设计系统时,不应该让高层组件直接依赖于低层组件的具体实现,而是应该让它们都依赖于抽象(例如接口或抽象类)。

什么是依赖?

在软件开发中,“依赖”指的是一个对象需要另一个对象来完成其功能。比如,一个 Car 对象可能需要一个 Engine 对象才能启动和运行。这时,我们可以说 Car 依赖 Engine

传统的做法是,Car 对象在自己的内部创建或查找 Engine 对象:

class Car {
    private Engine engine;

    public Car() {
        this.engine = new Engine(); // Car 自己创建了 Engine 对象
    }

    public void start() {
        engine.ignite();
        System.out.println("Car started!");
    }
}

这种也叫直接依赖,其问题在于:

  • 紧耦合: 如果低层模块的实现细节发生变化,高层模块也可能需要修改。
  • 测试困难: 在测试高层模块时,你不得不依赖真实的低层模块,这使得单元测试变得复杂且效率低下。你无法轻易地替换一个模拟的数据访问层。
  • 可扩展性差: 增加新的低层实现会影响到所有依赖它的高层模块。

什么是依赖注入?

依赖注入的核心思想是:一个对象不应该自己创建或查找它所依赖的对象,而是应该由外部(通常是一个“注入器”或“容器”)提供这些依赖。 依赖反转是我们的设计目标,依赖注入就是实现的路径。

CarEngine 的例子中,如果使用依赖注入,Car 就不再负责创建 Engine,而是等待外部把 Engine “注入”进来。结合Kotlin的构造函数写法, Car 类可以写成下面这种简洁的形式:

class Car(val engine: Engine) {
    public void start() {
        engine.ignite();
        System.out.println("Car started!");
    }
}

这种设计方案有哪些好处呢?

首先最明显的就是 解耦 ,对象不再需要关心它所依赖对象的创建细节,它们只需要知道如何使用这些依赖。这使得代码更灵活,更容易修改和扩展。

其次是 可测试性可维护性 提高了。更有利于单元测试,同时当外部的依赖发生变化时,只需要修改创建和提供依赖的部分,而不需要修改所有依赖该对象的代码。

还可以 提高代码复用性 ,独立的对象可以更容易地在不同的场景和组件中重用。

在 Android 端的依赖注入设计理念,整体的发展方向是从最初的手动管理到功能强大的自动化框架。

在早期的 Android 开发中,并没有成熟的 DI 框架。通常采用直接创建依赖示例的方式,后面又出现了由策略模式驱动的服务定位器的形式来提供依赖。

  • 手动实例化 这是最直接的方式,在一个类中直接 new 出它所需要的依赖。多了之后导致代码紧耦合,难以测试和维护,特别是当依赖链很深时,修改一个地方可能需要改动很多地方。
  • 服务定位器 这个模式会引入一个中央注册表(或单例)来存储和提供依赖。类需要依赖时,就向这个注册表“请求”对应的实例。
    • 优点: 相对于手动实例化,服务定位器提供了一定程度的解耦,因为消费者不再直接创建依赖。
    • 问题: 仍然存在隐藏依赖。你不知道一个类需要哪些依赖,除非查看其实现。难以追踪对象生命周期,并且测试时替换模拟对象不够优雅。它更像是“查找依赖”而不是“注入依赖”。

Dagger 1:初次尝试编译时注入

在 2012 年左右,Square 公司推出了 Dagger。这是 Android 平台第一个真正意义上的依赖注入框架,并且它采用了编译时代码生成的方式。

没有运行时的反射开销,带来了性能优势。它使用了 注解处理器 在编译阶段生成注入代码。

但是 Dagger 的配置和使用相对复杂,尤其是对于大型项目而言,编写和维护大量的模块 (Module) 和组件 (Component) 样板代码 成为一个挑战。

Dagger 2:性能与可扩展性的飞跃

2015 年,Google 接手 Dagger 项目并发布了全新的 Dagger 2。Dagger 2 是对 Dagger 1 的彻底重写,它秉承了 Dagger 1 的 编译时生成 的特性,但在设计理念和实现上有了重大改进。

Dagger 2 在编译时生成代码,这些代码负责实例化对象并提供它们的依赖。这意味着在运行时没有反射开销,性能非常高。它通过注解处理器来分析你的代码,生成一个 依赖图 ,然后根据这个图生成相应的 Java 代码。它生成的是直接的 Java 代码,模拟了你在手写工厂类和提供器时的行为,从而在性能上达到了极致。

运行 Dagger 2 示例需要添加 Dagger 依赖并配置注解处理器。在 Android 项目中,通常在 build.gradle 文件中配置。比如使用Kotlin的话,需要在 build.gradle 中配置 kapt 插件:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt' // 配置 Kotlin 注解处理器插件
}

Dagger 2 在使用时需要定义模块和组件。

  1. 定义依赖接口和实现:
    // repository/UserRepository.java
    interface UserRepository {
        void saveUser(String username);
    }
    
    // repository/DatabaseUserRepository.java
    class DatabaseUserRepository implements UserRepository {
        @Override
        public void saveUser(String username) {
            System.out.println("Saving user " + username + " to database.");
        }
    }
    
  2. 定义提供依赖的 Module:
    import dagger.Module;
    import dagger.Provides;
    import javax.inject.Singleton; // Dagger 2 提供的作用域注解
    
    // di/AppModule.java
    @Module
    public class AppModule {
        @Provides // 提供 UserRepository 实例
        @Singleton // 将 UserRepository 定义为单例
        UserRepository provideUserRepository() {
            return new DatabaseUserRepository();
        }
    }
    
  3. 定义注入器 Component:
    import dagger.Component;
    import javax.inject.Singleton;
    
    // di/AppComponent.java
    @Singleton // AppComponent 也是单例作用域
    @Component(modules = AppModule.class) // 关联 AppModule
    public interface AppComponent {
        // 定义注入方法,MyPresenter 可以通过这个方法被注入依赖
        void inject(MyPresenterWithDagger presenter);
    }
    
  4. 在需要注入的类中使用 @Inject
    import javax.inject.Inject;
    
    // presenter/MyPresenterWithDagger.java
    class MyPresenterWithDagger {
        @Inject // 声明需要注入 UserRepository
        UserRepository userRepository;
    
        public MyPresenterWithDagger() {
            // Dagger 2 会在调用 inject(this) 后自动填充 userRepository
        }
    
        public void registerUser(String username) {
            userRepository.saveUser(username);
            System.out.println("User " + username + " registered.");
        }
    }
    
  5. 在 Application 或 Main 方法中初始化和使用:
    // main/MainDagger2.java
    public class MainDagger2 {
        public static void main(String[] args) {
            // ✨ 构建 Dagger 组件,这个 DaggerAppComponent 是 Dagger 2 编译时生成的
            AppComponent component = DaggerAppComponent.builder().build();
    
            MyPresenterWithDagger presenter = new MyPresenterWithDagger();
            // ✨ 执行注入操作,Dagger 会找到 @Inject 标注的字段并填充依赖
            component.inject(presenter);
    
            presenter.registerUser("Alice");
        }
    }
    

Dagger 2 具有如下优点:

  • 极高性能: 纯粹的编译时生成代码,运行时无反射开销,性能极佳。
  • 类型安全: 编译时即可发现依赖错误,将运行时崩溃降到最低。
  • 强大的模块化能力: 提供了 @Module@Provides@Component@Subcomponent@Scope 等丰富的注解,可以精细地控制依赖的提供和生命周期。

Dagger 2 推出之后,很快成为 Android 平台最主流、最强大的 DI 框架。

它解决了大规模项目中的依赖管理难题,但也继承了其复杂性,仍然需要开发者投入大量时间学习和配置。

Hilt:Google 官方简化 Dagger

2020 年,Google 推出了 Hilt,这是构建在 Dagger 2 之上的 Android 官方推荐的依赖注入库。Hilt 的主要目标是简化 Dagger 在 Android 应用中的使用。大量减少项目中为了实现依赖注入而创建的重复的样板代码。

值得一提的是,在Google官方开源的旨在展示最新Android技术的开源项目—— NowInAndroid 中,就使用了Hilt来实现依赖注入。

Hilt 的核心思想是通过提供一套 标准化的 Android 组件绑定 (例如 @AndroidEntryPoint@ApplicationContext@ActivityContext 等),以及预定义的作用域,极大地 减少了 Dagger 所需的样板代码 和手动配置。

Hilt 基于注解实现,针对每个需要被注入的属性,Hilt 都会基于 KAPT/KSP 在编译期间查找它的注入源头,并生成一对一的注入方法。

  1. 添加依赖和插件:

    // project/build.gradle
    buildscript {
        dependencies {
            classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1' // 检查最新版本
        }
    }
    
    // app/build.gradle
    plugins {
        id 'kotlin-kapt' // 或 id 'androidx.navigation.safeargs.kotlin' 如果使用 kotlin
        id 'com.google.dagger.hilt.android'
    }
    
    dependencies {
        implementation 'com.google.dagger:hilt-android:2.51.1'
        kapt 'com.google.dagger:hilt-compiler:2.51.1'
        // ... 其他依赖
    }
    
  2. 在 Application 类上添加 @HiltAndroidApp

    import android.app.Application
    import dagger.hilt.android.HiltAndroidApp
    
    // di/MyApplication.kt
    @HiltAndroidApp // Hilt 的入口点,触发代码生成
    class MyApplication : Application() {
        // 通常不需要在这里写额外的代码,Hilt 会自动管理组件
    }
    
  3. 定义依赖接口和实现 (与 Dagger 类似):

    // repository/UserRepository.kt
    interface UserRepository {
        fun saveUser(username: String)
    }
    
    // repository/DatabaseUserRepository.kt
    // Hilt 可以在构造函数上直接使用 @Inject 来告知如何创建实例
    import javax.inject.Inject
    import javax.inject.Singleton
    
    @Singleton // Hilt 也支持 Dagger 的作用域注解
    class DatabaseUserRepository @Inject constructor() : UserRepository { // 🚀 构造函数注入的标志
        override fun saveUser(username: String) {
            println("Saving user $username to database.")
        }
    }
    

    注意: 如果 DatabaseUserRepository 的构造函数没有参数,或者其参数都可以被 Hilt 自动提供,那么可以直接使用 @Inject constructor()。对于第三方库或接口,仍然需要使用 @Module@Provides

  4. 定义 Hilt 模块(针对接口或外部类):

    import dagger.Binds
    import dagger.Module
    import dagger.hilt.InstallIn
    import dagger.hilt.components.SingletonComponent
    import javax.inject.Singleton
    
    // di/AppModule.kt
    @Module
    @InstallIn(SingletonComponent::class) // 🚀 指定模块安装到哪个 Hilt 组件(例如 Application 级别)
    abstract class AppModule { // 使用 abstract class 可以更高效地绑定接口
        @Binds // 🚀 绑定接口到具体实现
        @Singleton
        abstract fun bindUserRepository(impl: DatabaseUserRepository): UserRepository
    }
    
  5. 在 Android 组件上使用 @AndroidEntryPoint@Inject

    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import dagger.hilt.android.AndroidEntryPoint
    import javax.inject.Inject
    
    // activity/MainActivity.kt
    @AndroidEntryPoint // 🚀 标记这是一个 Hilt 入口点,Hilt 会为它生成组件并注入依赖
    class MainActivity : AppCompatActivity() {
    
        @Inject // 🚀 自动注入 UserRepository 实例
        lateinit var userRepository: UserRepository
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            userRepository.saveUser("Charlie")
            println("User Charlie saved from MainActivity.")
        }
    }
    

Hilt具有如下优点:

  • 集成度高: 与 Android 框架组件(Activity, Fragment, ViewModel, Service 等)无缝集成,自动生成 Dagger 组件。
  • 易用性: 显著降低了 Dagger 的学习曲线和使用门槛。
  • Google 官方支持: 作为官方推荐的 DI 解决方案,Hilt 在未来的发展和维护上更有保障。
  • 保留 Dagger 优势: 依然是编译时注入,拥有 Dagger 的高性能和类型安全。

Hilt 迅速成为 Android DI 的“新宠”,尤其适合新项目和希望简化 Dagger 配置的现有项目。在 nowinandroid 中使用,也代表了它是当前官方认为的 Android 依赖注入的最佳实践。

Koin:运行时注入的轻量级选择

随着 Kotlin 在 Android 领域的崛起,又出现了一些纯 Kotlin 编写的 DI 框架。其中,Koin 在 2017 年左右脱颖而出。

Koin 采取了与 Dagger 完全不同的策略,它是一个运行时依赖注入框架,不使用注解处理器,而是利用 Kotlin 的 DSL(领域特定语言)来声明依赖。Koin 在运行时通过 Kotlin 的 DSL 解析依赖关系,不依赖反射或注解处理器,避免了 Dagger 的编译时代码生成复杂性,启动速度更快。

Koin 除了使用上轻量化,还具有以下优点:它的配置简单,学习曲线平缓,几乎没有样板代码。与 Kotlin 语言特性无缝集成,代码简洁。由于没有注解处理,编译速度通常比 Dagger 更快。

同时,Koin 由于运行时生成的特点,如果有些依赖配置出错,只有在运行时才可以发现

Koin 采用 Kotlin DSL,不需要注解处理器。

步骤:

  1. 定义依赖接口和实现 (与 Dagger 类似):
    // repository/UserRepository.kt
    interface UserRepository {
        fun saveUser(username: String)
    }
    
    // repository/DatabaseUserRepository.kt
    class DatabaseUserRepository : UserRepository {
        override fun saveUser(username: String) {
            println("Saving user $username to database.")
        }
    }
    
  2. 定义 Koin 模块:
    import org.koin.dsl.module
    
    // di/appModule.kt
    val appModule = module {
        // single 表示单例,get() 会自动解析并提供所需的依赖
        single<UserRepository> { DatabaseUserRepository() }
    }
    
  3. 定义需要依赖的类:
    import org.koin.core.component.KoinComponent
    import org.koin.core.component.inject
    
    // presenter/MyPresenterWithKoin.kt
    class MyPresenterWithKoin : KoinComponent { // 实现 KoinComponent 接口
        // 🚀 通过 inject() 委托属性来获取依赖
        private val userRepository: UserRepository by inject()
    
        fun registerUser(username: String) {
            userRepository.saveUser(username)
            println("User $username registered.")
        }
    }
    
  4. 在 Application 或 Main 方法中启动 Koin:
    import org.koin.core.context.startKoin
    import org.koin.core.context.stopKoin
    
    // main/MainKoin.kt
    fun main() {
        // ✨ 启动 Koin 上下文,并加载模块
        startKoin {
            modules(appModule)
        }
    
        val presenter = MyPresenterWithKoin()
        presenter.registerUser("Bob")
    
        stopKoin() // 清理 Koin 上下文
    }
    

Koin 的核心优势在于 ​​简洁性​​ 和 ​​Kotlin 原生支持​​,通过 DSL 和运行时解析降低了 DI 的学习成本,适合追求开发效率的项目。但对于超大型应用或对性能极度敏感的场景,可能需要权衡其运行时解析的开销。如果你正在使用 Kotlin 开发 Android 或后端服务,Koin 是一个值得尝试的轻量级 DI 方案。

现如今,Kotlin Multiplatform 跨平台的迅速发展,Koin 也推出了其跨平台版本,在 Android,IOS,Desktop和 web 端 的Kotlin跨平台项目里,都可以助力开发者实现依赖注入,支持功能的快速开发。

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

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

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

初始化

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

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

Activity Window View初始加载

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

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

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

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

activity_window

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

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

attach 时期

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

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

onCreate 时期

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

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

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

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

onResume() 时期

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

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

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

setView() 源码:

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

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

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

UI刷新流程

Choreographer 编舞者介绍

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

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

核心作用:同步 Vsync 信号

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

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

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

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

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

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

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

Choreographer工作流程详解

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

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

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

View.invalidate() 刷新流程

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

1. 触发与同步阶段

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JVM内存模型

jvm_ram

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

类加载流程

父子类加载的具体流程

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

实例化时的加载顺序

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

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

代码示例

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

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

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

输出:

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

设计模式

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

垃圾回收流程

JVM

根搜索算法(GC ROOT Tracing)

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

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

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

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

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

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

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

    5)运行中的线程

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

    7)GC控制的对象

回收流程

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

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

jvm_find

年轻代(Young Generation)

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

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

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

回收时机:

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

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

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

为什么设置两个survivor区域?

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

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

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

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

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

年老代(Old Generation)

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

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

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

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

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

持久代(Permanent Generation)

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

DVM & Art

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

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

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

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

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

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

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

内存泄漏常见场景

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

线程间通信

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

线程池

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

安卓设备开机流程

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

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

冷启动流程

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

Handler & 消息处理机制

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

Activity & Window 初始化

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

基本关系框架

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

绑定过程的关键步骤

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

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

Window的创建时机

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

关键绑定点说明

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

Activity → Window 的通信

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

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

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

Activity实现了这个接口:

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

视图层级绑定

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

Activity.setContentView()

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

PhoneWindow.setContentView()

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

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

设计原理分析

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

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

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

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

基本流程概述

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

核心类与组件

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

初始化LayoutInflater

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

inflate()方法调用

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

实际膨胀过程

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

View创建过程(createViewFromTag)

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

递归膨胀子View(rInflate)

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

性能优化相关

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

自定义View问题:

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

setContentView流程

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

基本调用流程

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

PhoneWindow.setContentView() 主要步骤

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

installDecor() 过程

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

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

generateLayout() 过程

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

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

重要注意事项

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

View绘制三部曲

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

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

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

Measure

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

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

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

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

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

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

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

Layout

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

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

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

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

Draw

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

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

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

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

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

热门八股问题

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

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

三种动画

补间动画

核心特性

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

优缺点分析

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

属性动画

核心特性

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

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

优缺点分析

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

帧动画

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

核心特性

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

性能优化建议

图片优化:

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

事件分发

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

事件分发三大核心方法

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

完整分发流程

  1. Activity 层级分发

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

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

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

关键规则:

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

事件分发UML序列图:

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

详细图解:

常见场景分析

场景1:点击按钮

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

场景2:滑动冲突

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

场景3:自定义事件处理

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

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

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

性能优化建议

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

多点触控处理

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

两列表嵌套滑动

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

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

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

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

AIDL & Binder基础原理

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

Binder通信原理

架构组成

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

工作流程

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

通信细节

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

AIDL通信原理

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

工作流程

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

关键组件

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

总结

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

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

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

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

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

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

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

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

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

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

官方组件

ViewModel & Lifecycle

onSaveInstanceState 存储数据原理

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

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

ViewModel 存数据的原理

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

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

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

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

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

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

配置变更场景:

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

ViewModeProvider 的作用

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

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

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

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

Lifecycle 工作机制

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

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

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

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

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

ViewModel如何感知生命周期

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

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

Fragment共享viewmodel

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

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

LiveData & Flow

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

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

LiveData 与 RxJava/Observable 有什么区别?

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

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

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

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

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

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

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

LiveData 如何保证数据不丢失?

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

如何合并多个 LiveData 源?

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

在 Repository 层应该返回 LiveData 吗?

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

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

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

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

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

LiveData 与 StateFlow/SharedFlow 如何选择?

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

Room

DataStore & SharedPreferences & MMKV

SP

通过xml文件存储。 优点:

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

缺点:

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

DataStore

Google推荐的替代SharedPreferences的方案。

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

MMKV

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

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

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

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

Lottie

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

ViewBinding & DataBinding

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

Android Gradle 插件会:

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

运行时工作流程:

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

优点:

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

缺点:

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

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

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

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

双向绑定实现:

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

ViewPager2

Gson & Moshi & KotlinX Serialization

Proguard

Jetpack Compose

三方组件

RecyclerView

用途

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

核心组件

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

RecyclerView的View缓存机制

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

Retrofit + Okhttp

Retrofit的设计模式

动态代理机制

核心流程:

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

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

动态代理的优点

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

建造者模式 (Builder Pattern)

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

典型实现:

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

优势:

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

适配器模式 (Adapter Pattern)

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

核心组件:

CallAdapter.Factory:适配器工厂基类

RxJavaCallAdapterFactory:将 Call 适配为 RxJava 的 Observable

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

工作流程:

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

工厂方法模式 (Factory Method Pattern)

应用场景:Converter 和 CallAdapter 的创建

实现示例:

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

派生工厂:

GsonConverterFactory
MoshiConverterFactory
ScalarsConverterFactory

装饰者模式 (Decorator Pattern)

应用场景:OkHttpCall 的封装

实现方式:

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

作用:

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

策略模式 (Strategy Pattern)

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

实现体现:

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

示例:

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

观察者模式 (Observer Pattern)

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

典型实现:

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

责任链模式 (Chain of Responsibility)

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

Retrofit 中的集成:

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

处理流程:

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

设计模式综合应用示例

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

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

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

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

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

OKhttp的优势

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

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

Ktor

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

核心特性:

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

和Retrofit对比

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

CIO引擎

对比OkHttp:

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

Hilt & Dagger & Koin 依赖导入

依赖注入

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

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

三者简介

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

写法举例

Dagger

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

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

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

Hilt

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

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

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

Koin

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

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

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

对比表格

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

Glide & Coil

RxJava

EventBus

LeakCanary

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

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

工作原理流程图:

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

LeakCanary 默认监控以下Android组件:

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

泄漏检测触发时机

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

泄漏分析过程

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

核心优化技术

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

常见泄漏场景处理:

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

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

Kotlin

lateinit 和 by lazy 的区别

lateinit(延迟初始化)

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

by lazy(延迟加载)

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

实现原理

lateinit

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

by lazy

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

协程

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

apply & with & let & run

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

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

let

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

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

run

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

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

with

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

apply

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

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

also

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

数据类

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

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

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

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

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

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

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

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

    public String component1() {
        return name;
    }

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

扩展函数原理

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

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

解析:

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

Lambda

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

When关键字

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

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

?空安全

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

委托

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

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

示例:委托接口实现

interface Printer {
    fun print(message: String)
}

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

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

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

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

适用场景:

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

(1) lazy 延迟初始化

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

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

输出: Computed only once! Hello Hello

(2) observable 监听属性变化

import kotlin.properties.Delegates

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

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

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

(3) vetoable 可拦截修改

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

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

(4) 自定义属性委托

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

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

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

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

字符串连接

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

Object关键字

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

object Singleton {
    val name = "Kotlin Singleton"
}

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

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

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

    private Singleton() {
        // private constructor
    }

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

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

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

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

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

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

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

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

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

事件传递到Window之前

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

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

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

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

input_event

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

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

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

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

整体传递流程

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

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

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

总结

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

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

源码分析待补齐

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

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

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

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

硬件上电阶段

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

CPU初始启动

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

执行芯片内部BootROM代码

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

安全启动(Secure Boot)验证

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

Bootloader阶段

第一阶段Bootloader(Primary Bootloader)

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

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

加载并验证第二阶段Bootloader

第二阶段Bootloader(Secondary Bootloader)

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

初始化更多硬件设备

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

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

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

Linux内核启动阶段

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

内核开始初始化:

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

设备树(Device Tree)解析

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

init进程启动

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

init进程负责:

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

Android系统启动阶段

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

启动关键守护进程:

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

Zygote进程启动

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

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

System Server启动

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

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

服务启动顺序

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

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

桌面环境启动阶段

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

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

Launcher加载并显示:

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

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

最终用户界面

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

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

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

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

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


🟢 初级 (Beginner Level)

🧠 Kotlin 基础

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

🔁 控制流 (Control Flow)

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

🧩 函数 (Functions)

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

📚 集合 (Collections)

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

📱 Android 基础 (in Kotlin)

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

🟡 中级 (Intermediate Level)

🧱 Kotlin 中的面向对象概念

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

🔍 高级 Kotlin 函数和作用域

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

🔤 泛型和类型系统

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

🌐 协程 (基础)

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

🧩 Android + Kotlin (MVVM, Lifecycle)

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

🔵 高级 (Advanced Level)

🔁 协程和并发

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

🛠️ DSL (领域特定语言)

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

🌍 Kotlin Multiplatform

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

🏷️ 注解和反射

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

⚙️ 性能和内存

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

🎨 Jetpack Compose (基础到中级)

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

🔁 常见的 Kotlin + Android 集成问题

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

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

Jetpack Compose:高级

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

Clean 架构 + 架构模式

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

依赖注入 (DI)

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

测试

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

性能、安全及其他

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

补充:Kotlin 陷阱和模式

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

Pagination