【Android】只给个泛型,如何自动初始化ViewModel与ViewBinding?这几种方案值得了解

2024-06-11 19:11:23 浏览数 (1)

链接:https://juejin.cn/post/7357546247849197606 本文由作者授权发布

前言

例如我们的 Activity/Fragment 内部的对象初始化,如果是常规的通用的对象初始化,我们当然可以在基类中就定义了。但是对于一些类似ViewModel,ViewBindig之类的对象初始化,我们需要明确知道是哪一个类型才能初始化的怎么办?

类似我们在具体的页面定义泛型:

代码语言:javascript复制
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

但是我不想在具体的页面去写这些手动的调用:

代码语言:javascript复制
ViewModelProvider(owner).get(%T::class.java)
%T.inflate(layoutInflater)

或者基类抽象实现:

代码语言:javascript复制
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);
}

具体还是让子类去实现:

代码语言:javascript复制
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);
    }

    // ...
}

可以是可以,但是好麻烦哦,我想只给个泛型,让基类去自动帮我初始化,能不能直接在基类中:

代码语言:javascript复制
ViewModelProvider(this).get(VM::class.java)
VB.inflate(inflater)

这样会报错的,因为运行期间泛型会被擦除也无法实例化对应的对象。

那...可如何是好呐。

其实我们想要在基类完成泛型的实例化,我们目前是有两种思路,一种是反射获取到泛型的实例,一种是通过编译器代码生成完成对象的实例创建,其中又分为APT代码生成和ASM字节码插桩两个小分支。

一、使用反射

平常我们的封装就算再简单,我们也需要传入 ViewMiel 的 class 对象,或者 DataBinding::inflate 对象。

代码语言:javascript复制
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 ,这是很好的封装方式,性能也好,没用到反射,其实已经很优秀了,你绝对可以使用这种方式封装。

本文我们也是从懒人的角度看,除了这种方式之外我们还能用哪些更“懒”的方式来实现自动的初始化。

这里就得提到反射的作用了。

代码语言:javascript复制
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 的实例对象。

此时我们就能达到文章开始的效果:

代码语言:javascript复制
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

反射的方案有没有缺点?

反射太慢了可能有些人会脱口而出,其实反射真的慢吗?这不属于本文探讨的范围,随着越多越多的一些对比评测大家其实也明白过来,反射其实并没有比正常调用慢多少。

虽然反射需要在运行时动态解析类的元数据,执行安全权限检查,以及进行方法调用,虽然反射调用时,JVM会进行额外的安全检查,增加了性能开销,但是如果调用次数很少基本和正常方法调用区别不大,特别是对于 Android 开发的场景,特别还是这种只调用一次的场景,其实运行速度差别真的不大。

混淆,这才是大问题,反射代码在混淆过程中我们需要额外的注意,因为类和成员的名称可能会被改变。如果不正确配置混淆规则,可能导致在运行时无法正确地通过名称找到相应的类、方法或字段,引发异常。

例如我们混淆打包之后,如果通过反射,必须保证反射的直接对象需要保存不被混淆。

我们注释掉混淆规则

代码语言:javascript复制
# 保持ViewModel和ViewBinding不混淆,否则无法反射自动创建
-keep class * implements androidx.viewbinding.ViewBinding { *; }
-keep class * extends androidx.lifecycle.ViewModel { *; }

然后反编译我们的apk很容易的就能找到为混淆的类:

类型安全与可读性 反射调用减少了编译时类型检查的机会,增加了运行时错误的风险。例如,如果通过反射错误地调用了方法或访问了字段,可能会在运行时引发ClassCastException等异常,并且由于是硬编码不好调试不说,如果被反射方改变了方法那么会增加错误的风险。

二、使用APT代码生成

其实相比ASM的字节码插桩,使用APT生成代码相对简单很多,我们可以生成对应的 ViewBinding 和 ViewModel 的初始化对象。

如果你不会 APT 的代码生成,那么跟着过一遍就回了,下面的代码会给出详细的注释。

我们先定义对应的注解:

代码语言:javascript复制
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoInject

我们添加 auto-service 和 kotlinpoet 代码生成器的依赖

代码语言:javascript复制
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 定义如下:

代码语言:javascript复制
@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"
    }
}

那么我们只需要标记哪些类需要生成对应的文件即可,例如:

代码语言:javascript复制
@AutoInject
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

生成的代码:

代码语言:javascript复制
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)
}

基类中调用:

代码语言:javascript复制
  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()
        }
    }

我们同样可以无感的在基类自动创建对应的初始化代码,需要注意的是我同样需要混淆生成的代码

代码语言:javascript复制
#自定义的自动注入生成类,保护实现
-keep class **.*ViewModelInit { *; }

当然了,理论上我们可以直接在 ASM 字节码插桩生成的代码中直接在onCreate方法中自动调用给 mViewModel 和 mViewBinding 这两个固定的字段赋值,但是这有点"硬编码"的意思了,一旦在基类中修改了这个变量的名字就会导致异常,如果你确保不会变动,其实也可以直接用字节码插桩或者AOP面向切面自动赋值到这两个变量中。

后记

本文详细介绍了常用的三种封装方案,所以这三种方案你更喜欢哪一种?原始的:

代码语言:javascript复制
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
){}

反射:

代码语言:javascript复制
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
    }
}

使用:

代码语言:javascript复制
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

APT的使用:

代码语言:javascript复制
@AutoInject  //自定义注解,自己定义
class ProfileActivity : BaseActivity<ProfileViewModel, ActivityProfileBinding>() {}

总的来说三种方案各有利弊,都是可以实现的,用哪一种方案完全看自己的意愿。

如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。

0 人点赞