Android编译优化之kapt优化
The following article is from 搜狐技术产品 Author 狐友陈金凤
随着项目功能越来越多,项目的编译耗时也在不断增加,即使在高配电脑上也需要近7分钟,如下图所示:
这样的编译速度,严重影响了我们的开发效率,需要我们及时解决。
在我们项目中,打开守护进程,并行编译和增量编译等配置,进行增量编译,获取编译报告。
# 在 gradle.properties 中添加
# 开启gradle daemon,
org.gradle.daemon=true
# 配置gradle daemon 编译内存,
org.gradle.jvmargs=-Xmx8192m -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# 配置kotlin daemon 编译内存,
kotlin.daemon.jvmargs=-Xmx8192m -Xms=500m
# 开启kotlin的增量
kotlin.incremental=true
kotlin.incremental.java=true
kotlin.incremental.js=true
# 开启kotlin并行编译
kotlin.parallel.tasks.in.project=true
# 开启kotlin的编译缓存
kotlin.caching.enabled=true
# 开启Kapt的增量(增量编译 kapt1.3.30版本以上支持)
kapt.incremental.apt=true
从Build Analyzer报告中,我们可以看出编译耗时并没有没减少太多,似乎增量编译没起到作用。虽然只修改了一个文件,但是Kotlin编译耗时3分多钟,KAPT耗时1分多钟,约占了整体编译耗时的 82%,如下图所示:
不论是run编译,还是apply change编译,还是通过gradle面板的build→assembleflavorDev Task编译,耗时都约6~7分钟,耗时分布也非常相似,大致统计如下:
CompileFlavorsDevDebugKotlin (65%)
KaptGenerateStubsFlavorsDevDebugKotlin (15%)
KaptFlavorsDevDebugKotlin (2%)
DexBuilder (1%)
CompileFlavorsDevDebugJavaWithJavac (1%)
从编译log中,我们发现一个警告:注解处理器hy-apt-compiler 不支持增量,会影响编译速度。
具体log如下:
> Task :app:compileFlavorsDevDebugJavaWithJavac
The following annotation processors are not incremental: hy-apt-compiler-1.1.0.jar (com.sohu.hy:hy-apt-compiler:1.1.0).
Make sure all annotation processors are incremental to improve your build speed.
> Task :photoedit_module:compileFlavorsTest_arm64ReleaseJavaWithJavac
The following annotation processors are not incremental: hy-apt-compiler-1.1.0.jar (com.sohu.hy:hy-apt-compiler:1.1.0).
Make sure all annotation processors are incremental to improve your build speed.
针对此问题,我们先通过Task开关,临时关闭KAPT task,避免KAPT每次都重新生成注解代码:
//在app和module的build.gradle.kts中加入
tasks.whenTaskAdded {
if (this.name.contains("kaptGenerateStubs"+currentflavortype+"Kotlin") || this.name.contains("kapt"+currentflavortype+"Kotlin")) {
// 对应任务kaptGenerateStubsFlavorsDevDebugKotlin,kaptFlavorsDevDebugKotlin
this.enabled = false
}
}
重新编译发现,不但2个KAPT任务耗时没有了,Kotlin编译耗时也只需要21秒了,很明显,此时增量编译起效了!
因此而知,在我们的项目里,KAPT任务不但耗时长,还影响了项目的增量编译,我们必须对它进行优化处理。
那么KAPT到底是什么呢?在优化处理之前,我们先来了解清楚什么是KAPT注解处理器。2注解处理器介绍注解处理器(Annotation Processor)是一种在编译时扫描和处理注解的工具,它可以自动化生成代码、检查代码的正确性、生成文档等。在Android中使用过四种注解处理器:
Android-apt
在Android平台,最先支持注解生成代码的库,是由个人开发者提供的一个插件,即android-apt。它只支持javac的方式,使用时需要引入插件 'com.neenbedankt.gradle.plugins:android-apt:1.8' 。
APT
谷歌在Gradle插件中引入annotationProcessor插件,简称APT(Annotation Processor Tool),用来简单快速地替换android-apt插件。APT可以同时支持Javac 和Jack的方式编译,功能与android-apt相同,但可以直接使用,而不需要额外引入插件。
KAPT
Kotlin引入到Android开发后,为了支持Kotlin文件的注解处理,引入了KAPT (Kotlin Annotation Processor Tool)。KAPT不仅支持Java文件,还支持 Kotlin文件,使用时需要引入插件'kotlin-kapt'。
KSP
KSP,即Kotlin符号处理器(Kotlin Symbol Processing ), 是Google基于Kotlin编译器提供的符号处理工具,也是Google解决KAPT编译缓慢问题的最新方案,它比KAPT有更快的速度。KSP不仅支持Kotlin 文件,还支持Java文件,使用时需要引入插件'com.google.devtools.ksp'。
当我们的Kotlin项目用了Dagger,ButterKnife, Glide ,Room,Builder等框架的KAPT插件时,会发现编译速度变得极其缓慢,正如上文项目编译速度分析所示,仅KAPT任务就耗时1分13秒,不但影响编译速度,还影响了增量编译效果。我们项目升级使用Kotlin时的痛点之一就是编译速度过慢。
那么为什么KAPT会比APT慢,而KSP又是怎么解决KAPT慢的问题呢?
我们将四种注解方案进行对比分析,如下表所示:
注解器 | 输入文件 | 输出文件 | 引入插件 | 编译效率 | 编译流程 |
---|---|---|---|---|---|
android-apt | Java | Java | com.neenbedankt.gradle.plugins:android-apt:1.8 | 中 | Java注解->生成所有Java文件的AST->调用android-apt注解处理器->生成Java代码 |
APT | Java | Java | 不需要 | 高 | Java注解->生成所有Java文件的AST->调用APT注解处理器->生成Java代码 |
KAPT | Kotlin,Java | Java | kotlin-kapt | 低 | Kotlin注解->生成JavaStub文件->生成所有Java文件的AST->调用APT注解处理器->生成Java代码 |
KSP | Kotlin,Java | Kotlin(推荐), | com.google.devtools.ksp | 高 | Java/Kotlin注解->生成所有Kotlin和Java文件的AST->调用KSP注解处理器->生成Kotlin源码 |
由表可知,KAPT还是基于APT注解处理器实现的一个支持Kotlin文件处理的插件,它只是对Kotlin文件做了一个预处理,即把Kotlin文件生成APT可解析的Java Stub文件。而KAPT生成Java stub文件需要耗费大量的时间,拖慢了整体编译速度。
KSP的开发者显然意识到了这个问题,它基于Kotlin Compiler Plugin实现,在Kotlinc的编译时,同步处理注解。它不再生成Java stub文件,而是直接将Kotlin文件像Java文件一样,直接生成注解处理器可以直接识别的抽象语法树(Abstract Syntax Tree,简称 AST),提供给KSP注解处理器。3KAPT 插件使用情况了解到KAPT的缺点后,我们先梳理出项目的KAPT插件使用情况,如下表所示:
KAPT插件 | 介绍 | KAPT增量 | KSP | KSP增量 |
---|---|---|---|---|
Room | Google, | 支持 | 支持 | 支持 |
Lifecycle | Google, | 支持 | 不支持 | 不支持 |
Glide | bumptech个人, | 支持 | 支持 | 支持 |
ButterKnife | JakeWharton个人, | 支持 | 不支持 | 不支持 |
apt-compiler | 自研, | 不支持 | 不支持 | 不支持 |
Google官方目前支持KSP的库列表,我们可以在官网页中查看:https://kotlinlang.org/docs/ksp-overview.html#supported-libraries
要解决上文中KAPT导致编译速度慢的问题,我们研究了下面三种解决方案:
1.支持KAPT的增量;
2.升级到KSP;
3.迁移到Android Studio插件。
接下来,我们对三种方案进行实践和效果分析。4KAPT 支持增量从上面梳理出的KAPT插件使用情况表,我们可以看出,所有三方KAPT插件都支持增量编译,不需要进行额外处理。自研的apt-compiler不支持KAPT增量,需要进行修改。
4.1增量模式
从Gradle 4.7开始,Java的增量编译就支持增量注解处理。Gradle增量注解处理器分为两种isolating,aggregating:
1.Isolating隔离模式:是最快的一种增量注解处理器,但它要求每一个注解处理器仅能使用一个注解去生成新的文件;
2.Aggregating总汇模式:增量成功率和效率比不上isolating,但支持使用多个注解组合生成一个文件。这两种增量模式都有一些使用限制:
注解的保留时效只能是CLASS或RUNTIME;
如果用户传递-parameters编译参数,它们只能读取参数名称;
只能使用Filer API生成新的文件;
不能依赖于特定编译器API;
如果使用Filer#createResource这个 API 生成新的资源文件,location参数只能是CLASS_OUTPUT, SOURCE_OUTPUT, NATIVE_HEADER_OUTPUT。
隔离模式除了上述限制,还有一些限制:
每一个注解处理器只能使用一个注解类型去生成新的文件;
每个用filer生成的文件,必须要在构造时传入一个originating element与其对应。不同模式的具体说明,可以参考官网:https://docs.gradle.org/current/userguide/java_plugin.html#sec:incremental_annotation_processing
4.2增量开发
我们在resources/META-INF/gradle/incremental.annotation.processors下进行增量处理器模式声明:
// 格式要求:注解处理器的全限定名,类别
// 可以包含多个,不同处理器可以处理不同的注解
com.sohu.hy.JavaLauncherProcessor,aggregating
com.sohu.hy.JavaLauncherProcessor2,isolating
com.sohu.hy.JavaLauncherProcessor3,dynamic
模式声明除了上述两种增量模式,还可以选dynamic动态模式,即用户自己编译时根据需求在代码中动态决定是否开启增量注解处理,及开启aggregating模式还是isolating模式。
如果声明为isolating模式,需要filer新建一个输出文件时,传入一个对应的originating element参数来指定依赖的源部件,参考代码如下:
public class JavaLauncherProcessor2 extends AbstractProcessor {
// 省略......
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
logger = new Logger(processingEnv.getMessager());
filer = processingEnv.getFiler();
types = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
Set<? extends Element> set3 = roundEnv.getElementsAnnotatedWith(Launcher.class);
StringBuilder builder = new StringBuilder().append("package ").append(Constants.PACKAGE_NAME).append(";").append("\n");
// 省略......
for (Element element : set3) {
try {
// 指定输出文件依赖的源部件element
JavaFileObject source = filer.createSourceFile(newClassName, element);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
}
}
如果声明为dynamic模式,需要在build.gradle中进行模式配置,并重写AbstractProcessor.getSupportedOptions函数。函数中,我们可以根据gradle配置和注解处理器实际支持的模式返回相应的模式,参考代码如下:
// 在app的build.gradle.kts 中加入
kapt {
//省略......
arguments {
//将这个选项传递到注解处理器中
arg("enableIncremental", "true")
}
}
// 在apt-compiler中的JavaLauncherProcessor3.java中加入
public class JavaLauncherProcessor3 extends AbstractProcessor {
private String enableIncremental = "false";
@Override
public synchronized void init(ProcessingEnvironment processor) {
super.init(processor);
//省略......
// 通过processor获取gradle中的配置
enableIncremental = processor.options["enableIncremental"];
}
@Override
public Set<String> getSupportedOptions() {
if ("true" == enableIncremental) {
// 需要根据 JavaLauncherProcessor3 处理注解器的情况来选择增量注解处理的模式
// Collections.singleton("org.gradle.annotation.processing.isolating");
Collections.singleton("org.gradle.annotation.processing.aggregating");
} else {
Collections.singleton("");
}
}
}
自研的apt-compiler注解处理器需要支持@Launcher,@LauncherCallback,@LauncherField等多个注解,所有只能用aggregating模式,不需要增加额外的代码支持。
我们实现了注解处理器的增量支持后,同时还需要在gradle.properties中开启KAPT的增量编译开关,具体如下:
// 在 gradle.properties 中加入
# 开启Kapt incremental(增量编译 kapt 1.3.30版本以上支持)
kapt.incremental.apt=true
# 开启kapt avoiding(编译规避 Kotlin 1.3.20 及以上支持)。
# 如果用kapt依赖的内容没有变化,会完全重用编译内容,省掉 :app:kaptGenerateStubsDebugKotlin 的时间。
kapt.include.compile.classpath=false
具体配置说明,可以参考官网:https://kotlinlang.org/docs/kapt.html
4.3增量效果
我们重新编译发现,注解处理器hy-apt-compiler不支持增量的警告log没有了,增量编译时KAPT任务耗时也明显下降很多,如下图所示,KAPT任务耗时从1分多钟减少到28秒,compileKotlin任务耗时从3分多钟减少到了8秒,增量编译效果明显,如下图所示:
从上面梳理出的KAPT插件使用情况表,我们可以看出,有些三方注解插件支持KSP和KSP增量,有些三方注解插件不支持KSP。而自研的apt-compiler插件也不支持KSP。
对于不支持的KSP的三方注解插件Lifecycle,我们发现其注解处理器只处理与生命周期相关的注释,如@OnLifecycleEvent,它会把普通方法绑定到对应的生命周期回调方法上。
但是在@OnLifecycleEvent的声明中,其注释说Lifecycle注解插件已经被官方废弃了,官方认为这个注解需要依赖代码生成和反射,运行比较耗时,推荐开发者用DefaultLifecycleObserver或LifecycleEventObserver来实现相同功能。
并且我们梳理项目代码后,发现代码中并没有用到Lifecycle相关注解,所以我们直接不再引入这个KAPT插件。这样即使它不支持KSP,也不影响我们把项目从KAPT迁移到KSP。
对于不支持的KSP的三方注解插件ButterKnife,其功能逻辑不复杂,我们也可以通过自研实现其相关功能,具体办法可以参考后续“自研插件KSP迁移”部分的说明。
下面我们开始对支持KSP的三方注解插件进行迁移。
5.1三方插件迁移
KSP插件的第一个正式版是“1.5.30-1.0.0”,它的版本号分2部分,一部分是插件对应的Kotlin版本“1.5.30”,一个是KSP插件自身的版本“1.0.0”。
KSP的版本依赖Kotlin的版本,而且同样的KSP版本,适配不同的Kotlin版本,也会有不同的最终版本号,比如:“1.9.21-1.0.15”,“1.9.21-1.0.16”,“1.9.22-1.0.16”。具体版本对应关系,可以参考KSP releases说明:https://github.com/google/ksp/releases
首先,我们项目需要升级kotlin版本,并且指定对应Kotlin版本对应的KSP版本:
// buildSrc/build.gradle.kts中添加
dependencies{
val version_koltin="1.8.20" //指定kotlin版本
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${version_koltin}")
}
// 在setting.gradle.kts中添加
pluginManagement {
plugins {
val version_ksp = "1.8.20-1.0.11" //指定ksp的版本
id("com.google.devtools.ksp") version version_ksp apply false
}
}
注意:升级完Kotlin版本,可能需要对应升级Gradle版本和Android Studio版本。
接着,我们在对应模块中引入KSP插件,并在依赖中使用KSP,配置如下:
// 模块下的build.gradle中,替换掉 kapt plugin
plugins {
//id("kotlin-kapt")
id("com.google.devtools.ksp")
}
// 模块下的build.gradle中,替换掉
dependencies {
//kapt("androidx.room:room-compiler:2.4.2")
ksp("androidx.room:room-compiler:2.4.2")
//kapt("com.github.bumptech.glide:compiler:4.14.2")
ksp("com.github.bumptech.glide:ksp:4.14.2")//apt和ksp不在同一个包里
}
上面几个简单步骤就完成了三方注解处理器的KSP迁移,成本非常低。但对自研的KAPT注解插件进行KSP迁移,会有一定的开发成本。5.2自研插件迁移
与KAPT不同,KSP注解处理器不会以Java的方式看待输入程序,其API对Kotlin来说更加自然,尤其是对于Kotlin专有功能,如顶层函数。由于KSP不会象KAPT那样将处理代理给javac,因此它不会依赖于JVM,也可以用于其它平台。
我们发现同一个插件可以同时支持KAPT和KSP,比如Room就是如此。所以我们的自研注解插件apt-compiler也会在原KAPT项目里,直接进行KSP支持。
Apt-compiler的功能是根据注解自动生成Builder模式的Activity Launcher启动类,具体使用如下代码所示:
//不使用Laucher的方式,启动MemberSearchActivity需要定义常量和获取参数
public class MemberSearchActivity extends BaseSearchActivity {
public static final String CIRCLE_ID = "circle_id";
public static final String CIRCLE_NAME = "circle_name";
public static final String CIRCLE_ADMIN = "circle_admin";
public static final String CIRCLE_MASTER = "circle_master";
//省略......
public String circleId = "";
public String circleName = "";
public int totalCount = 0;
public String admin = "";
public String master = "";
public int sourcePage = 0
public ArrayList<UserBean> userBeans = new ArrayList<UserBean>();
//省略......
@Override
protected void initView() {
//省略......
mCircleId = intent.getStringExtra(CIRCLE_ID)
sourcePage = intent.getIntExtra(SOURCE_PAGE)
userBeans = (ArrayList<UserBean>)intent.getSerializableExtra(USERBEANS)
}
//省略......
}
//不使用Laucher的方式,启动MemberSearchActivity需要给intent一一对应传参
public static void toMemberSearchActivity(Context context, CircleBean circleBean, int totalCount, int sourcePage, ArrayList<UserBean> userBeans) {
Intent intent = new Intent(context, MemberSearchActivity.class);
intent.putExtra(MemberSearchActivity.CIRCLE_ID, circleBean.getCircleId());
intent.putExtra(MemberSearchActivity.CIRCLE_NAME, circleBean.getCircleName());
intent.putExtra(MemberSearchActivity.CIRCLE_ADMIN, circleBean.getAdmin());
intent.putExtra(MemberSearchActivity.CIRCLE_MASTER, circleBean.getMaster())
intent.putExtra(MemberSearchActivity.TOTALCOUNT, totalCount);
intent.putExtra(MemberSearchActivity.SOURCE_PAGE, sourcePage);
intent.putExtra(MemberSearchActivity.USERBEANS, userBeans);
if (context != null ) {
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
}
}
//使用Laucher的方式,启动MemberSearchActivity不用定义常量,bind统一获取参数
@LauncherCallback(data = UserDataBean.class)
public class MemberSearchActivity extends BaseSearchActivity {
@LauncherField
public String circleId = "";
@LauncherField
public String circleName = "";
@LauncherField
public int totalCount = 0;
@LauncherField
public String admin = "";
@LauncherField
public String master = "";
@LauncherField(required = false)
public int sourcePage = 0
@LauncherField(required = false)
public ArrayList<UserBean> userBeans = new ArrayList<UserBean>();
//省略......
@Override
protected void initView() {
//省略......
LauncherService.bind(this) //也可以统一放到BaseActivity统一调用,页面不需要单独处理
}
//省略......
}
//使用Laucher的方式, 启动MemberSearchActivity通过builder模式设置参数,更清晰安全
public static void toMemberSearchActivity(Context context, CircleBean circleBean, int totalCount, int sourcePage, ArrayList<UserBean> userBeans) {
MemberSearchActivity.Builder().setCircleId(circleBean.getCircleId())
.setCircleName(circleBean.getCircleName())
.setAdmin(circleBean.getAdmin())
.setMaster(circleBean.getMaster())
.setTotalCount(totalCount)
.setSourcePage(sourcePage)
.setUserBeans(userBeans)
.lunch(mContext)
}
可以看到,使用Launcher启动Activity,减少了常量定义和参数获取的模板代码,还通过Builder模式的使得传参更简洁和安全。
首先,我们为apt-compiler插件项目添加KSP依赖:
plugins {
id("java-library") //插件模块属于java项目
kotlin("jvm") //Android中ksp注解开发需引入kotlin jvm插件
}
dependencies {
implementation(LocalRepository.aptannotationSdk)
val version_ksp = "1.8.20-1.0.11" //指定ksp的版本
val version_ksp_kotlinpoet="1.15.3" //指定kotlinpoet版本
implementation("com.google.devtools.ksp:symbol-processing-api:${version_ksp}")
implementation("com.squareup:kotlinpoet-ksp:${version_ksp_kotlinpoet}")
}
其中KotlinPoet是JavaPoet对应的Kotlin版本, 也是Square开发的开源库,用于帮助生成Kotlin代码文件,非常适合在注释处理器开发时使用。如果不使用Poet,我们需要手动拼接代码,非常繁琐易错,而Poet生成代码不但可读性好,而且更加简单安全。
和APT类似,在项目中新建文件:项目根目录\src\main\resources\META-INF\services\com.google.devtools.ksp.processing.SymbolProcessorProvider,并在文件中声明注解入口:
com.sohu.hy.LauncherProcessorProvider
一个KSP插件一般由SymbolProcessorProvider,和 SymbolProcessor组成:
SymbolProcessorProvider:环境提供者,主要为符号处理器SymbolProcessor提供环境,参数 SymbolProcessorEnvironment包含了编译后符号相关的各种信息;
/**
* [SymbolProcessorProvider] is the interface used by plugins to integrate into Kotlin Symbol Processing.
*/
fun interface SymbolProcessorProvider {
/**
* Called by Kotlin Symbol Processing to create the processor.
*/
fun create(environment: SymbolProcessorEnvironment): SymbolProcessor
}
SymbolProcessor:真正处理“符号”的符号处理器,其关键方法是process,此方法提供的Resolver参数,包含大量的符号处理方法。
/**
* [SymbolProcessor] is the interface used by plugins to integrate into Kotlin Symbol Processing.
* SymbolProcessor supports multiple rounds of execution, a processor may return a list of deferred symbols at the end
* of every round, which will be passed to processors again in the next round, together with the newly generated symbols.
* On exceptions, KSP will try to distinguish between exceptions from KSP and exceptions from processors.
* Exceptions from processors will immediately terminate processing and be logged as an error in KSPLogger.
* Exceptions from KSP should be reported to KSP developers for further investigation.
* At the end of the round where exceptions or errors happened, all processors will invoke onError() function to do
* their own error handling.
*/
interface SymbolProcessor {
/**
* Called by Kotlin Symbol Processing to run the processing task.
*
* @param resolver provides [SymbolProcessor] with access to compiler details such as Symbols.
* @return A list of deferred symbols that the processor can't process. Only symbols that can't be processed at this round should be returned. Symbols in compiled code (libraries) are always valid and are ignored if returned in the deferral list.
*/
fun process(resolver: Resolver): List<KSAnnotated>
/**
* Called by Kotlin Symbol Processing to finalize the processing of a compilation.
*/
fun finish() {}
/**
* Called by Kotlin Symbol Processing to handle errors after a round of processing.
*/
fun onError() {}
}
自研注解插件的LauncherProcessorProvider继承自 SymbolProcessorProvider,负责返回SymbolProcessor的实现类LauncherProcessor,代码参考如下:
class LauncherProcessorProvider: SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
// KSP 暂时无法像 KAPT 一样单步联调,
// 日志打印无法使用println, 需使用: environment.logger.warn("log")
// 注意:使用warn才能在build面板里看到ksp的日志,而用info就看不到日志!!!
environment.logger.warn("cjfaaa---LauncherProcessorProvider")
return LauncherProcessor(environment.codeGenerator, environment.logger, environment.options)
}
}
在LauncherProcessor的process方法里,可以通过其Resolver参数的访问者模式获取文件的AST信息,代码参考如下:
class LauncherProcessor(val codeGenerator: CodeGenerator,val logger: KSPLogger,val options: Map<String, String>) : SymbolProcessor {
// 注解符号的核心处理方法
override fun process(resolver: Resolver): List<KSAnnotated> {
logger.warn("cjfaaa---LauncherProcessor")
//打印gradle中配置的ksp参数"{ksp.enablelog=true}",配置如下:
//ksp {
// arg("ksp.enablelog", "true")
//}
logger.warn("cjfaaa--- ${options.toString()}")
//获取待处理的符号集
val symbols_LauncherCallback = resolver.getSymbolsWithAnnotation(LauncherCallback::class.qualifiedName!!)
symbols_LauncherCallback.filterIsInstance<KSClassDeclaration>()
.filter { it.validate() }
.forEach {
// 通过访问者模式获取AST中LauncherCallback注解对应的类信息,
// 信息通过回调visitClassDeclaration通知, 我们解释后存入LauncherData里。
it.accept(LauncherVisitor(), LauncherData())
}
val symbols_Launcher = resolver.getSymbolsWithAnnotation(Launcher::class.qualifiedName!!)
//省略......
// 返回list集合,包含此轮处理暂不处理的符号,会在下一轮处理中,和新的待处理的符号一起回调处理
val ret = symbols_Launcher.filter { !it.validate() }.toMutableList()
ret.addAll(symbols_LauncherCallback.filter { !it.validate() })
return ret
}
}
//访问者模式,访问对应源文件的KSClassDeclaration对象
class LauncherVisitor : KSDefaultVisitor<LauncherData, Unit>() {
// 从KSClassDeclaration中,收集相关信息到LauncherData数据结构中,后续用来生成Activity Launcher。
override
fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: LauncherData) {
data.launcherClass = classDeclaration.toClassName()
//省略......
classDeclaration.annotations.filter {itAnnotation->
itAnnotation.annotationType.toString().equals(Launcher::class.simpleName) || itAnnotation.annotationType.toString().equals(LauncherCallback::class.simpleName)
}.forEach {
if (it.annotationType.toString().equals(LauncherCallback::class.simpleName)){
data.isCallback = true
}
it.arguments.forEach{
//获取回调数据的相关数据结构
if (it.name?.getShortName().equals("data")) {
data.callback_data = (it.value as KSType).toTypeName()
}
//省略......
}
}
classDeclaration.getAllProperties().forEach {itProper->
itProper.annotations.filter {
it.annotationType.toString().equals(LauncherField::class.simpleName)
}.forEach {
//获取需要绑定变量的名称和类型
fieldData = LauncherFieldData()
fieldData.fieldName = itProper.toString()!!
fieldData.fieldType = itProper.type.toTypeName()
it.arguments.forEach{
if (it.name?.getShortName().equals("required")) {
fieldData!!.required = it.value as Boolean
} else if (it.name?.getShortName().equals("key")) {
fieldData!!.key = it.value as String
}else if (it.name?.getShortName().equals("isBig")) {
fieldData!!.isStatic = it.value as Boolean
}
}
data.propertys.add(fieldName!! to fieldData!!)
}
//省略......
generateLauncherCode(data, classDeclaration.containingFile!!)
}
}
// 根据收集的LauncherData,生成对应的Activity Launcher类
private fun generateLauncherCode(data: LauncherData, containingFile: KSFile) {
val classNameOrigin = data.launcherClass
val classNameNewLauncher = ClassName(classNameOrigin!!.packageName, "${classNameOrigin!!.simpleName}Launcher")
//省略......
//创建Launcher类,使用Poet相关接口生成。
val launcherObject = TypeSpec.objectBuilder(classNameNewLauncher)
.addProperties(staticParamList)
.addFunction(bindFunSpec.build())
.addType(builderClass.build())
if (data.isCallback) {
launcherObject.addType(callBackInterface)
}
//创建Launcher文件
val fileSpec = FileSpec.builder(classNameNewLauncher)
.addImport(ClassName("android","app"),"Activity")
.addImport(ClassName("android","content"),"Intent")
.addType(launcherObject.build())
.build()
//Dependencie中指定生成文件是否aggregating模式,
//根据ksp中增量模式的说明,我们属于isolate模式,所以传false。这和KAPT不同,需注意。
fileSpec.writeTo(codeGenerator, Dependencies(false, containingFile))
}
}
KSP注解插件的AST信息获取的类和APT不一样,我们需要一一转换过来,具体转换列表可以参考KSP官网。其次,我们要用KotlinPoet来生成Launcher的kt文件,其用法和JavaPoet 类似。我们的app模块只需要参照上节“三方插件迁移”的方式在build.gradle.kt文件中使用ksp替换kapt就完成了自研插件的迁移。
在生成的Launcher kt文件时,我们遇到了一些需要特别注意处理的地方,如下:
在处理输入文件的类型信息时,所有Java文件中的基本类型,都会变成Kotlin版,如int变成kotlin.Int。但Java文件中的非基本类型不会变,如:java.util.ArrayList,没有变成kotlin.collections.ArrayList;
Kotlin语言里没有静态变量和方法,只有companion object伴生类和objec类。如果需要Java调用保持和APT时一致需加@JvmStatic;
Launcher是通过Intent将变量传输给目标Activity的,所以必须是支持序列化的类型,如ArrayList。我们常用的Map类型是不支持序列化的。Launcher可以通过静态变量传输非序列化的变量;
Kotlin支持可空类型,我们可以巧用Poet的KSType扩展函数toClassName将类型转换后,通过其函数 copy(nullable = true)转换为可空类型;
通过ClassName引入的类,不需要单独引入import。通过String引入的类,需要单独引入import。
5.3增量支持
Kotlin的增量编译也支持KSP的注解增量处理。KSP的增量注解处理器也分为两种isolating,aggregating,但是它和KAPT有很大区别:
1.Isolating 隔离模式:是最快的一种增量注解处理器。隔离模式下注解输出只依赖于特定的源代码, 其他源代码的变更不会影响其输出;
2.Aggregating聚合模式:增量成功率和效率都比不上isolating。聚合模式下注解输出可能依赖其它源代码,任何输入更改都会导致其输出重建。KSP的增量支持,不需要像KAPT那样新增一个文件来声明增量模式,它可以直接在代码中控制增量模式,所以KSP没有动态模式的概念。在KAPT中,我们的注解插件因为使用了2个以上的注解,所以属于aggregating模式,而在KSP中,因为生成的Launcher文件只会依赖一个源文件,所有属于isolating模式,其设置代码参考如下:
//containingFile, 是从visitClassDeclaration回调参数中获取的源文件路径,即classDeclaration.containingFile!!
//根据ksp中增量模式的说明,我们属于isolate模式,所以传false。这和KAPT不同,需注意。
fileSpec.writeTo(codeGenerator, Dependencies(false, containingFile))
设置模式主要通过Dependencies类,其第一个参数aggregating表示是否为聚合模式:
Aggregating = false :处理器确定它的信息只来自特定的输入文件, 不会来自其它文件或新的文件;
Aggregating = true :一个输出可能潜在的依赖于新的信息, 可能来自新的文件, 或者既有的但被变更的文件。其第二个参数表示输出依赖的源文件列表,可以依赖一个或多个源文件,如果是aggregating模式则可以依赖任何一个源文件变更,则不需要依赖源文件列表,如下代码所示:
// 如果依赖多个指定源文件,也属于isolating模式
fileSpec.writeTo(codeGenerator, Dependencies(false, containingFile1,containingFile2,containingFile3))
// 不依赖指定源文件,属于aggregating模式
fileSpec.writeTo(codeGenerator, Dependencies(true))
KSP的增量编译是默认打开的,如果我们开发时需要关闭增量编译,可以在gradle中配置,代码如下:
//在 gradle.properties 中添加:
# Incremental processing is currently enabled by default.
# To disable it, set the Gradle property
ksp.incremental=false.
我们也可以打开增量编译log,查看增量编译的情况,只需要在gradle总配置,如下:
//在 gradle.properties 中添加:
# 打开增量log
# To enable logs that dump the dirty set according to dependencies and outputs。
# You can find these log files in the build output directory with a .log file extension.
ksp.incremental.log=true
修改FeedPreviewActivity.java文件后,重新编译,然后查看文件:项目根目录/app/build/kspCaches/flavorsDevDebug/logs/kspSourceToOutputs.log,可以发现KSP确实只重新处理了修改的 FeedPreviewActivity.java文件,log如下:
=== Build 1709807360615 ===
Accumulated source to outputs map
src/main/java/hy/sohu/com/app/search/schoolsearch/SchoolSearchActivity.kt:
build/generated/ksp/flavorsDevDebug/kotlin/hy/sohu/com/app/search/schoolsearch/SchoolSearchActivityLauncherCjfKsp.kt
src/hytest/java/hy/sohu/com/app/test/model/TestHyDatabase.java:
build/generated/ksp/flavorsDevDebug/java/hy/sohu/com/app/test/model/db/CommentDao_Impl.java
build/generated/ksp/flavorsDevDebug/java/hy/sohu/com/app/test/model/db/ProductDao_Impl.java
build/generated/ksp/flavorsDevDebug/java/hy/sohu/com/app/test/model/TestHyDatabase_Impl.java
//省略......
Reprocessed sources and their outputs
src/main/java/hy/sohu/com/app/ugc/preview/view/FeedPreviewActivity.java:
build/generated/ksp/flavorsDevDebug/kotlin/hy/sohu/com/app/ugc/preview/view/FeedPreviewActivityLauncher.kt
All reprocessed outputs
build/generated/ksp/flavorsDevDebug/kotlin/hy/sohu/com/app/ugc/preview/view/FeedPreviewActivityLauncher.kt
5.4限制
KSP作为注解处理的解决方案,对比其它解决方案,它存在几点限制:
不能获取到源代码的表达式级信息;
不能修改源文件代码;
不能百分百兼容APT的API;
当前的IDE对KSP生成的代码无法感知,必须手动为项目配置添加KSP生成路径。
5.5效果
完成KSP迁移后,我们重新编译发现,KSP的任务耗时只有7秒,比KAPT的任务耗时1分多钟要优秀很多,并且项目增量编译起效的成功率也大大提高了,效果如下图所示:
我们日常开发时,其实经常使用Android Studio插件来处理文件,提高我们的开发效率和质量,比如JsonToKotlinClass, ButterKnife,SonarLint等插件。那我们看看使用Android Studio插件来处理文件注解的效果如何。
Android Studio插件生成的文件,和我们自己新建的文件是一样的,可以直接调用,不需要像KAPT和KSP那样要先编译一次,动态生成文件后才能进行接口调用。而且Android Studio插件只会在创建时运行一次,不会影响编译速度,更不会影响增量效果。
接下来,我们继续实现一个Launcher的Android Studio插件。
6.1工具准备
因为Android Studio是基于IntelliJ为模版开发的,IDE插件必须通过IntelliJ IDEA开发,再安装到Android Studio中,我们的Android项目才能使用插件功能。具体说明,可见IDEA插件的官网:https://plugins.jetbrains.com/docs/intellij/plugins-quick-start.html
我们需要根据团队使用的Android Studio版本,下载对应的IDEA版本(历史版本下载地址:https://www.jetbrains.com/idea/download/other.html),以避免新版的IDEA包含的功能接口在我们的Android Studio上没有,导致插件无法使用。具体版本对应,可以参考下图:
程序结构接口(Program Structure Interface,简称PSI)是IntelliJ 平台中的一个抽象层,负责解析文件并创建语法和语义代码模型,为平台的众多功能提供支持。我们进行IDEA插件开发时,需要遵循PSI规范, 具体说明见官网:https://plugins.jetbrains.com/docs/intellij/psi.html
PSI文档看着比较复杂,我们可以使用界面工具PSIViewer插件来查看源代码文件的对应树形结构,如下图所示:
工具的详细介绍见官网:https://www.jetbrains.com/help/idea/psi-viewer.html
6.2插件开发
首先我们通过IDEA向导创建一个插件项目。因为插件项目的创建向导“Plugin DevKit”在2023.2之前的版本才是默认集成在IDEA中的,所有用新版本的同学,需要先安装一下“Plugin DevKit”这个插件。创建选项如下图所示:
创建完成后,我们的项目结构如图所示:
我们需要根据项目需要配置两个文件:
Build.gradle.kts,可以配置IntelliJ平台IDE的版本和依赖库:
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.8.21"
id("org.jetbrains.intellij") version "1.13.3"
}
group = "com.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
// Configure Gradle IntelliJ Plugin
// Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij {
//version和localPath属性不能同时使用!!!
version.set("2022.2.5") //运行插件的IntelliJ IDEA版本
//localPath.set("/Applications/Android Studio.app/Contents") //运行插件的Android Studio应用程序目录
type.set("IC") // Target IDE Platform
plugins.set(listOf(/* Plugin Dependencies */
"com.intellij.java",
"org.jetbrains.kotlin" )
)
}
tasks {
// Set the JVM compatibility versions
withType<JavaCompile> {
sourceCompatibility = "17"
targetCompatibility = "17"
}
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
patchPluginXml {
//sinceBuild = '222' ,表示插件支持IntelliJ IDEA的最低版本是2022.2。
//untilBuild = '232.*' ,表示插件支持IntelliJ IDEA的最高版本是2023.2。 * 通配符,表示后续IDEA新版本也可兼容
sinceBuild.set("222")
untilBuild.set("232.*")
}
signPlugin {
certificateChain.set(System.getenv("CERTIFICATE_CHAIN"))
privateKey.set(System.getenv("PRIVATE_KEY"))
password.set(System.getenv("PRIVATE_KEY_PASSWORD"))
}
publishPlugin {
token.set(System.getenv("PUBLISH_TOKEN"))
}
}
Plugin.xml,配置我们插件的各项属性,及核心功能入口注册actions:
<idea-plugin>
<id>com.cjf.gen.LaunchActivity</id>//插件的唯一标识符,应使用全限定名
<name>genLaunchActivity</name>//公共插件名称
<vendor email="support@yourcompany.com" url="https://www.yourcompany.com">YourCompany</vendor>
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>]]>
</description>//插件的描述,如果发布插件,将在插件介绍页面和 IDE 插件管理器中显示。
//定义插件所依赖的产品模块和插件。插件依赖于 IntelliJ IDEA 平台模块、Java 模块和 Kotlin 插件。
<depends>com.intellij.modules.platform</depends>
<extensions defaultExtensionNs="com.intellij">
</extensions>
<actions> //功能入口注册
//定义Launcher注解生成插件的动作(Action)
//其中 class 属性指定了动作的实现类,
//id 属性指定了动作的唯一标识符,
//text 属性指定了动作显示的文本,
//icon 属性指定了动作的图标路径
//description 属性指定了动作的描述
<action id="LaunchActivity_ID"
class="com.cjf.gen.LaunchActivityAction"
text="genLaunchActivity"
icon="AllIcons.Actions.Annotate"
description="LaunchActivity_Des">
<add-to-group group-id="GenerateGroup"/>
</action>
</actions>
</idea-plugin>
Aciton可以通过IDEA创建向导来添加,首先在项目目录下右键,在菜单中选择New->Plugin DevKit->Action,如下图所示:
然后在Action创建向导中,选择对应的Group和Action的位置,如下图所示:
向导会自动生成LaunchActivityAction.java文件,并在plugin.xml中添加genLaunchActivity action 的注册信息。
同步项目,如果出现“Plugin[id: 'org.jetbrains.kotlin.jvm', version: '1.8.21'] was not found in any of the following sources”的错误,可以配置适合的代理解决。
接着我们运行Gradle工具栏的runIde任务,这时会运行一个 IDEA 模拟器Main,用来运行我们的测试项目,如下图所示:
如果我们的插件只需要在Android上运行,可以配置Android Studio为启动路径,那样就会打开一个Android Studio模拟器来运行我们的测试项目。
向导生成的LaunchActivityAction类,继承自AnAction,我们需要重写actionPerformed方法,在方法中获取源码文件进行分析处理。
由于KAPT和KSP插件,是通过Gradle插件的方式,在Java和Kotlin编译过程执行,所以重写方法process时,能从参数中拿到所有注解信息及其上下文,所以能够很快的获取到类型信息,实现注解生成新文件。
但在IDEA插件,在重写方法actionPerformed时,只能从参数 event中拿到当前编辑文件的信息,没有项目所有文件上下文,所以获取类型信息会比较麻烦。
1.获取PSI结构
程序结构接口PSI同时支持Kotlin和Java代码文件,但支持的接口不同,所以需要针对两种源文件分别处理。从AnActionEvent获取到当前文件psiFile 和psiElement后,分别解释出Kotlin和Java代码文件对应的数据对象:KtClass和PsiClass,代码如下:
@Override
public void actionPerformed(AnActionEvent event) {
project = event.getData(PlatformDataKeys.PROJECT);
editor = event.getData(PlatformDataKeys.EDITOR);
psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (psiFile == null ) {
Utils.showInfoNotification(project, "LaunchActivity genarate fail ---1 !!!");
return;
}
Location location = Location.fromEditor(editor, project);
psiElement = psiFile.findElementAt(location.getStartOffset());
if (psiElement == null) {
Utils.showInfoNotification(project, "LaunchActivity genarate fail ---2 !!!");
return;
}
modulePath = psiFile.getVirtualFile().getPath();
//String srcPath = "/src/main/";
modulePath = modulePath.substring(0, modulePath.indexOf(srcPath));
KtClass ktClass = getPsiClassFromEvent(editor);
PsiClass clazz = getTargetClass(editor);
Boolean genResult = false;
if(ktClass!=null){
genResult = genFromKotlin(ktClass);
}else if(clazz!=null){
genResult = genFromJava(clazz);
} else { // just notify user about no element selected
Utils.showInfoNotification(project, "No injection was selected");
return;
}
if(genResult) {
Utils.showInfoNotification(project, "LaunchActivity genarate success :)");
return;
}
Utils.showInfoNotification(project, "LaunchActivity genarate fail !!!");
}
private KtClass getPsiClassFromEvent(Editor editor) {
if (!(psiFile instanceof KtFile))
return null;
return getKtClassForElement(psiElement);
}
public static KtClass getKtClassForElement(@NotNull PsiElement psiElement) {
if (psiElement instanceof KtLightElement) {
PsiElement origin = ((KtLightElement) psiElement).getKotlinOrigin();
if (origin != null) {
return getKtClassForElement(origin);
} else {
return null;
}
} else if (psiElement instanceof KtClass &&
!((KtClass) psiElement).isEnum() &&
!((KtClass) psiElement).isInterface() &&
!((KtClass) psiElement).isAnnotation() &&
!((KtClass) psiElement).isSealed()) {
return (KtClass) psiElement;
} else {
PsiElement parent = psiElement.getParent();
if (parent == null) {
return null;
} else {
return getKtClassForElement(parent);
}
}
}
private PsiClass getTargetClass (Editor editor) {
if (!(psiFile instanceof PsiJavaFile))
return null;
final PsiClass target = PsiTreeUtil.getParentOfType(psiElement, PsiClass.class);
return target instanceof SyntheticElement ? null : target;
}
2.收集Launcher信息。
根据KtClass和PsiClass对象收集Launcher/LauncherCallback源注解信息:classPath,hasCallback,dataClassPath,isList等,代码如下:
List<KtAnnotationEntry> annotations = ktClass.getAnnotationEntries();
for (KtAnnotationEntry annotation : annotations) {
final String referenceName = annotation.getShortName().getIdentifier();
if(referenceName.equals(Launcher.class.getSimpleName())) {
hasLauncher = true;
annotationLaunch = annotation;
} else if (referenceName.equals(LauncherCallback.class.getSimpleName())) {
hasLauncherCallback = true;
annotationLaunch = annotation;
}
}
if(!hasLauncher && !hasLauncherCallback) {
return false;
}
CallBackBundleInfo bean = new CallBackBundleInfo();
bean.bundleInfos = new ArrayList<>();
bean.classPath = ((KtFile) psiFile).getPackageName() + "." + ktClass.getName();
if (hasLauncherCallback) {
bean.hasCallback = true;
//获取 @LauncherCallback 注解的参数信息
List<KtValueArgument> attrs = (List<KtValueArgument>) annotationLaunch.getValueArguments();
List<String> methods = Arrays.stream(LauncherCallback.class.getMethods()).map(new Function<Method, String>() {
@Override
public String apply (Method t) {
return t.getName();
}
}).toList();
for (KtValueArgument attr:attrs) {
if(attr.getArgumentName().getAsName().getIdentifier().equals("data") && methods.contains("data")) {
KtExpression argumentExpression = attr.getArgumentExpression();
//得到的不是全路径类名
bean.dataClassPath = argumentExpression.getFirstChild().getText();
//参数类的类型,需要通过文件搜索的方式获取对应文件,进行分析,以便获得全路径名
PsiElement pe = getClassElementFromName(bean.dataClassPath);
if(pe instanceof KtClass) {
bean.dataClassPath = ((KtClass)pe).getFqName().toString();
} else if(pe instanceof PsiClass){//参数对应的类,也可能是Java类
bean.dataClassPath = ((PsiClass)pe).getQualifiedName();
}
}
if(attr.getArgumentName().getAsName().getIdentifier().equals("isList") && methods.contains("isList")) {
KtExpression argumentExpression = attr.getArgumentExpression();
bean.isList = Boolean.parseBoolean(argumentExpression.getText());
}
}
}
3.收集LauncherField信息。
根据KtClass和PsiClass,获取源注解类的LauncherField信息:fieldName,fieldTypeName,isStatic等,代码如下:
List<KtDeclaration> fields = ktClass.getDeclarations();
for (KtDeclaration field : fields) {
List<KtAnnotationEntry> fiedAnnotations = field.getAnnotationEntries();
for (KtAnnotationEntry annotation : fiedAnnotations) {
final String referenceName = annotation.getShortName().getIdentifier();
if (referenceName!=null && referenceName.equals(LauncherField.class.getSimpleName())){
//按照原来kapt的逻辑,只要有LauncherField,就不属于OnlyLauncher了!!!
bean.isOnlyLauncher = false;
BundleInfo info = new BundleInfo();
info.fieldName = field.getName().toString();
info.fieldMethodName = "set" + StringUtils.toUpperCaseFirstOne(info.fieldName);
List<KtValueArgument> attrs = (List<KtValueArgument>) annotation.getValueArguments();
List<String> methods = Arrays.stream(LauncherField.class.getMethods()).map(new Function<Method, String>() {
@Override
public String apply (Method t) {
return t.getName();
}
}).toList();
for (KtValueArgument attr:attrs) {
if(attr.getArgumentName().getAsName().getIdentifier().equals("required") && methods.contains("required")) {
info.isRequired = Boolean.parseBoolean(attr.getArgumentExpression().getText());
}
if(attr.getArgumentName().getAsName().getIdentifier().equals("isBig") && methods.contains("isBig")) {
info.isStatic = Boolean.parseBoolean(attr.getArgumentExpression().getText());
}
}
//kotlin中没有基础类型,如果使用基础类型定义变量,虽然ide中不提示错误,但编译会自动进行处理。
//但是在PSI结构中,会读不到正确类型,所以需要对注解的变量写法进行约束,如下写法会提示生成错误:
// var ccc:double = 0.0 #不能使用基础类型:int,double等。
// var ccc = 0.0 #没有类型声明,默认会被ide自动转换为Double类型了。但在PSI中会读不到类型信息。
KtTypeReference ktTypeReference = ((KtProperty)field).getTypeReference();
if(ktTypeReference == null) return false;
//fieldTypeName可能值:List<EditBean>, EditBean?,List<EditBean>?
info.fieldTypeName = ktTypeReference.getTypeElement().getText();
if( info.fieldTypeName.endsWith("?")){
//Java中不支持可空类型,我们去掉Kotlin中的 ?号
info.fieldTypeName = info.fieldTypeName.substring(0, info.fieldTypeName.length()-1);
}
String tFieldTypeName = info.fieldTypeName;
if(info.fieldTypeName.contains(">")) {
// 获取List<EditBean>? 中的泛型类 EditBean
tFieldTypeName = info.fieldTypeName.substring(info.fieldTypeName.indexOf("<")+1,info.fieldTypeName.indexOf(">"));
}
String tFieldClassPath = "";
PsiElement pe1 = getClassElementFromName(tFieldTypeName);
if(pe1 instanceof KtClass) {
tFieldClassPath = ((KtClass)pe1).getFqName().toString();
} else if(pe1 instanceof PsiClass) {
tFieldClassPath = ((PsiClass)pe1).getQualifiedName();
}
if( !StringUtils.isEmpty(tFieldClassPath) && info.fieldTypeName.contains(">") ) {
// List<EditBean> 中的类全路径 EditBean:List<com.cjf.test.EditBean>
info.fieldTypeName = info.fieldTypeName.replace(tFieldTypeName, tFieldClassPath);
pe1 = null;//后续typeExchange函数中,不能以list内部的类型作为 field 的类型
} else if(!StringUtils.isEmpty(tFieldClassPath)){
info.fieldTypeName = tFieldClassPath;
}
// kt类型装换为对应的java类型
if(info.fieldTypeName.equals(KT_INT)) {
info.fieldTypeName = INTEGER;
} else if (info.fieldTypeName.equals(KT_StringArrayList)) {
info.fieldTypeName = StringArrayList;
} else if (info.fieldTypeName.equals(KT_IntegerArrayList)) {
info.fieldTypeName = IntegerArrayList;
} else if (info.fieldTypeName.startsWith(KT_LIST)) {
info.fieldTypeName = info.fieldTypeName.replace(KT_LIST, "ArrayList");
}
System.out.println("cjf---" + "genFromKotlin fieldTypeName = " + info.fieldTypeName);
// in kotlin have no 8 Primitive types will box to BoxedType
// 在kotlin中没有8种基础类型,我们会转换成Java中对应的类型。
// 非基础类型,我们会对应转换为Intent Bundle支持传递的数据类型:StringArrayList,IntegerArrayList,Parcelable,Serializable
info.fieldType = TypeUtils2.typeExchange4Kotlin(ktTypeReference, pe1);
System.out.println("cjf---" + "genFromKotlin fieldType = " + info.fieldType);
bean.bundleInfos.add(info);
}
}
}
因为kotlin中很多类型和java不同,需要进行类型转换:
public static int typeExchange4Kotlin(KtTypeReference ktTypeReference, PsiElement pe) {
String fieldTypeName = ktTypeReference.getTypeElement().getText();
if( fieldTypeName.endsWith("?")){
fieldTypeName = fieldTypeName.substring(0, fieldTypeName.length()-1);
}
//获取的是类型全路径(在java项目中是全的,在android中不是),则只要最后一节才是类名字
String[] fieldTypePath = fieldTypeName.split("\\.");
fieldTypeName = fieldTypePath[fieldTypePath.length - 1];
switch (fieldTypeName) {
case KT_BYTE:
return TypeKind.BYTE.ordinal();
case KT_SHORT:
return TypeKind.SHORT.ordinal();
case KT_INT:
return TypeKind.INT.ordinal();
case KT_LONG:
return TypeKind.LONG.ordinal();
case KT_FLOAT:
return TypeKind.FLOAT.ordinal();
case KT_DOUBLE:
return TypeKind.DOUBLE.ordinal();
case KT_BOOLEAN:
return TypeKind.BOOLEAN.ordinal();
case KT_CHAR:
return TypeKind.CHAR.ordinal();
case KT_STRING:
return TypeKind.STRING.ordinal();
case KT_StringArrayList:
return TypeKind.StringArrayList.ordinal();
case KT_IntegerArrayList:
return TypeKind.IntegerArrayList.ordinal();
default:
return typeExchange(pe);
}
}
public static int typeExchange( PsiElement pe) {
boolean isParcelableType = false;
boolean isSerializableType = false;
String superClassName = "";
if(pe != null && pe instanceof KtClass) {
List<KtSuperTypeListEntry> superTypes= ((KtClass)pe).getSuperTypeListEntries();
for (KtSuperTypeListEntry supertype:superTypes) {
superClassName = supertype.getTypeReference().getTypeElement().getText(); //Android里拿不到父类的全路径
if (superClassName.equals(KT_PARCELABLE)) {
isParcelableType = true;
} else if (superClassName.equals(KT_SERIALIZABLE)) {
isSerializableType = true;
}
}
} else if(pe != null && pe instanceof PsiClass) {
superClassName = ((PsiClass)pe).getSuperClass().getQualifiedName();
if (superClassName.equals(KT_PARCELABLE)) {
isParcelableType = true;
} else if (superClassName.equals(KT_SERIALIZABLE)) {
isSerializableType = true;
}
} else {
return TypeKind.SERIALIZABLE.ordinal();
}
if (isParcelableType) {
return TypeKind.PARCELABLE.ordinal();
} else if (isSerializableType) {
return TypeKind.SERIALIZABLE.ordinal();
} else {
// SERIALIZABLE,生成的类如果类型路径不全,开发可以自己引入。
return TypeKind.SERIALIZABLE.ordinal();
}
}
4.生成Launcher类内容。
根据源代码中收集的 CallBackBundleInfo信息,使用StringBuilder拼接生成launcher类的内容。内容需要考虑代码缩进等格式问题,比较繁琐,可视效果也不好,代码如下:
private String generateJavaContent(String className, CallBackBundleInfo bean) {
StringBuilder builder = new StringBuilder();
builder.append("package ");
builder.append(Constants.PACKAGE_NAME);
builder.append(";");
builder.append("\n");
builder.append("import android.os.Bundle;").append("\n");
builder.append("import java.lang.*;").append("\n"); //包括基础类型Integer,Short等,和String
builder.append("import java.util.*;").append("\n"); //包括list,ArrayList
//省略......
if (bean.hasCallback) {
builder.append("import hy.sohu.com.comm_lib.utils.rxbus.RxBus;").append("\n");
builder.append("import hy.sohu.com.comm_lib.utils.callback.LaunchDataEvent;").append("\n");
//省略......
}
builder.append("import ").append(bean.classPath).append(";").append("\n");
builder.append("public final class ").append(className).append("Launcher {\n" +"\n");
builder.append("\n");
if(bean.isOnlyLauncher){
builder.append(" public static final class Builder {\n" +
" private Uri uri;");
builder.append("\n");
builder.append(" public void lunch(Context ctx) {\n" +
" if (ctx==null)return;\n");
builder.append(
" Intent intent = new Intent(ctx,").append(className).append(".class);\n" +
" if (!(ctx instanceof Activity)) {\n" +
" intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n" +
" }\n");
builder.append(
" if (uri!=null){\n" +
" intent.setData(uri);\n" +
" }\n" +
" ctx.startActivity(intent);\n" +
" }");
//省略......
} else {
builder.append(" public static final class Builder {\n" +
"\n" +
" private final Bundle args;\n" +
" private Uri uri;");
if (bean.hasCallback) {
builder.append("\n").append(" private String launchId;");
builder.append("\n").append(" private CallBack callback;");
}
builder.append("\n");
for (BundleInfo info : bean.bundleInfos) {
if (info.isStatic) {
builder.append(" private static ").append(info.fieldTypeName).append(" ").append(info.fieldName).append(";");
builder.append("\n");
}
}
builder.append("\n");
builder.append(" public Builder(");
StringBuilder builder1 = new StringBuilder();
for (BundleInfo info : bean.bundleInfos) {
if (info.isThrowError) {
builder1.append(info.fieldTypeName).append(" ").append(info.fieldName).append(",");
}
}
String params = builder1.toString();
if (!params.isEmpty()) {
params = params.substring(0, params.length() - 1);
builder.append(params);
}
builder.append(") {\n" +
" this.args = new Bundle();\n");
for (BundleInfo info : bean.bundleInfos) {
if (info.isThrowError) {
if(info.isStatic){
if(info.fieldTypeName.contains("List")){
builder.append(" if(this.").append(info.fieldName).append("!=null){\n")
.append(" this.").append(info.fieldName).append(".clear();\n")
.append(" this.").append(info.fieldName).append(".addAll(").append(info.fieldName).append(");")
.append("\n")
.append(" }else {\n")
.append(" this.").append(info.fieldName).append("= new java.util.ArrayList<>();\n")
.append(" this.").append(info.fieldName).append(".addAll(").append(info.fieldName).append(");")
.append("\n")
.append(" }")
.append("\n");
}else {
builder.append(" this.").append(info.fieldName).append(" = ").append(info.fieldName).append(";")
.append("\n");
}
}else {
builder.append(" ").append(buildPutDoc(logger, info.fieldType, "\"" + info.fieldName + "\""
, info.fieldName, className, info.fieldTypeName)).append("\n");
}
}
}
//省略......
if(bean.hasCallback){
builder.append("\n @Subscribe(threadMode = ThreadMode.MAIN)\n" +
" public void onReceiveDataEvent(").append("LaunchDataEvent<");
if(bean.isList){
builder.append("List<").append(bean.dataClassPath).append(">");
}else {
builder.append(bean.dataClassPath);
}
builder.append("> event").append("){\n")
.append(" if(event.launchId.equals(launchId)){\n" +
" if(callback!=null){\n" +
" callback.onSuccess(event.data);\n" +
" callback=null;\n" +
" }\n")
.append(" LaunchUtil.INSTANCE.postDelayUnRegister(this);")
.append(" }")
.append("\n }\n\n");
//省略......
}
//省略......
}
builder.append("}");
return builder.toString();
}
5.生成Launcher类文件。
得到Launcher类内容后,将内容写入generate目录中的对应文件里,代码如下:
String genFilePackagePath = "/src/main/java/com/sohu/generate";
private boolean genLaunchJavaFile(String className, String content) {
modulePath = psiFile.getVirtualFile().getPath();
try {
File file = new File(modulePath+genFilePackagePath);
if (!file.exists()) {
file.mkdirs();
}
File genFile = new File( modulePath+genFilePackagePath, className + "Launcher.java");
if (genFile.exists()) {
genFile.delete();
System.out.println("cjf---"+ "genFile already exists, delete first ! ");
}
genFile.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(genFile);
fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
fileOutputStream.flush();
fileOutputStream.close();
} catch (FileNotFoundException e) {
return false;
} catch (IOException e) {
return false;
}
return true;
}
开发完成后,我们可以通过runIde task进行调试,最后编译生成插件genLaunchActivity-1.0-SNAPSHOT.jar提供给项目开发人员使用。6.3插件安装和使用
1.插件安装
在Android Studio中,通过本地安装genLaunchActivity-1.0-SNAPSHOT.jar插件,如图所示:
2.插件使用
在需要生成Launcher类的源码中,右键菜单选择 Generate->genLauncherActivity,如下图所示:
插件运行完成,会toast提示生成结果,如果提示success,则会在对应project下的“/src/main/java/com/sohu/generate”目录中,找到对应的***Launcher.java文件,如下图所示:
6.4效果
GenLaunchActivity插件运行生成文件耗时在毫秒级,非常快。
GenLaunchActivity插件生成文件,也不存在增量支持的问题,源文件修改后,使用插件重新生成一次即可。
GenLaunchActivity插件生成的Launcher文件会直接成为源码文件。如果没有重新生成Launcher文件,是完全不用重新编译的,也不会影响项目的增量编译。7总结优化KAPT编译耗时慢问题,有三种解决方案,不同方案各自的优缺点,总结如下:
1.支持KAPT的增量:开发难度小,工作量小,能解决增量编译速度慢问题,但不解决KAPT全量编译缓慢的问题,增量起效的成功率不稳定;
2.升级到KSP:开发工作量和KAPT相当,难度中等,能同时解决增量编译速度慢问题和KAPT全量编译速度缓慢问题,且增量起效的成功率有明显提高;
3.迁移到Android Studio插件:开发难度大,需要分别处理Java文件和Kotlin文件,类型获取麻烦,且不支持KotlinPoet和JavaPoet生成代码文件,手动拼接代码,非常繁琐易错。但它生成文件速度快,编译过程不会运行,所有也不影响增量编译效果。;
每个方案都有自己的优点,我们需要根据自身的项目情况,和插件的使用场景,复杂度来选择合适的方案。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
受欢迎的鸿蒙工具库(harmony-utils)
NDK开发、Native Hook、Android性能优化必知:动态链接
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!