自动完成VM和VB初始化的几种方案
前言
例如我们的 Activity/Fragment 内部的对象初始化,如果是常规的通用的对象初始化,我们当然可以在基类中就定义了。但是对于一些类似ViewModel,ViewBindig之类的对象初始化,我们需要明确知道是哪一个类型才能初始化的怎么办?
类似我们在具体的页面定义泛型:
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
但是我不想在具体的页面去写这些手动的调用:
ViewModelProvider(owner).get(%T::class.java)
%T.inflate(layoutInflater)
或者基类抽象实现:
public abstract class BaseActivity<VM extends ViewModel, VB extends ViewBinding> extends AppCompatActivity {
protected VM viewModel;
protected VB binding;
protected abstract Class<VM> getViewModelClass();
protected abstract VB inflateViewBinding(LayoutInflater inflater);
}
具体还是让子类去实现:
public class ProfileActivity extends BaseActivity<ProfileViewModel, ActivityProfileBinding> {
@Override
protected Class<ProfileViewModel> getViewModelClass() {
return ProfileViewModel.class;
}
@Override
protected ActivityProfileBinding inflateViewBinding(LayoutInflater inflater) {
return ActivityProfileBinding.inflate(inflater);
}
// ...
}
可以是可以,但是好麻烦哦,我想只给个泛型,让基类去自动帮我初始化,能不能直接在基类中:
ViewModelProvider(this).get(VM::class.java)
VB.inflate(inflater)
这样会报错的,因为运行期间泛型会被擦除也无法实例化对应的对象。
那...可如何是好呐。
其实我们想要在基类完成泛型的实例化,我们目前是有两种思路,一种是反射获取到泛型的实例,一种是通过编译器代码生成完成对象的实例创建,其中又分为APT代码生成和ASM字节码插桩两个小分支。
一、使用反射
平常我们的封装就算再简单,我们也需要传入 ViewMiel 的 class 对象,或者 DataBinding::inflate 对象。
abstract class BaseVDBActivity<VM : ViewModel,VB : ViewBinding>(
private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
}
class MainActivity : BaseVDBActivity<ActivityMainBinding, MainViewModel>(
ActivityMainBinding::inflate,
MainViewModel::class.java
){}
类似于上面这种写法,定义了泛型对象,在通过构造传入对应的Class对象和函数对象。我们才能在基类中正常的初始化 ViewModel 和 ViewBinding ,这是很好的封装方式,性能也好,没用到反射,其实已经很优秀了,你绝对可以使用这种方式封装。
本文我们也是从懒人的角度看,除了这种方式之外我们还能用哪些更“懒”的方式来实现自动的初始化。
这里就得提到反射的作用了。
abstract class BaseActivity<VM : ViewModel, VB : ViewBinding> : AppCompatActivity() {
protected lateinit var viewModel: VM
protected lateinit var binding: VB
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModelInstance()
binding = getViewBindingInstance(layoutInflater)
setContentView(binding.root)
}
private fun getViewModelInstance(): VM {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vmClass = (superClass.actualTypeArguments[0] as Class<VM>).kotlin
return ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(vmClass.java)
}
private fun getViewBindingInstance(inflater: LayoutInflater): VB {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vbClass = superClass.actualTypeArguments[1] as Class<VB>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, inflater) as VB
}
}
我们指定第一个泛型为ViewModel,第二个泛型为ViewBinding,那么我们就能找到当前类的泛型对象的class,更进一步我们甚至能通过反射调用它的方法得到 VB 的实例对象。
此时我们就能达到文章开始的效果:
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
反射的方案有没有缺点?
反射太慢了可能有些人会脱口而出,其实反射真的慢吗?这不属于本文探讨的范围,随着越多越多的一些对比评测大家其实也明白过来,反射其实并没有比正常调用慢多少。
虽然反射需要在运行时动态解析类的元数据,执行安全权限检查,以及进行方法调用,虽然反射调用时,JVM会进行额外的安全检查,增加了性能开销,但是如果调用次数很少基本和正常方法调用区别不大,特别是对于 Android 开发的场景,特别还是这种只调用一次的场景,其实运行速度差别真的不大。
混淆,这才是大问题,反射代码在混淆过程中我们需要额外的注意,因为类和成员的名称可能会被改变。如果不正确配置混淆规则,可能导致在运行时无法正确地通过名称找到相应的类、方法或字段,引发异常。
例如我们混淆打包之后,如果通过反射,必须保证反射的直接对象需要保存不被混淆。
我们注释掉混淆规则
# 保持ViewModel和ViewBinding不混淆,否则无法反射自动创建
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }
然后反编译我们的apk很容易的就能找到为混淆的类:
类型安全与可读性 反射调用减少了编译时类型检查的机会,增加了运行时错误的风险。例如,如果通过反射错误地调用了方法或访问了字段,可能会在运行时引发ClassCastException等异常,并且由于是硬编码不好调试不说,如果被反射方改变了方法那么会增加错误的风险。
二、使用APT代码生成
其实相比ASM的字节码插桩,使用APT生成代码相对简单很多,我们可以生成对应的 ViewBinding 和 ViewModel 的初始化对象。
如果你不会 APT 的代码生成,那么跟着过一遍就回了,下面的代码会给出详细的注释。
我们先定义对应的注解:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoInject
我们添加 auto-service 和 kotlinpoet 代码生成器的依赖
implementation 'com.squareup:kotlinpoet:1.4.0'
compileOnly "com.google.auto.service:auto-service:1.0.1"
kapt 'com.google.auto.service:auto-service:1.0.1'
auto-service是一种用于生成APT(Annotation Processing Tool)代码的方案之一。APT是Java编译器提供的一个工具,用于在编译期间处理注解,并生成相应的代码。
auto-service是一个Google开源的库,它简化了使用APT生成代码的过程。它提供了一个注解@AutoService和一个抽象类AutoService,通过在实现类上添加@AutoService注解,并继承AutoService抽象类,可以自动生成用于注册该实现类的META-INF/services文件。
在你的代码中,你使用了auto-service库,并使用@AutoService注解和AutoService抽象类来自动生成META-INF/services文件,用于注册你的注解处理器。这样,当你的项目构建时,编译器会自动调用APT并生成相应的代码。
kotlinpoet 是一个用于生成 Kotlin 代码的库,由 Square 公司开发。KotlinPoet 通过提供一个强大的 DSL(领域特定语言)来帮助开发者编程地构建 Kotlin 源文件。这个库特别适合那些需要自动生成 Kotlin 代码的场景,比如编写编译时注解处理器(Annotation Processors)或是其他需要生成 Kotlin 代码的工具。
两者经常被一起使用,尤其是在创建编译时注解处理器时,当你编写一个注解处理器来处理注解时,可能会用到 KotlinPoet 来生成一些 Kotlin 代码,同时用 AutoService 来注册注解处理器,使得在编译时可以被 javac 工具自动发现和调用。这样可以大大简化注解处理器的开发过程,使得开发者更专注于处理注解的逻辑,而不是服务文件的细节。
本场景的 Processor 定义如下:
@Suppress("unused")
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions(AutoInjectAnnotationProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME)
class AutoInjectAnnotationProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): Set<String> = setOf(
AutoInject::class.java.canonicalName,
)
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(AutoInject::class.java).forEach { element ->
if (element.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "@AutoInject can only be applied to classes.")
return true
}
val typeElement = element as TypeElement
generateCodeForViewModel(element, typeElement)
}
return true
}
private fun generateCodeForViewModel(element: Element, typeElement: TypeElement) {
val className = element.simpleName.toString()
val pack = processingEnv.elementUtils.getPackageOf(element).toString()
val fileName = "${className}ViewModelInit"
val superClassType = (element as TypeElement).superclass //泛型指定在父类上
val typeArguments = (superClassType as? DeclaredType)?.typeArguments ?: emptyList() //获取到所有的泛型
if (typeArguments.isEmpty()) return // 如果没有泛型参数,则不生成代码
val viewModelName = typeArguments[0].asTypeName().toString() // 第一个泛型参数总是用于ViewModel
val typeSpecBuilder = TypeSpec.classBuilder(fileName) // 生成的主要类
.addModifiers(KModifier.PUBLIC) // 指定类是公有的
// 添加方法provideViewModel
typeSpecBuilder.addFunction(
FunSpec.builder("provideViewModel") // 方法名
.addModifiers(KModifier.PUBLIC) // 指定方法是公有的
.addParameter("owner", ClassName("androidx.lifecycle", "ViewModelStoreOwner")) // 参数
.returns(ClassName(pack, viewModelName)) // 返回类型
.addStatement("return ViewModelProvider(owner).get(%T::class.java)", ClassName(pack, viewModelName)) // 具体的方法
.build()
)
// 如果有第二个泛型参数,则生成provideViewBinding方法
if (typeArguments.size > 1) {
val viewBindingName = typeArguments[1].asTypeName().toString()
typeSpecBuilder.addFunction(
FunSpec.builder("provideViewBinding") // 方法名
.addModifiers(KModifier.PUBLIC) // 指定方法是公有的
.addParameter("layoutInflater", ClassName("android.view", "LayoutInflater")) // 参数
.returns(ClassName(pack, viewBindingName)) // 返回类型
.addStatement("return %T.inflate(layoutInflater)", ClassName(pack, viewBindingName)) // 具体的方法
.build()
)
}
val fileSpec = FileSpec.builder(pack, fileName)
.addImport("androidx.lifecycle", "ViewModelProvider") //指定导入类
.addImport("android.view", "LayoutInflater")
.addType(typeSpecBuilder.build())
.build()
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
fileSpec.writeTo(File(kaptKotlinGeneratedDir, "$fileName.kt"))
}
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
}
那么我们只需要标记哪些类需要生成对应的文件即可,例如:
@AutoInject
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
生成的代码:
public class ProfileActivityViewModelInit {
public fun provideViewModel(owner: ViewModelStoreOwner): com.newki.profile.mvi.vm.ProfileViewModel
= ViewModelProvider(owner).get(com.newki.profile.mvi.vm.ProfileViewModel::class.java)
public fun provideViewBinding(layoutInflater: LayoutInflater):
com.newki.profile.databinding.ActivityProfileBinding =
com.newki.profile.databinding.ActivityProfileBinding.inflate(layoutInflater)
}
基类中调用:
protected open fun createViewBinding() {
try {
val currentPackageName = this::class.java.`package`?.name
val className = currentPackageName + "." + this::class.java.simpleName + "ViewModelInit"
val generatedClass = Class.forName(className)
val method = generatedClass.getDeclaredMethod("provideViewBinding", LayoutInflater::class.java)
val generatedClassInstance = generatedClass.getDeclaredConstructor().newInstance()
_binding = method.invoke(generatedClassInstance, layoutInflater) as VB
} catch (e: Exception) {
e.printStackTrace()
}
}
protected open fun createViewModel(): VM {
try {
val currentPackageName = this::class.java.`package`?.name
val className = currentPackageName + "." + this::class.java.simpleName + "ViewModelInit"
val generatedClass = Class.forName(className)
val method = generatedClass.getDeclaredMethod("provideViewModel", ViewModelStoreOwner::class.java)
val generatedClassInstance = generatedClass.getDeclaredConstructor().newInstance()
return method.invoke(generatedClassInstance, this) as VM
} catch (e: Exception) {
e.printStackTrace()
}
}
我们同样可以无感的在基类自动创建对应的初始化代码,需要注意的是我同样需要混淆生成的代码
#自定义的自动注入生成类,保护实现
-keep class **.*ViewModelInit { *; }
当然了,理论上我们可以直接在 ASM 字节码插桩生成的代码中直接在onCreate方法中自动调用给 mViewModel 和 mViewBinding 这两个固定的字段赋值,但是这有点"硬编码"的意思了,一旦在基类中修改了这个变量的名字就会导致异常,如果你确保不会变动,其实也可以直接用字节码插桩或者AOP面向切面自动赋值到这两个变量中。
后记
本文详细介绍了常用的三种封装方案,所以这三种方案你更喜欢哪一种?
原始的:
abstract class BaseActivity<VM : ViewModel,VB : ViewBinding>(
private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
}
class ProfileActivity : BaseActivity<ActivityProfileBinding, ProfileViewModel>(
ActivityProfileBinding::inflate,
ProfileViewModel::class.java
){}
反射:
abstract class BaseActivity<VM : ViewModel,VB : ViewBinding>(){
//...
private fun getViewModelInstance(): VM {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vmClass = (superClass.actualTypeArguments[0] as Class<VM>).kotlin
return ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(vmClass.java)
}
private fun getViewBindingInstance(inflater: LayoutInflater): VB {
val superClass = javaClass.genericSuperclass as ParameterizedType
val vbClass = superClass.actualTypeArguments[1] as Class<VB>
val method = vbClass.getMethod("inflate", LayoutInflater::class.java)
return method.invoke(null, inflater) as VB
}
}
使用:
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
APT的使用:
@AutoInject //自定义注解,自己定义
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}
总的来说三种方案各有利弊,都是可以实现的,用哪一种方案完全看自己的意愿。
【Android】只给个泛型,如何自动初始化ViewModel与ViewBinding?这几种方案值得了解
作者:Newki
链接:https://juejin.cn/post/7357546247849197606
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。