阅读 2868

Android厂商推送Plugin化 | 掘金技术征文-双节特别篇

背景

由于要把项目内的推送能力提供给别的业务接入,当前已经接入了FCM(谷歌推送),HMS,小米,Vivo,OPPO,极光等好多平台的推送。但是业务接入可能只需要其中的几种而已,抛开SDK设计的一部分,光光从接入成本上来说其实就比较复杂了,下面是问题的汇总啊。

  1. 要先设置很多AppKey之类的。
  2. 要设置些类似applicationId之类的pleaceHolder
  3. FCM和HMS现在是通过plugin的方式来接入的,多个plugin会让开发迷惑行为
  4. 要动态设置很多推送策略,根据不同的厂商决定当前的推送策略等。
  5. 如何将aar变更成源码依赖。

说实话,只要写的越多那么可能发生问题的地方也就越多,特别是推送业务本身就存在很多不确定性(厂商抽风啥的)。

如何解决这些问题

我们先把推送plugin配置在'com.android.application'下面,这一段可以通过Plugin内的定义。通过PluginExtension把一些动态配置的变更成参数传入Plugin中,然后通过这些配置来完成我们所需要的推送业务聚合。

举个例子,A app只需要HMS和极光,则只需要配置其中两项,并不会引入其他推送的代码,而B app则需要所有的,则会根据这些配置引入所有的仓库,和动态生成配置文件。

这种方式有什么好处呢?首先它更直观,配置的地方会更少,同时避免了由开发去定义这些配置策略,这样对于使用方来说,他们只需要完成简单的初始化配置就可以使用这些推送业务了。

buildSrc + setting

给大家安利下这个模式,buildSrc的模式下,我们可以不需要推本地的jar就可以直接调试plugin插件,这个就解决了plugin插件非常不好调试的问题。

include ':plugin'
project(":plugin").projectDir = new File( "../plugin")
复制代码

但是buildSrc其实也有点小问题,就是这个东西不是特别方便我们去发布plugin项目。这个时候安利大家一个小姿势了。buildSrc下面其实可以使用setting.gradle,这个时候我们就可以把plugin的module引入,这样就可以同时兼顾buildSrc的快速调试,同时又可以很方便的发布项目了。

安利下我大佬的一个Demo项目,有兴趣的可以参考下这个。

小贴士 对于插件的调试 可以在./gradlew taskName -s 这样把异常日志打印出来

如果不会调试Gradle的同学可以学习下这个,也可以方便你学习Gradle Plugin的源代码,传送门只Gradle plugin debug

buildTypes resValue

我们有时候在写build.gradle的时候,会在buildTypes中增加一些resValue "string", "AppName", "app1"类似的东西。

我们最后会在build/generated/下面生成一个资源文件gradleResValue.xml,它会在在编译时会被合并到项目资源文件内。那么我们如何在plugin内如何使用这个呢?

                        val android = project.extensions.getByName("android") as BaseAppModuleExtension
                        val config = android.defaultConfig
                        config.resValue("string", key, value)
复制代码

我们可以通过project.extensions去获取到当前项目下的其他的Extension。这个时候我们只要获取到androidExtension,就可以像在build.gradle调用resValue一样,调用defaultConfig内的resValue方法,去添加资源信息了。

修改Mainfest?

由于项目内的一些特殊编码需求,我们要根据applicationId的不同来设置不同的placeHolder。

如果不用ManifestPleaceHolder的方法,我们可以不可以考虑下通过什么方式去在manifest合并完成之后,再对这个已经定稿的manifest做一些修改。

  val variantName = variant.name.capitalize()
  val processManifestTask = project.tasks.getByName("process${variantName}Manifest")
          as ProcessApplicationManifest
  processManifestTask.doLast {
  }
复制代码

上面的代码可以从projcet的task内获取到Manifest合并的Task。上一篇文章我介绍过,Task作为Gradle任务的核心单元,其实我们可以在doFirst,doLast对这个Task进行一定的修改。

比如说任务完成之后我们可以通过文件路径对Manifest的xml进行一些修改的操作,这样就能根据不同的代码需要对manifest做一些增删改查了,最后只要覆盖当前的Manifest文件就会对整个项目生效。

如何在Plugin中添加依赖?

大家有没有想过项目内的dependencies是什么东西呢??

其实我们在项目内添加的implementation,api等等,这些操作都只是在Project的DefaultDependencyHandler内添加一个数据结构,其中包含了group+name+version,最后在Configuration内添加真实的依赖关系。

可以参考下我大佬的文章Gradle Configuration

  project.dependencies.add(Implement, "com.squareup.okhttp3:okhttp:4.9.0")
复制代码

我们只要在project中获取到dependencies,然后调用add方法就可以在plugin中,给项目添加依赖的aar了。

如何在Plugin中添加另外一个Plugin?

由于项目内FCMHMS都需要引入一个厂商编写的Plugin,而当使用方要去接入的时候就会造成很多问题。那么我们能不能通过我们自己的插件去把这些插件依赖也整合起来呢?

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'commons-io:commons-io:2.6'
    implementation "com.squareup:javapoet:1.13.0"
    implementation 'com.huawei.agconnect:agcp:1.3.1.300'
    implementation "com.google.gms:google-services:4.2.0"
}
复制代码

首先我们要通过implementation把我们需要的插件先整合进来。要不然我们不就引用不到他们的Plugin的代码。

        val plugins = project.pluginManager
        if (!plugins.hasPlugin("com.google.android.gms.google-services")) {
              plugins.apply(GoogleServicesPlugin::class.java)
        }
复制代码

从代码上来解释,我们只是给project添加一个额外的Plugin而已,所以相对来说还是比较简单的。当然这个Plugin是实际会生效的,各位可以放心。

根据条件生成策略类

首先抛出一个问题,Plugin内有没有什么节点可以和apt一样生成一个java代码呢?

各位大佬,不知道有没有了解过Jake大神的ButterKnife'com.jakewharton.butterknife'插件是如何生成R2的。

因为以前的Module内的R.Id因为都不是final的所以没有办法被注解所使用,这个时候Jake大神通过Hook了R文件生成的Task,然后copy了一份重新生成了一份R2.

android.applicationVariants.all { variant ->
     variant.outputs.all { output ->
       variant.outputs.all { output ->
      // Though there might be multiple outputs, their R files are all the same. Thus, we only
      // need to configure the task once with the R.java input and action.
      if (once.compareAndSet(false, true)) {
        val processResources = output.processResourcesProvider.get() // TODO lazy

        // TODO: switch to better API once exists in AGP (https://issuetracker.google.com/118668005)
        val rFile =
            project.files(
                when (processResources) {
                  is GenerateLibraryRFileTask -> processResources.textSymbolOutputFile
                  is LinkApplicationAndroidResourcesTask -> processResources.textSymbolOutputFile
                  else -> throw RuntimeException(
                      "Minimum supported Android Gradle Plugin is 3.3.0")
                })
                .builtBy(processResources)
        val generate = project.tasks.create("generate${variant.name.capitalize()}R2", R2Generator::class.java) {
          it.outputDir = outputDir
          it.rFile = rFile
          it.packageName = rPackage
          it.className = "R2"
        }
        variant.registerJavaGeneratingTask(generate, outputDir)
      }
    }
}
复制代码

上面的代码,就是在AndroidPlugin中可以注册一个registerJavaGeneratingTask的Task。这个Task可以在编译阶段生成一些我们所需要的java类,而这个阶段和Transform不一样,因为没有进入JavaCompiler环节,所以我们可以通过javapoet去生成java类,而且在我们实际编码的环节中是可以引用到这个类的。

因为Plugin的Extension是知道当前的项目需要使用几个厂商推送的,这样我们就可以通过生成代码的方式直接生成好策略类(以前这个策略类是要由接入方自己实现的),能让代码解决的问题就尽量不要让开发来写。

上面就是我生成的一个简单的java类,对于接入方来说做的越少那么就越不容易出问题,也就是架构上所说的高内聚。javapoet还有kotlinpoet这两个都可以展开一篇文章了,这边就不过分展开了啊。

如何将aar变更成源码依赖

前文解决了Plugin调试困难的问题,但是文章还有最后一个小问题,因为在Plugin是提供给别的App使用,所以直接使用了maven依赖。但是在Demo开发阶段源码的编译方式会更适合我开发,所以如何将一个group+name+version更换成一个本地的Module呢??

以前介绍过git-repo这个项目就是通过setting的方式给当将别的仓库引入到当前工程,然后通过configurationsresolutionStrategy所谓的解决策略来帮助项目解决多仓库依赖的问题。

那么我们能不能把这段逻辑偷过来呢,哈哈哈。

val splite = pair.first.split(":")
val substitute = "${splite[0]}:${splite[1]}"
project.subprojects.forEach { it ->
    it.configurations.all { config ->
      config.resolutionStrategy {
            it.dependencySubstitution.substitute(
                  it.dependencySubstitution.module(
                      substitute
                  )
                  ).because("debug enable").with(it.dependencySubstitution.project(pair.second))
                        it.dependencySubstitution.project(pair.second)
        }
    }
}
复制代码

首先在策略中,我们更换了dependencySubstitution,其中substitute就是group+name,然后pair.second则是其映射到本地的project。所以这段逻辑就是把group+name对应的取出来,然后替换成本地的仓库映射,而resolutionStrategy会更换项目内所有的group更换,这样我们就完成了项目内的本地映射了哦。

TODO

我还是有个地方想做的,由于当前推送为了保证最少的依赖,所以就连OKHttp都没有直接引用,其实可以在Plugin内根据当前Projcet的dependencies中是否含有一些第三方库,然后根据这个来引入其中的一部分类似Retrofit一样的Adapter的方式来引入依赖,就可以更好的提高当前仓库的能力。

总结

来了大B之后还是做了点好玩的东西的。关于行业内劝退安卓我个人看法哦,虽然现在客户端的职位可能会相对以前少了很多,但是并不代表着客户端的技术栈很浅啊。安卓可以玩的东西其实有很多啊,Aop, Apt,Apm性能监控,调试相关,编译优化,CI/CD,静态检查,网络优化,模块化,gradle相关,DSL等等。

对于一个技术来说,最重要的其实就是在自己的领域做得非常深入,只要能做到所谓的不可替代性,所以也没有什么客户端劝退一说吧,和各位大佬共勉吧。