【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?

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

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

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

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

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

【Android进阶】RecyclerView原理和LazyColumn

【Android进阶】RecyclerView原理和LazyColumn

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

RecyclerView的优化点

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

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

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

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

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

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

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

动画

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

职责解耦

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

数据更新效率

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

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

RecyclerView 的主要组成部分

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

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

视图回收的详细流程

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

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

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

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

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

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

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

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

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

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

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

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

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

4. ViewHolder:视图引用持有者

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

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

总结整个流程:

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

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

LazyColumn

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

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

LazyColumn 的核心显示逻辑

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

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

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

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

总结

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

【Android进阶】Android自定义View

【Android进阶】Android自定义View

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

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

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

继承自ViewGroup

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

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

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


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

</LinearLayout>

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

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

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

效果截图:

直接继承自View

基础绘制类和方法

Paint

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

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

常见于初始化操作:

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

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

例如绘制文字:

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

Canvas

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

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

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

常用几何图形类

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

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

它们都存储四个坐标值:

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

例如:

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

    ...

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

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

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

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

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

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

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

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

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

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

常用绘制方法详解

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

重要回调方法

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

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

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

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

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

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

onDraw(Canvas canvas)

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

onTouchEvent(MotionEvent event)

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

computeScroll()

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

总结流程

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

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

静态View

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

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

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

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

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

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

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

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

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

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

效果截图:

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

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

参数解释:

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

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

onMeasure 实现自适应尺寸

包裹内容

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

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

固定尺寸

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

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

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

动态View

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

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

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

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

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

        // 绘制表盘
        drawClockFace(canvas)

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

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

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

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

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

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

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

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

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

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

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

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

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

运行截图:

交互式

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

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

public class TemperaturePickerView extends View {

    private static final String TAG = "TemperaturePickerView";

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return true;
    }

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

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

        currentScrollY = newScrollY;
        invalidate();
    }

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

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

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

            invalidate();
        }
    }

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

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

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

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

运行截图:

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

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

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

Pagination