从事网站开发办理什么个体/优化网站界面的工具
前言
之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。
通过本篇文章,你将了解到:
- 什么是依赖注入?
- Hilt 的引入与基本使用
- Hilt 的进阶使用
- Hilt 原理简单分析
- Android到底该不该使用DI框架?
1. 什么是依赖注入?
什么是依赖?
以手机为例,要组装一台手机,我们需要哪些部件呢?
从宏观上分类:软件+硬件。
由此我们可以说:手机依赖了软件和硬件。
而反映到代码的世界:
class FishPhone(){val software = Software()val hardware = Hardware()fun call() {//打电话software.handle()hardware.handle()}
}
//软件
class Software() {fun handle(){}
}
//硬件
class Hardware() {fun handle(){}
}
FishPhone 依赖了两个对象:分别是Software和Hardware。
Software和Hardware是FishPhone的依赖(项)。
什么是注入?
上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)
有几种方式:
- 构造函数传入
- SetXX函数传入
- 从其它对象间接获取
构造函数依赖注入:
class FishPhone(val software: Software, val hardware: Hardware){fun call() {//打电话software.handle()hardware.handle()}
}
FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。
为什么需要依赖注入框架?
手机制造出来后交给客户使用。
class Customer() {fun usePhone() {val software = Software()val hardware = Hardware()FishPhone(software, hardware).call()}
}
用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?
而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?
你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?
解耦
再看看如下Demo:
interface ISoftware {fun handle()
}//硬件
interface IHardware {fun handle()
}//软件
class SoftwareImpl() : ISoftware {override fun handle() {}
}//硬件
class HardwareImpl : IHardware {override fun handle() {}
}class FishPhone() {val software: ISoftware = SoftwareImpl()val hardware: IHardware = HardwareImpl()fun call() {//打电话software.handle()hardware.handle()}
}
FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。
应该改为如下形式:
class FishPhone(val software: ISoftware, val hardware: IHardware) {fun call() {//打电话software.handle()hardware.handle()}
}
消除模板代码
即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:
//硬件
class HardwareImpl : IHardware {val cpu = CPU(Regisgter(), Cal(), Bus())val gpu = GPU(Image(), Video())val disk = Disk(Block(), Flash())//...其它模块override fun handle() {}
}
现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。
class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {override fun handle() {}
}
可以看出,下面的代码比上面的简洁多了。
- 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架
- 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期
- 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成
先想想若是我们想要实现这样的框架需要怎么做呢?
相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。
这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。
2. Hilt 的引入与基本使用
Hilt的引入
从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。
前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。
以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。
一:project级别的build.gradle 引入如下代码:
plugins {//指定插件地址和版本id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}
二:module级别的build.gradle引入如下代码:
plugins {id 'com.android.application'id 'org.jetbrains.kotlin.android'//使用插件id 'com.google.dagger.hilt.android'//kapt生成代码id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置
Hilt的简单使用
前置步骤整好了接下来看看如何使用。
一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:
@HiltAndroidApp
class MyApp : Application() {override fun onCreate() {super.onCreate()}
}
@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。
二:注入一个对象到MyApp里:
有个类定义如下:
class Software {val name = "fish"
}
我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:
class Software @Inject constructor() {val name = "fish"
}
在构造函数前添加了@Inject注解,表示该类可以被注入。
而在MyApp里使用Software对象:
@HiltAndroidApp
class MyApp : Application() {@Injectlateinit var software: Softwareoverride fun onCreate() {super.onCreate()println("inject result:${software.name}")}
}
对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。
最后查看打印输出正确,说明Software对象被创建了。
这是最简单的Hilt应用,可以看出:
- 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了
- @HiltAndroidApp 只用于修饰Application
如何注入接口?
一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:
interface ISoftware {fun printName()
}class SoftwareImpl @Inject constructor(): ISoftware{override fun printName() {println("name is fish")}
}@HiltAndroidApp
class MyApp : Application() {@Injectlateinit var software: ISoftwareoverride fun onCreate() {super.onCreate()println("inject result:${software.printName()}")}
}
不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。
二:正确示范
再定义一个类如下:
@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {@Bindsabstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}
- @Module 表示该类是一个Hilt的Module,固定写法
- @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局
- 一个抽象类,类名随意
- 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,
如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
可以看出,实际注入的是SoftwareImpl。
@Binds 适用在我们能够修改类的构造函数的场景
如何注入第三方类
上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。
在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?
一:定义Provides模块
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {@Providesfun provideHardware():Hardware {return Hardware()}
}
- @Module和@InstallIn 注解是必须的
- 定义object类
- 定义函数,方法名随意,返回类型为我们需要注入的类型
- 函数体里通过构造或是其它方式创建具体实例
- 使用@Provides注解函数
二:依赖使用
而Hardware定义如下:
class Hardware {fun printName() {println("I'm fish")}
}
在MyApp里引用Hardware:
虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。
当然我们也可以注入接口:
interface IHardware {fun printName()
}class HardwareImpl : IHardware {override fun printName() {println("name is fish")}
}
想要注入IHardware接口,需要定义provides模块:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {@Providesfun provideHardware():IHardware {return HardwareImpl()}
}
@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象
3. Hilt 的进阶使用
限定符
上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?
比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:
class SoftwareChina @Inject constructor() : ISoftware {override fun printName() {println("from china")}
}class SoftwareUS @Inject constructor() : ISoftware {override fun printName() {println("from US")}
}@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {@Bindsabstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware@Bindsabstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}//依赖注入:
@Inject
lateinit var software: ISoftware
兴高采烈的进行编译,然而却报错:
也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。
这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。
改造一下:
@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {@Binds@Chinaabstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware@Binds@USabstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China
定义新的注解类,使用@Qualifier修饰。
而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。
最后在引用依赖注入的地方分别使用@China @US修饰。
@Inject@USlateinit var software1: ISoftware@Inject@Chinalateinit var software2: ISoftware
此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。
@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景
预定义限定符
上面提及的限定符我们还可以扩展其使用方式。
你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:
class Software @Inject constructor(val context: Context) {val name = "fish"fun getWindowService(): WindowManager?{return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager}
}
//注入
@Inject
lateinit var software: Software
这个时候编译会报错:
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。
由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。
先定义Module:
@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {@Provides@GlobalContextfun provideContext(): Context? {return MyApp.myapp}
}@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext
再注入Context:
class Software @Inject constructor(@GlobalContext val context: Context?) {val name = "fish"fun getWindowService(): WindowManager?{return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager}
}
可以看出,借助@Provides和@Qualifier,可以实现全局的Context。
当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。
与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:
@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}
因此我们只需要在需要的地方引用它即可:
class Software @Inject constructor(@ApplicationContext val context: Context?) {val name = "fish"fun getWindowService(): WindowManager?{return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager}
}
如此一来我们无需重新定义Module。
- 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。
- 如果想自己提供限定符,可以参照GlobalContext的做法。
组件作用域和生命周期
Hilt支持的注入点(类)
以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
除了Application和Activity,Hilt内置支持的注入点如下:
除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。
注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖
Hilt组件的生命周期
什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。
因此Hilt的组件有两个主要功能:
- 创建、注入依赖的对象
- 管理对象的生命周期
Hilt组件如下:
可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。
你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。
继续看个例子:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {@Providesfun provideHardware():IHardware {return HardwareImpl()}
}
@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里,SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。
问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?
答案是否定的。
这就涉及到组件的作用域。
组件的作用域
想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {@Provides@Singletonfun provideHardware():IHardware {return HardwareImpl()}
}
当我们在任何地方注入IHardware时,获取到的都是同一个实例。
除了@Singleton表示组件的作用域,还有其它对应组件的作用域:
简单解释作用域:
@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例
@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例
@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例
@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致
- Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象
- 组件的作用域要么不指定,要指定那必须和组件的生命周期一致
以下几种写法都不符合第二种限制:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {@Provides@ActivityScoped//错误,和组件的作用域不一致fun provideHardware():IHardware {return HardwareImpl()}
}@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {@Provides@Singleton//错误,和组件的作用域不一致fun provideHardware():IHardware {return HardwareImpl()}
}@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {@Provides@ActivityScoped//错误,和组件的作用域不一致fun provideHardware():IHardware {return HardwareImpl()}
}
除了修饰Module,作用域还可以用于修饰构造函数:
@ActivityScoped
class Hardware @Inject constructor(){fun printName() {println("I'm fish")}
}
@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。
构造函数里无法注入的字段
一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。
class Hardware @Inject constructor(val context: Context) {fun printName() {println("I'm fish")}
}
以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:
class Hardware @Inject constructor(@ApplicationContext val context: Context) {fun printName() {println("I'm fish")}
}
这就可以成功注入了。
再看看此种场景:
class Hardware @Inject constructor(@ApplicationContext val context: Context,val version: String,
) {fun printName() {println("I'm fish")}
}
很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。
由此引入新的写法:辅助注入
class Hardware @AssistedInject constructor(@ApplicationContext val context: Context,@Assistedval version: String,
) {//辅助工厂类@AssistedFactoryinterface Factory{//不支持注入的参数都可以放这,返回值为待注入的类型fun create(version: String):Hardware}fun printName() {println("I'm fish")}
}
在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {private lateinit var binding: ActivitySecondBinding@Injectlateinit var hardwareFactory : Hardware.Factoryoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivitySecondBinding.inflate(layoutInflater)setContentView(binding.root)val hardware = hardwareFactory.create("3.3.2")println("${hardware.printName()}")}
}
如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。
自定义注入点
Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。
思考一种场景:小明同学写的模块都是需要注入:
class Hardware @Inject constructor(val gpu: GPU,val cpu: CPU,
) {fun printName() {println("I'm fish")}
}class GPU @Inject constructor(val videoStorage: VideoStorage){}//显存
class VideoStorage @Inject constructor() {}class CPU @Inject constructor(val register: Register) {}//寄存器
class Register @Inject() constructor() {}
此时小刚需要引用Hardware,他有两种选择:
- 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。
- 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。
这个时候适合小刚的方案是:
自定义注入点
方案实施步骤:
一:定义入口点
@InstallIn(SingletonComponent::class)
interface HardwarePoint {//该注入点负责返回Hardware实例fun getHardware(): Hardware
}
二:通过入口点获取实例
class XiaoGangPhone {fun getHardware(context: Context):Hardware {val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)return entryPoint.getHardware()}
}
三:使用Hardware
val hardware = XiaoGangPhone().getHardware(this)println("${hardware.printName()}")
注入object类
定义了object类,但在注入的时候也需要,可以做如下处理:
object MySystem {fun getSelf():MySystem {return this}fun printName() {println("I'm fish")}
}@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {@Provides@Singletonfun provideSystem():MySystem {return MySystem.getSelf()}
}
//使用注入
class Middleware @Inject constructor(val mySystem:MySystem
) {
}
4. Hilt 原理简单分析
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}
Hilt通过apt在编译时期生成代码:
public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {private boolean injected = false;Hilt_SecondActivity() {super();//初始化注入监听_initHiltInternal();}Hilt_SecondActivity(int contentLayoutId) {super(contentLayoutId);_initHiltInternal();}private void _initHiltInternal() {addOnContextAvailableListener(new OnContextAvailableListener() {@Overridepublic void onContextAvailable(Context context) {//真正注入inject();}});}protected void inject() {if (!injected) {injected = true;//通过manager获取组件,再通过组件注入((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));}}
}
在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。
由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值
真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。
5. Android到底该不该使用DI框架?
有人说DI比较复杂,还不如我直接构造呢?
又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。
从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
摘抄官网的:现代Android 应用架构
通常来说我们这么设计UI层到数据层的架构:
class MyViewModel @Inject constructor(val repository: LoginRepository
) :ViewModel() {}class LoginRepository @Inject constructor(val rds : RemoteDataSource,val lds : LocalDataSource
) {}//远程来源
class RemoteDataSource @Inject constructor(val myRetrofit: MyRetrofit
) {}class MyRetrofit @Inject constructor(
) {}//本地来源
class LocalDataSource @Inject constructor(val myDataStore: MyDataStore
) {}class MyDataStore @Inject constructor() {}
可以看出,层次比较深,使用了Hilt简洁了许多。
本文基于 Hilt 2.48.1
参考文档:
https://dagger.dev/hilt/gradle-setup
https://developer.android.com/topic/architecture/recommendations?hl=zh-cn
https://repo.maven.apache.org/maven2/com/google/dagger/hilt/android/com.google.dagger.hilt.android.gradle.plugin/
相关文章:

Android使用Hilt依赖注入,让人看不懂你代码
前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网&…...

ZYNQ连载01-ZYNQ介绍
ZYNQ连载01-ZYNQ介绍 1. ZYNQ 参考文档:《ug585-zynq-7000-trm.pdf》 ZYNQ分为PS和PL两大部分,PS即ARM,PL即FPGA,PL作为PS的外设。 2. 方案 ZYNQ7020为双核A9架构,多核处理器常用的运行模式为AMP(非对称多处理)和…...

第十节——Vue组件
一、什么是组件 组件(Component)是vue.js中很强大的一个功能,可以将一些可重用的代码进行封重用。 所有的Vue 组件同时也是Vue 的实例,可以接受使用相同的选项对象和提供相同的生命周期钩子。 一句话概括:组件就是可以扩展HTML元素ÿ…...

Redis(01)| 数据结构
这里写自定义目录标题 Redis 速度快的原因除了它是内存数据库,使得所有的操作都在内存上进行之外,还有一个重要因素,它实现的数据结构,使得我们对数据进行增删查改操作时,Redis 能高效的处理。 因此,这次我…...

SpringBoot工程启动时自动创建数据库、数据表
文章目录 一,序二,自动创建数据库1. 数据源配置2. 修改支持数据库创建 三,自动创建数据库表以及数据1. 准备DDL、DML语句1.)典型DDL语句2.)典型DML语句 2. 设置初始化参数 四、源码传送 一,序 针对Java工程…...

Uni-app智慧工地可视化信息平台源码
智慧工地的核心是数字化,它通过传感器、监控设备、智能终端等技术手段,实现对工地各个环节的实时数据采集和传输,如环境温度、湿度、噪音等数据信息,将数据汇集到云端进行处理和分析,生成各种报表、图表和预警信息&…...

计算机网络重点概念整理-第五章 传输层【期末复习|考研复习】
第五章 传输层 【期末复习|考研复习】 计算机网络系列文章传送门: 第一章 计算机网络概述 第二章 物理层 第三章 数据链路层 第四章 网络层 第五章 传输层 第六章 应用层 第七章 网络安全 计算机网络整理-简称&缩写 文章目录 第五章 传输层 【期末复习|考研复习…...

Java毕业设计 SpringBoot 新能源充电桩管理系统
Java毕业设计 SpringBoot 新能源充电桩管理系统 SpringBoot 新能源充电桩管理系统 功能介绍 管理员 登录 验证码 注册 系统用户管理 普通用户管理 通知公告管理 留言管理 充电站管理 充电桩管理 充电桩预约 充电管理 订单管理 修改密码 普通用户 登录 修改个人资料 通知公告…...

JNI接口
NewStringUTF和NewString接口测试 打开输入法“显示表情与符号” 右键,拷贝字符简介 🍌 香蕉 Unicode: U1F34C,UTF-8: F0 9F 8D 8C unicode码 🍌 \U0001f34c utf-8编码为0xf09f8d8c,结合char或char8_t的长度&…...

国内好用的免费ai软件
国内就只推荐几个大厂,基本感受都差不多 百度文心一言 百度研发的知识增强大语言模型,能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。 地址:文心一言 (baidu.com) 阿里通…...

MAC缓解WebUI提示词反推
当前环境信息: 在mac上安装好stable diffusion后,能做图片生成了之后,遇到一些图片需要做提示词反推,这个时候需要下载一个插件,参考: https://gitcode.net/ranting8323/stable-diffusion-webui-wd14-tagg…...

【设计模式之原型模式 】– C++
5. 原型模式 – 复制粘贴,一摸一样 简介 原型模式(Prototype Pattern)是一种创建型设计模式,其主要目的是通过复制现有对象来创建新的对象,而无需从头开始构建。 使用场景 它通常用于以下情况: 当一个系统…...

Flask路由机制分析之二
一、前言 上篇 《Flask 路由机制分析之一》主要讲了Python函数的特性以及装饰器的基本概念,这节我们具体分析一下路由内部机制,Flask路由依赖于werkzegu的routing模块来实现。 二、werkzegu的routing模块介绍 Werkzegu库的routing模块主要功能在于URL…...

vue中如何获取当时时间时分秒
在 Vue 中可以使用 JavaScript 中的 Date 对象来获取当前时间,然后使用 Vue 中的数据绑定将时间显示在页面上。 <template><div><p>当前时间:{{ time }}</p></div> </template><script> export default {dat…...

matlab simulink 直线一级倒立摆控制(自起摆和稳态控制)
1、内容简介 略 6-可以交流、咨询、答疑 2、内容说明 控制器设计 自起摆建模 规定正方向:顺时针为角度(力矩)正方向,向右为位移正方向。 在规定的正方向条件下,图 1 所示摆杆的角度φ为正值, 下车向右加…...

Transformers实战(二)快速入门文本相似度、检索式对话机器人
Transformers实战(二)快速入门文本相似度、检索式对话机器人 1、文本相似度 1.1 文本相似度简介 文本匹配是一个较为宽泛的概念,基本上只要涉及到两段文本之间关系的,都可以被看作是一种文本匹配的任务, 只是在具体…...

【错误解决方案】ModuleNotFoundError: No module named ‘PeptideBuilder‘
1. 错误提示 在python程序中,试图导入一个不存在的模块PeptideBuilder导致的错误: 错误提示:ModuleNotFoundError: No module named PeptideBuilder 2. 解决方案 解决方案是确保你已经正确安装了PeptideBuilder模块。你可以通过pip来安装它…...

汇编学习(1)
汇编、CPU架构、指令集、硬编码之间的关系 ● 汇编语言:这是一种低级语言,用于与硬件直接交互。它是由人类可读的机器码或指令组成的,这些指令告诉CPU如何执行特定的任务。每条汇编指令都有一个对应的机器码指令,CPU可以理解和执…...

C#,数值计算——分类与推理Svmlinkernel的计算方法与源程序
1 文本格式 using System; namespace Legalsoft.Truffer { public class Svmlinkernel : Svmgenkernel { public int n { get; set; } public double[] mu { get; set; } public Svmlinkernel(double[,] ddata, double[] yy) : base(yy, ddata) …...

【鸿蒙软件开发】ArkTS容器组件之Badge
文章目录 前言一、Badge组件1.1 子组件1.2 接口接口1参数 接口2参数 BadgePosition枚举说明BadgeStyle对象说明 1.3 示例代码 总结 前言 Badge组件:可以附加在单个组件上用于信息标记的容器组件。 一、Badge组件 可以附加在单个组件上用于信息标记的容器组件。 说…...

H5游戏源码分享-命悬一线
H5游戏源码分享-命悬一线 在合适的时机跳下绳子,能安全站到木桩上,就通过。 游戏源码 <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><meta name&…...

【电路笔记】-交流电阻和阻抗
交流电阻和阻抗 文章目录 交流电阻和阻抗1、概述:电阻率2、交流状态与直流状态近似性3、交流状态与直流状态的差异性3.1 趋肤效应(The Skin Effect)3.2 靠近效应(The Proximity Effect) 4、总结 电阻是一种特性,用于表征当电压差施…...

android开发使用OkHttp自带的WebSocket实现IM功能
一、背景 android app开发经常会有IM需求,很多新手不晓得如何入手,难点在于通讯不中断。其实android发展到今天,很多技术都很完善,有很多类似框架可以实现。例如有:okhttp自带的websocket框架、easysocket等等。本文主…...

前端小技巧: TS实现柯里化函数
实现 curry 函数,把其他函数柯里化 curry 返回一个函数fn执行fn, 中间状态返回函数,如 add(1), 或者 add(1)(2)最后返回执行结果,如 add(1)(2)(3) function curry(fn:Function) {const fnArgsLen fn.length // 传入函数的参数长度let args…...

【算法-数组2】有序数组的平方 和 长度最小的子数组
今天,带来数组相关算法的讲解。文中不足错漏之处望请斧正! 理论基础点这里 有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 示例 1: 输…...

H5游戏源码分享-接苹果游戏拼手速
H5游戏源码分享-接苹果游戏拼手速 看看在20秒内能接多少个苹果 <html> <head><title>我是你的小苹果</title><meta charset"utf-8"/><meta name"viewport" content"initial-scale1, user-scalableno, minimum-scale…...

详解类生到死的来龙去脉
类生命周期和加载过程 一个类在 JVM 里的生命周期有 7 个阶段,分别是加载(Loading)、校验(Verification)、准备(Preparation)、解析(Resolution)、初始化(Ini…...

寻找倒数第K个节点
这篇文章也是凑数的 ... 寻找倒数第K个节点 描述 : 找出单向链表中倒数第 k 个节点。返回该节点的值。 题目 : LeetCode 返回倒数第K个节点 : 面试题 02.02. 返回倒数第 k 个节点 说明 : 给定的 k 保证是有效的。 分析 : 我们给出个例子 : 首先,我们创建两个…...

[ROS系列]ubuntu 20.04 从零配置orbslam3(无坑版)
目录 背景: 结果展示: 一、配置虚拟机 二、 同步网络时间 三、ping网络 四、 安装ros 五、下载源码 六、下载orb_slam3 error1:Pangolin error2: ./HelloPangolin: error while loading shared libraries: libpango_windowing.so: cannot open shared object file…...

网络协议--TCP的保活定时器
23.1 引言 许多TCP/IP的初学者会很惊奇地发现可以没有任何数据流通过一个空闲的TCP连接。也就是说,如果TCP连接的双方都没有向对方发送数据,则在两个TCP模块之间不交换任何信息。例如,没有可以在其他网络协议中发现的轮询。这意味着我们可以…...