Jetpack架构组件库:Hilt

news2024/10/6 10:34:02

Hilt

Hilt 是基于 Dagger2 的依赖注入框架,Google团队将其专门为Android开发打造了一种纯注解的使用方式,相比 Dagger2 而言使用起来更加简单。

依赖注入框架的主要作用就是控制反转IOC, Inversion of Control), 那么什么是控制反转呢?

首先它是一种技术思想,而不是一种具体的技术体现。它描述的是:面向对象的开发语言领域中的对象的创建和管理问题。

  • 控制:指的是对象创建(实例化、管理)的权力
  • 反转:将控制权交给外部环境(IOC框架)
  • 传统的开发方式:一个类里面有很多成员变量对象,这些成员对象,都需要new出来
  • IOC思想的开发方式IOC原则:NO,我们不要new,这样耦合度太高(入参改变,所有引用都要改),而是通过IOC容器(如Hilt)来帮助我们实例化对象并赋值。

总结一句话就是:一个类中创建对象是一种控制能力,控制反转就是将这种创建对象的控制权转交给外部框架来自动实现。

常见的依赖注入手段:

  • 解决方案一:配置xml文件,里面标明哪个类,用了哪些成员变量,等需要加载这个类的时候,我帮你注入(new)进去。(Spring服务器开发常用IOC方案)
  • 解决方案二:在需要注入的成员变量上添加注解,例如 @Inject,在编译时生成相关的实现类,运行时动态注入。(Android开发中常用的手段是 APT + JavaPoet 或者是 KSP + KotlinPoet)

Hilt 的使用

首先添加依赖项:

首先,将 hilt-android-gradle-plugin 插件添加到项目的根级 build.gradle 文件中:

plugins {
  ...
  id 'com.google.dagger.hilt.android' version '2.44' apply false
}

然后,应用 Gradle 插件并在 app/build.gradle 文件中添加以下依赖项:

...
plugins {
  id 'kotlin-kapt'
  id 'com.google.dagger.hilt.android'
}

android {
  ...
}

dependencies {
  implementation "com.google.dagger:hilt-android:2.44"
  kapt "com.google.dagger:hilt-compiler:2.44"
}

// Allow references to generated code
kapt {
  correctErrorTypes true
}

Hilt 使用 Java 8 功能。如需在项目中启用 Java 8,请将以下代码添加到 app/build.gradle 文件中:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Hilt 应用类

所有使用 Hilt 的应用都必须包含一个带有 @HiltAndroidApp 注解的 Application 类。

@HiltAndroidApp 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。
在这里插入图片描述

生成的这一 Hilt 组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外,它也是应用的父组件,这意味着,其他组件可以访问它提供的依赖项。

将依赖项注入 Android 类

Application 类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint 注解的其他 Android 类提供依赖项:

在这里插入图片描述

Hilt 目前支持以下 Android 类:

  • Application(通过使用 @HiltAndroidApp
  • ViewModel(通过使用 @HiltViewModel
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

如果您使用 @AndroidEntryPoint 为某个 Android 类添加注解,则还必须为依赖于该类的 Android 类添加注解。例如,如果您为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。

注意:在 HiltAndroid 类的支持方面适用以下几项例外情况:

  • Hilt 仅支持扩展 ComponentActivityactivity,如 AppCompatActivity
  • Hilt 仅支持扩展 androidx.FragmentFragment
  • Hilt 不支持保留的 fragment

@AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项,如组件层次结构中所述。

如需从组件获取依赖项,请使用 @Inject 注解执行字段注入:

在这里插入图片描述

注意:由 Hilt 注入的字段不能为私有字段。尝试使用 Hilt 注入私有字段会导致编译错误。

Hilt 注入的类可以有同样使用注入的其他基类。如果这些类是抽象类,则它们不需要 @AndroidEntryPoint 注解。

如需详细了解 Android 类被注入的是哪个生命周期回调,请参阅组件生命周期。

定义 Hilt 绑定

为了执行字段注入,Hilt 需要知道如何从相应组件提供必要依赖项的实例。“绑定”包含将某个类型的实例作为依赖项提供所需的信息。

Hilt 提供绑定信息的一种方法是构造函数注入。在某个类的构造函数中使用 @Inject 注解,以告知 Hilt 如何提供该类的实例:

在这里插入图片描述

在一个类的代码中,带有注解的构造函数的参数即是该类的依赖项。在本例中,AnalyticsServiceAnalyticsAdapter 的一个依赖项。因此,Hilt 还必须知道如何提供 AnalyticsService 的实例。

注意:在构建时,Hilt 会为 Android 类生成 Dagger 组件。然后,Dagger 会走查您的代码,并执行以下步骤:

  • 构建并验证依赖关系图,确保没有未满足的依赖关系且没有依赖循环。
  • 生成它在运行时用来创建实际对象及其依赖项的类。

Hilt 模块

有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用 Hilt 模块向 Hilt 提供绑定信息。

Hilt 模块是一个带有 @Module 注解的类。与 Dagger 模块一样,它会告知 Hilt 如何提供某些类型的实例。与 Dagger 模块不同的是,您必须使用 @InstallInHilt 模块添加注解,以告知 Hilt 每个模块将用在或安装在哪个 Android 类中。

注意:Hilt 模块与 Gradle 模块不同。

您在 Hilt 模块中提供的依赖项可以在生成的所有与 Hilt 模块安装到的 Android 类关联的组件中使用。

注意:由于 Hilt 的代码生成操作需要访问使用 Hilt 的所有 Gradle 模块,因此编译 Application 类的 Gradle 模块还需要在其传递依赖项中包含您的所有 Hilt 模块和通过构造函数注入的类。

使用 @Binds 注入接口实例

AnalyticsService 为例。如果 AnalyticsService 是一个接口,则您无法通过构造函数注入它,而应向 Hilt 提供绑定信息,方法是在 Hilt 模块内创建一个带有 @Binds 注解的抽象函数。

@Binds 注解会告知 Hilt 在需要提供接口的实例时要使用哪种实现。
在这里插入图片描述
带有注解的函数会向 Hilt 提供以下信息:

  • 函数返回类型会告知 Hilt 该函数提供哪个接口的实例。
  • 函数参数会告知 Hilt 要提供哪种实现

Hilt 模块 AnalyticsModule 带有 @InstallIn(ActivityComponent::class) 注解,因为您希望 Hilt 将该依赖项注入 ExampleActivity。此注解意味着,AnalyticsModule 中的所有依赖项都可以在应用的所有 activity 中使用。

使用 @Provides 注入实例

接口不是无法通过构造函数注入类型的唯一一种情况。如果某个类不归您所有(因为它来自外部库,如 RetrofitOkHttpClientRoom 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。

接着前面的例子来讲。如果 AnalyticsService 类不直接归您所有,您可以告知 Hilt 如何提供此类型的实例,方法是在 Hilt 模块内创建一个函数,并使用 @Provides 为该函数添加注解。

在这里插入图片描述

带有注解的函数会向 Hilt 提供以下信息:

  • 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
  • 函数参数会告知 Hilt 相应类型的依赖项
  • 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。

为同一类型提供多个绑定

如果您需要让 Hilt 以依赖项的形式提供同一类型的不同实现,必须向 Hilt 提供多个绑定。您可以使用限定符为同一类型定义多个绑定。

限定符是一种注解,当为某个类型定义了多个绑定时,您可以使用它来标识该类型的特定绑定。

仍然接着前面的例子来讲。如果需要拦截对 AnalyticsService 的调用,您可以使用带有拦截器的 OkHttpClient 对象。对于其他服务,您可能需要以不同的方式拦截调用。在这种情况下,您需要告知 Hilt 如何提供两种不同的 OkHttpClient 实现。

首先,定义要用于为 @Binds@Provides 方法添加注解的限定符:

在这里插入图片描述
然后,Hilt 需要知道如何提供与每个限定符对应的类型的实例。在这种情况下,您可以使用带有 @ProvidesHilt 模块。这两种方法具有相同的返回类型,但限定符将它们标记为两个不同的绑定:
在这里插入图片描述
您可以通过使用相应的限定符为字段或参数添加注解来注入所需的特定类型:
在这里插入图片描述
最佳做法是,如果您向某个类型添加限定符,应向提供该依赖项的所有可能的方式添加限定符。让基本实现或通用实现不带限定符容易出错,并且可能会导致 Hilt 注入错误的依赖项。

Hilt 中的预定义限定符

Hilt 提供了一些预定义的限定符。例如,由于您可能需要来自应用或 activityContext 类,因此 Hilt 提供了 @ApplicationContext@ActivityContext 限定符。

假设本例中的 AnalyticsAdapter 类需要 activity 的上下文。以下代码演示了如何向 AnalyticsAdapter 提供 activity 上下文:

在这里插入图片描述
如需了解 Hilt 中提供的其他预定义绑定,请参阅组件默认绑定。

为 Android 类生成的组件

对于您可以从中执行字段注入的每个 Android 类,都有一个关联的 Hilt 组件,您可以在 @InstallIn 注解中引用该组件。每个 Hilt 组件负责将其绑定注入相应的 Android 类。

前面的示例演示了如何在 Hilt 模块中使用 ActivityComponent

Hilt 提供了以下组件:
在这里插入图片描述

注意:Hilt 不会为广播接收器生成组件,因为 Hilt 直接从 SingletonComponent 注入广播接收器。

组件生命周期

Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例
在这里插入图片描述

注意:ActivityRetainedComponent 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。

组件作用域

默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。

在本例中,每当 Hilt 提供 AnalyticsAdapter 作为其他类型的依赖项或通过字段注入提供它(如在 ExampleActivity 中)时,Hilt 都会提供 AnalyticsAdapter 的一个新实例。

不过,Hilt 也允许将绑定的作用域限定为特定组件。Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。

下表列出了生成的每个组件的作用域注解:
在这里插入图片描述
在本例中,如果您使用 @ActivityScopedAnalyticsAdapter 的作用域限定为 ActivityComponentHilt 会在相应 activity 的整个生命周期内提供 AnalyticsAdapter 的同一实例:

在这里插入图片描述

注意:将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例,绑定需要同步,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。

假设 AnalyticsService 的内部状态要求每次都使用同一实例 - 不只是在 ExampleActivity 中,而是在应用中的任何位置。在这种情况下,将 AnalyticsService 的作用域限定为 SingletonComponent 是一种恰当的做法。结果是,每当组件需要提供 AnalyticsService 的实例时,都会提供同一实例。

以下示例演示了如何将绑定的作用域限定为 Hilt 模块中的某个组件。绑定的作用域必须与其安装到的组件的作用域一致,因此在本例中,您必须将 AnalyticsService 安装在 SingletonComponent 中,而不是安装在 ActivityComponent 中:

在这里插入图片描述
如需详细了解 Hilt 组件作用域,请参阅 Android 和 Hilt 中的作用域限定。

注意:如需详细了解使用 @ActivityRetainedScoped 或 @ViewModelScoped 限定作用域的区别,请参阅 Hilt 和 Jetpack 集成文档中的 @ViewModelScoped 部分。

组件层次结构

将模块安装到组件后,其绑定就可以用作该组件中其他绑定的依赖项,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项

在这里插入图片描述

注意:默认情况下,如果您在视图中执行字段注入,ViewComponent 可以使用 ActivityComponent 中定义的绑定。如果您还需要使用 FragmentComponent 中定义的绑定并且视图是 Fragment 的一部分,应将 @WithFragmentBindings 注解和 @AndroidEntryPoint 一起使用。

组件默认绑定

每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定。请注意,这些绑定对应于常规 activityfragment 类型,而不对应于任何特定子类。这是因为,Hilt 会使用单个 activity 组件定义来注入所有 activity。每个 activity 都有此组件的不同实例。

在这里插入图片描述
还可以使用 @ApplicationContext 获得application上下文绑定。例如:

在这里插入图片描述
此外,还可以使用 @ActivityContext 获得 activity 上下文绑定。例如:

在这里插入图片描述

在 Hilt 不支持的类中注入依赖项

Hilt 支持最常见的 Android 类。不过,您可能需要在 Hilt 不支持的类中执行字段注入。

在这些情况下,您可以使用 @EntryPoint 注解创建入口点。入口点是由 Hilt 管理的代码与并非由 Hilt 管理的代码之间的边界。它是代码首次进入 Hilt 所管理对象的图的位置。入口点允许 Hilt 使用并非由 Hilt 管理的代码提供依赖关系图中的依赖项。

例如,Hilt 并不直接支持 content provider。如果您希望 content provider 使用 Hilt 来获取某些依赖项,需要为所需的每个绑定类型定义一个带有 @EntryPoint 注解的接口并添加限定符。然后,添加 @InstallIn 以指定要在其中安装入口点的组件,如下所示:

在这里插入图片描述

如需访问入口点,请使用来自 EntryPointAccessors 的适当静态方法。参数应该是组件实例或充当组件持有者的 @AndroidEntryPoint 对象。确保您以参数形式传递的组件和 EntryPointAccessors 静态方法都与 @EntryPoint 接口上的 @InstallIn 注解中的 Android 类匹配:

在这里插入图片描述
在本例中,您必须使用 ApplicationContext 检索入口点,因为入口点安装在 SingletonComponent 中。如果您要检索的绑定位于 ActivityComponent 中,应改用 ActivityContext

Hilt 和 Dagger

Hilt 在依赖项注入库 Dagger 的基础上构建而成,提供了一种将 Dagger 纳入 Android 应用的标准方法。

关于 DaggerHilt 的目标如下:

  • 简化 Android 应用的 Dagger 相关基础架构。
  • 创建一组标准的组件和作用域,以简化设置、提高可读性以及在应用之间共享代码。
  • 提供一种简单的方法来为各种 build 类型(如测试、调试或发布)配置不同的绑定。

由于 Android 操作系统会实例化它自己的许多框架类,因此在 Android 应用中使用 Dagger 要求您编写大量的样板。Hilt 可减少在 Android 应用中使用 Dagger 所涉及的样板代码。Hilt 会自动生成并提供以下各项:

  • 用于将 Android 框架类与 Dagger 集成的组件 - 您不必手动创建。
  • 作用域注解 - 与 Hilt 自动生成的组件一起使用。
  • 预定义的绑定 - 表示 Android 类,如 ApplicationActivity
  • 预定义的限定符 - 表示 @ApplicationContext@ActivityContext

DaggerHilt 代码可以共存于同一代码库中。不过,在大多数情况下,最好使用 Hilt 管理您在 Android 上对 Dagger 的所有使用。

将 Hilt 和其他 Jetpack 库一起使用

Hilt 包含可用于从其他 Jetpack 库提供类的扩展。Hilt 目前支持以下 Jetpack 组件:

  • ViewModel
  • Navigation
  • Compose
  • WorkManager

您必须添加 Hilt 依赖项,才能利用这些集成。

使用 Hilt 注入 ViewModel 对象

提供 ViewModel,方法是为其添加 @HiltViewModel 注解,并在 ViewModel 对象的构造函数中使用 @Inject 注解。
在这里插入图片描述
然后,带有 @AndroidEntryPoint 注解的 activityfragment 可以使用 ViewModelProviderby viewModels() KTX 扩展照常获取 ViewModel 实例:

在这里插入图片描述

@ViewModelScoped

所有 Hilt ViewModel 都由 ViewModelComponent 提供,后者遵循与 ViewModel 相同的生命周期,因此可以在配置更改后继续存在。如需将依赖项的作用域限定为 ViewModel,请使用 @ViewModelScoped 注解。

使用 @ViewModelScoped 类型后,系统会在注入 ViewModel 的所有依赖项中提供限定了作用域的类型的单个实例。请求限定了作用域的类型的 ViewModel 的其他实例会收到其他实例。

如果需要在不同 ViewModel 之间共享单个实例,则应使用 @ActivityRetainedScoped@Singleton 限定其作用域。

与 Jetpack Navigation 库集成

请将下面这些额外的依赖项添加到 Gradle 文件中:

dependencies {
    ...
    implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0'
}

如果 ViewModel 的作用域限定为导航图,请使用 hiltNavGraphViewModels 函数,该函数可与带有 @AndroidEntryPoint 注解的 fragment 搭配使用。

val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)

与 Jetpack Compose 集成

ViewModel 部分提及的 viewModel() 函数自动使用 Hilt 通过 @HiltViewModel 注解构造的 ViewModel
在这里插入图片描述

Hilt 和 Navigation

Hilt 还与 Navigation Compose 库集成。请将下面这些额外的依赖项添加到 Gradle 文件中:

dependencies {
    implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
}

使用 Navigation Compose 时,请始终使用 hiltViewModel 可组合函数获取带有 @HiltViewModel 注解的 ViewModel 的实例。该函数可与带有 @AndroidEntryPoint 注解的 fragmentactivity 搭配使用。

例如,如果 ExampleScreen 是导航图中的目的地,请调用 hiltViewModel() 来获取作用域限定为该目的地的 ExampleViewModel 实例,如以下代码段所示:
在这里插入图片描述
如果您需要改为检索作用域限定为导航路线或导航图的 ViewModel 实例,请使用 hiltViewModel 可组合函数并将相应的 backStackEntry 作为参数传递:

在这里插入图片描述

使用 Hilt 注入 WorkManager

将下面这些额外的依赖项添加到 Gradle 文件中。请注意,除了库之外,您还需要添加一个额外的注释处理器,它在 Hilt 注释处理器的基础上运行:

dependencies {
  ...
  implementation 'androidx.hilt:hilt-work:1.0.0'
  // When using Kotlin.
  kapt 'androidx.hilt:hilt-compiler:1.0.0'
  // When using Java.
  annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0'
}

注入一个 Worker,方法是在类中使用 @HiltWorker 注解,并在 Worker 对象的构造函数中使用 @AssistedInject。您只能在 Worker 对象中使用 @Singleton 或未限定作用域的绑定。您还必须使用 @AssistedContextWorkerParameters 依赖项添加注解:

在这里插入图片描述
然后,让 Application 类实现 Configuration.Provider 接口,注入 HiltWorkFactory 的实例,并将其传入 WorkManager 配置,如下所示:

在这里插入图片描述

注意:由于这会自定义 WorkManager 配置,因此您还必须按照 WorkManager 文档中指定的方法,从 AndroidManifest.xml 文件中移除默认的初始化程序。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Hilt组件库背后做了啥

添加了Hilt相应注解的类会在build目录下会生成对应的以Hilt_xxx开头的java类:

在这里插入图片描述

在这里插入图片描述
可以看到,对于 Activity 是在生成的 Hilt_xxxActivity 的构造方法中调用了 inject() 方法。 Hilt_xxxActivity 里面的逻辑很简单,就是将当前对象强转成对应的子类对象,然后保存到对应类型的Component中。

而对于 Application 则是在 onCreate() 方法中调用了 inject() 方法:

import android.app.Application;
import androidx.annotation.CallSuper;
import dagger.hilt.android.internal.managers.ApplicationComponentManager;
import dagger.hilt.android.internal.managers.ComponentSupplier;
import dagger.hilt.android.internal.modules.ApplicationContextModule;
import dagger.hilt.internal.GeneratedComponentManagerHolder;
import dagger.hilt.internal.UnsafeCasts;
import java.lang.Object;
import java.lang.Override;

/**
 * A generated base class to be extended by the @dagger.hilt.android.HiltAndroidApp annotated class. If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
 */
public abstract class Hilt_MyApp extends Application implements GeneratedComponentManagerHolder {
  private boolean injected = false;

  private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
    @Override
    public Object get() {
      return DaggerMyApp_HiltComponents_SingletonC.builder()
          .applicationContextModule(new ApplicationContextModule(Hilt_MyApp.this)).build();
    }
  });

  @Override
  public final ApplicationComponentManager componentManager() {
    return componentManager;
  }

  @Override
  public final Object generatedComponent() {
    return this.componentManager().generatedComponent();
  }

  @CallSuper
  @Override
  public void onCreate() {
    hiltInternalInject();
    super.onCreate();
  }

  protected void hiltInternalInject() {
    if (!injected) {
      injected = true;
      // This is a known unsafe cast, but is safe in the only correct use case:
      // MyApp extends Hilt_MyApp
      ((MyApp_GeneratedInjector) generatedComponent()).injectMyApp(UnsafeCasts.<MyApp>unsafeCast(this));
    }
  }
}

但是,这些都是一些抽象类,那么在哪里实现的呢?

在build目录下搜索会发现添加了相关注解的类都会生成对应类名的dex文件,比如MainActivity会生成一个MainActivity.dex,使用反编译工具打开对应的dex文件进行查看:

在这里插入图片描述
在这里插入图片描述
可以看到,我们自己写的业务类的父类被修改为继承了Hilt生成的Hilt_XXX相关的抽象类。

因此可知,Hilt 会在编译器修改字节码,修改了继承的父类为Hilt_XXXA相关的抽象类,然后在父类中进行了强行注入处理逻辑。

IOC 的优缺点

Hilt 很好,但是我们一定要在项目中使用它吗?文章的最后,我们来总结一下 IOC 的优缺点。

IOC 的优势:

  • 对象之间的耦合度降低
  • 对象实例的创建变得比较容易管理,很容易创建一个全局/局部的共享单例
  • 场景:模板代码创建实例对象,全局或局部对象共享

IOC 的缺点:

  • 代码可读性较差:不知道对象在哪里被创建的?实例创建的时机在哪里?入参是什么?
  • 增加新人学习成本和维护成本,尤其是对团队的新成员而言,对接手的新项目,要熟悉整个IOC框架的运作模式,才敢下手改代码。
  • 额外生成大量的父类或方法来做IOC注入处理逻辑,增加编译成本和App的体积,加速触及65536方法数等

是否有必要引入 Hilt 或其他类似的 IOC?

  • 通常来说,这不是必须的,取决于你的项目复杂程度。如果是简单的小项目,使用它并不会变得更加简单,反而可能会适得其反,学习成本和工作收益不成正比。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

表格相关的一些标签

<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>表格相关的标一些签</title> </head> <body> <!-- 需求 1&#xff1a;做一个四行&#xff0c;三…

Golang进阶

"白昼会边长&#xff0c;照亮心脏,让万物生长。"一、Golang进阶我们对golang的语法进行了一定的了解后&#xff0c;也算是入门了。本节的进阶篇围绕三个方向展开,Goroutine 、 Channel 、Sync。如何理解并行与并发&#xff1f;并行是指“并排行走”或“同时实行或实施…

用数组实现链表、栈和队列

目录前言一、用数组实现链表1.1 单链表1.2 双链表二、用数组实现栈三、用数组实现队列前言 众所周知&#xff0c;链表可以用结构体和指针来实现&#xff0c;而栈和队列可以直接调用STL&#xff0c;那为什么还要费尽心思用数组来实现这三种数据结构呢&#xff1f; 首先&#x…

好的质量+数量 = 健康的创作者生态

缘起 CSDN 每天都有近万名创作者发表各种内容&#xff0c; 其中博客就有一万篇左右。 这个数量是非常可喜的&#xff0c;这也是 CSDN 的产品、研发运营小伙伴、和各位博主持续工作的结果。 衡量一个 IT 内容平台&#xff0c;除了数量之外&#xff0c;还有另外一些因素&#xf…

Linux——动态库

目录 制作并发布动态库 使用动态库 使用动态库程序运行时的错误 制作并发布动态库 静态库的代码在链接的时候会被拷贝进对应的可执行程序内部&#xff0c;动态库则不需要拷贝。 动态库在形成目标文件时&#xff0c;需要加一个选项 -fPIC&#xff1a;形成一个与位置无关的二…

Yocto常用术语

Yocto常用术语 Yocto是一套开源、专为嵌入式定制的编译系统&#xff0c;它提供了toolset和开发环境&#xff0c;开发人员可以利用Yocto定制基于Linux的系统。Yocto官网介绍了其常用术语&#xff0c;官网链接Yocto Project Terms&#xff0c;了解这些术语可以加深对Yocto的认识…

第五章 高级数据管理

在第4章&#xff0c;我们审视了R中基本的数据集处理方法&#xff0c;本章我们将关注一些高级话题。本章分为三个基本部分。在第一部分中&#xff0c;我们将快速浏览R中的多种数学、统计和字符处理函数。为了让这一部分的内容相互关联&#xff0c;我们先引入一个能够使用这些函数…

低功耗广域网LPWAN 8大关键技术对比

物联网被认为是继计算机、互联网之后&#xff0c;世界信息产业发展的第三次浪潮&#xff0c;它的出现将大大改变人们现有的生活环境和习惯。智能家居、工业数据采集等场景通常采用的是短距离通信技术&#xff0c;但对于广范围、远距离的连接&#xff0c;远距离通信技术不可或缺…

分享146个ASP源码,总有一款适合您

ASP源码 分享146个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 146个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1HG8AMPldOPHcEmMsGnVwMA?pwdg97k 提取码&#x…

矩阵的运算、运算规则及C语言实现

在人工智能运算和原理的过程中,我们需要了解非常多的数学知识,但是大学时候学的东西已经忘的差不多了,这里我把矩阵的一系列概念总结并复习一下,以便于大家在学习AI的时候要明白很多数学计算的物理意义,当年在学习线性代数的时候,我们不一定明白这些计算的意义,现在要和…

【图卷积网络】02-谱域图卷积介绍

注&#xff1a;本文为第2章谱域图卷积介绍视频笔记&#xff0c;仅供个人学习使用 目录1、图卷积简介1.1 图卷积网络的迅猛发展1.2 回顾&#xff0c;经典卷积神经网络已在多个领域取得成功1.3 两大类数据1.4 经典卷积神经网络的局限&#xff1a;无法处理图数据结构1.5 将卷积扩展…

代码随想录算法训练营第四十八天|● 198.打家劫舍 ● 213.打家劫舍II ● 337.打家劫舍III

动态规划 一、198.打家劫舍 题目&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系…

流批一体计算引擎-7-[Flink]的DataStream连接器

参考官方手册DataStream Connectors 1 DataStream连接器概述 一、预定义的Source和Sink 一些比较基本的Source和Sink已经内置在Flink里。 1、预定义data sources支持从文件、目录、socket&#xff0c;以及collections和iterators中读取数据。 2、预定义data sinks支持把数据写…

Eclipse中的Build Path

Eclipse中的Build Path简介如果修改了Build Path中的中的JRE版本&#xff0c;记得还需要同步修改Java编译器的版本&#xff0c;如下图红框所示简介 Build Path是Java工程包含的资源属性合集&#xff0c;用来管理和配置此Java工程中【除当前工程自身代码以外的其他资源】的引用…

Vision Transformer 简单复现和解释

一些我自己不懂的过程&#xff0c;我自己在后面写了demo解释。 import torch import torch.nn as nnfrom einops import rearrange, repeat from einops.layers.torch import Rearrangedef pair(t):return t if isinstance(t, tuple) else (t, t) class PreNorm(nn.Module):…

数据库系统概念 | 第七章:使用E-R模型的数据库设计 | ER图设计| ER图转化为关系模型 | 强实体和弱实体

文章目录&#x1f4da;设计过程概览&#x1f4da;实体-联系模型&#x1f407;E-R数据模型&#x1f955;实体集&#x1f955;联系集&#x1f955;属性&#x1f407;E-R图&#x1f4da;映射基数&#x1f407;二元联系集⭐️&#x1f955;一对一&#x1f955;一对多&#x1f955;多…

二叉树的顺序结构——堆的概念实现(图文详解+完整源码 | C语言版)

目录 0.写在前面 1.什么是堆&#xff1f; 2.堆的实现 2.1 堆的结构定义 2.2 函数声明 2.3 函数实现 2.3.1 AdjustUp&#xff08;向上调整算法&#xff09; 2.3.2 AdjustDown&#xff08;向下调整算法&#xff09; 2.3.3 HeapCreate&#xff08;如何建堆&#xff09; …

更多的选择器 更多伪类选择器 颜色选中时写法 被选中的第一行文字 选中第几个元素

目录更多的选择器更多伪类选择器1. first-child2. last-child3. nth-child4. nth-of-type更多的伪元素选择器1. first-letter2. first-line3. selection更多的选择器 更多伪类选择器 1. first-child 选择第一个子元素 圈住的地方意思是&#xff1a;li 的第一个子元素设置为红…

第三篇:Haploview做单倍型教程3--结果解读

大家好&#xff0c;我是邓飞&#xff0c;这里介绍一下如何使用Haploview进行单倍型的分析。 计划分为三篇文章&#xff1a; 第一篇&#xff1a;Haploview做单倍型教程1–软件安装第二篇&#xff1a;Haploview做单倍型教程2–分析教程第三篇&#xff1a;Haploview做单倍型教程…

java中对泛型的理解

那么什么是泛型泛型&#xff1a;是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中&#xff0c;操作的数据类型被指定为一个参数&#xff0c;而这种参数类型可以用在类、方法和接口中&#xff0c;分别被称为泛型类、泛型…