全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-10-24_深入浅出 Compose Compiler(2) 编译器前端检查

您的位置:首页 >> 新闻 >> 行业资讯

深入浅出 Compose Compiler(2) 编译器前端检查 本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究! 前一篇文章最后提到了 Compose Compiler 中的众多 Extension,其中一些是编译期前端的各种 Checker ,他们负责对 Compose 代码进行编译期检查: ComposableCallChecker:检查是否可以调用 @Composable 函数ComposableDeclarationChecker:检查 @Composable 的位置是否正确ComposeDiagnosticSuppressor:屏蔽不必要的编译诊断错误ComposableCallCheckerComposableCallChecker负责检查 Composable 的调用是否合法。Compose Compiler 的 Checker 目前还不支持 FIR ,需基于 PSI 进行检查。 Compiler 基于访问者模式深度遍历每个 PSI 节点。ComposableCallChecker 继承自 CallChecker,后者在 PSI 访问过程中,当遇到CALL_EXPRESSION时 check 方法会被回调,我们可以在此处通过向上遍历 Parent 看调用是否合理。 上图的 Case 中,当我们遇到 CALL_EXPRESSION 节点时,判断它是否是一个 Composable 调用,我们向上查找父节点,当 Parent 中出现 FUN 时,检查它有没有携带 @Composable ,如果没有携带则报错。 简单看一下 check 方法的相关实现: openclassComposableCallChecker: CallChecker, AdditionalTypeChecker, StorageComponentContainerContributor{ //... overridefuncheck( resolvedCall:ResolvedCall, reportOn:PsiElement, context:CallCheckerContext ){ if(!resolvedCall.isComposableInvocation()){ //如果当前不是Composable调用,则停止检查 return } //... loop@while(node!=null){ //遍历父节点,对调用处的合法性进行检查 when(node){ //... isKtFunction-{ valdescriptor=bindingContext[BindingContext.FUNCTION,node] if(descriptor==null){ illegalCall(context,reportOn) return } valcomposable=descriptor.isComposableCallable(bindingContext) if(!composable){ illegalCall(context,reportOn,node.nameIdentifier?:node) } //... return } //... } node=node.parentas?KtElement } //... } //... } KtFunction是 PsiElement 中 FUN 对应的节点类型,这里出现了前一篇文章中介绍过的 bindingContext 。我们可以从 BindingContext 获取当前 node 对应的 Descriptor。isComposableCallable中判断节点是否添加了 @Composable 注解,如果不是一个 Composable 函数,即出现了非法调用,使用illegalCall编译报错;若是一个合法调用则正常 return。 再看一下当 node 为KtLambdaExpression的 case,即在 Lambda 中调用 Composable 函数: loop@while(node!=null){ when(node){ //... isKtLambdaExpression-{ //... //检查是否是@Composable valcomposable=descriptor.isComposableCallable(bindingContext) if(composable)return //... //如果不是@Composable,则判断是否是inline valisInlined=isInlinedArgument( node.functionLiteral, bindingContext, true ) if(!isInlined){ //如果不是inline报错退出 illegalCall(context,reportOn) return }else{ //如果是inline在BindingContext做记录,然后继续向上查找 context.trace.record( ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE, descriptor, true ) } } //... } node=node.parentas?KtElement } 这里有一个值得注意的检查逻辑,判断 lambda 是否为 inline。对于 inline lambda 可以不添加 @Composable ,只要调用 lambda 的地方是 @Composable 即可。 用下面的例子阐释这个检查效果: Bar 接收一个 lambda 参数 block,由于 Bar 是一个 inline 函数,即使 block 本身没有 @Composable,但是当在 @Composable 的 Foo 中调用 inline 的 lambda 时,lambda 内部对 Composable 的调用不会出错,所以可以正常调用 Composable Baz。 代码中出现了context.trace.record,它用来在 BindingContext 中为 descriptor 添加一些上下文信息。PSI 的遍历基于访问者模式,因此获取距离当前节点较远的信息是比较麻烦的。通过context.trace可以对访问过的节点信息记录后更大范围使用,比如这里对访问过的 inline lambda 做了标记LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,表示这个 lambda 中可以调用 Composable ,在后续访问其他节点时,就可以快速对这个 node 进行这方面的判断。 @DisallowComposableCalls到这里也许有人会问,我如果就是不想 inline lambda 中调用 Composable 怎么办?原来 Compiler 源码中也已经揭示了相关解决方案: //获取lambda参数的信息 valarg=getArgumentDescriptor(node.functionLiteral,bindingContext) //检查lambda参数是否有@DisallowComposableCalls注解 if(arg?.type?.hasDisallowComposableCallsAnnotation()==true){ context.trace.record( ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE, descriptor, false ) context.trace.report( ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION.on( reportOn, arg, arg.containingDeclaration ) ) return } 这段逻辑会获取 lambda 作为参数定义时的信息,判断 lambda 参数是否添加了@DisallowComposableCalls注解。添加了此注解的 lambda 即使是 inline 的也不允许内部调用 Composable。因此这里使用context.trace.report报了编译错误,同时用context.trace.record为 node 做了记录LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE为 false。 context.trace.report报错时的具体文案定义在 ComposeErrorMessages 中,有时这些 messages 可以帮助我们理解 Compiler 源码的含义 MAP.put( ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION, "Composablecallsarenotallowedinsidethe{0}parameterof{1}", Renderers.NAME, Renderers.COMPACT ) @ReadOnlyComposableisKtFunction-{ //检查@Composable注解 valcomposable=descriptor.isComposableCallable(bindingContext) if(!composable){ illegalCall(context,reportOn,node.nameIdentifier?:node) } //检查@ReadOnlyComposable注解 if(descriptor.hasReadonlyComposableAnnotation()){ //enforcethattheoriginalcallwasreadonly if(!resolvedCall.isReadOnlyComposableInvocation()){ illegalCallMustBeReadonly( context, reportOn ) } } return } 当 node 是 KtFunction 时,除了 @Composable,还对另一个注解@ReadOnlyComposable进行了检查,即 @ReadOnlyComposable 函数只能在 @ReadOnlyComposable 内调用。那么 @ReadOnlyComposable 是做什么的呢? 我们知道添加 @Composable 注解的函数内部在编译期会生成startXXGroup/endXXGroup等代码,Group 可以理解为 Composition 的节点,函数在运行时,通过这些生成的代码将创建 Group 并写入 Composition ,最终实现整个 UI 树的构建和更新。某些情况下 Composable 函数并不需要创建 Group,所以也无需生成这些代码,此时通过添加 @ReadOnlyComposable 注解,有助于节省一些 Compose 编译和运行时的开销。 一个常见的 @ReadOnlyComposable 的使用场景是对 MaterialTheme 的 colors, typography, shapes 等的访问,此时我们仅仅是需要访问 CompositionLocal,并不会调用其他 Composable 函数: objectMaterialTheme{ valcolors:Colors @Composable @ReadOnlyComposable get()=LocalColors.current //... } 之前大家可能很少留意到 @DisallowComposableCalls,@ReadOnlyComposable 等注解的存在,而现在通过阅读 Compiler 源码,加深了我们对 Compose 的掌握程度。 ComposableCallChecker 里还很多检查逻辑,相信有了前面的介绍,剩余的源码大家应该又能去自行阅读了。 ComposableDeclarationCheckerComposableDeclarationChecker主要检查 @Composable 出现的位置是否合法。 @Retention(AnnotationRetention.BINARY) @Target( //functiondeclarations //@ComposablefunFoo(){...} //lambdaexpressions //valfoo=@Composable{...} AnnotationTarget.FUNCTION, //typedeclarations //varfoo:@Composable()-Unit={...} //parametertypes //foo:@Composable()-Unit AnnotationTarget.TYPE, //composabletypesinsideoftypesignatures //foo:(@Composable()-Unit)-Unit AnnotationTarget.TYPE_PARAMETER, //composablepropertygettersandsetters //valfoo:Int@Composableget(){...} //varbar:Int //@Composableget(){...} AnnotationTarget.PROPERTY_GETTER ) annotationclassComposable 从注解本身的定义可知,@Composable 可以修饰函数、函数类型、函数类型的参数以及 Custom-get 等场所。对于 AnnotationTarget 不正确的情况,无需 Compose Compiler,常规 Kotlin Compiler 就能发现错误。但即使 AnnotationTarget 符合上述几种类型,也不代表就一定可以添加 @Composable 注解,此时需要借助 Compose Compiler 的 ComposableDeclarationChecker 进行进一步检查。 checkFunction当 @Composable 修饰了函数时,并非所有的函数都可以变身为 Composable 函数。 例如 main 函数不能成为 Composable 函数,因为 main 需要被系统调用,还无法提供 Composer 上下文; //main不能添加@Composable if(hasComposableAnnotation&& descriptor.name.asString()=="main"&& MainFunctionDetector( context.trace.bindingContext, context.languageVersionSettings ).isMain(descriptor) ){ context.trace.report( COMPOSABLE_FUN_MAIN.on(declaration.nameIdentifier?:declaration) ) } 再比如,suspend 函数也不能成为 Composable 函数,suspend 自身在编译期有大量的 codegen 产生,这与 Compose 的 codegen 难以协调: //suspend不能添加@Composable if(descriptor.isSuspendhasComposableAnnotation){ context.trace.report( COMPOSABLE_SUSPEND_FUN.on(declaration.nameIdentifier?:declaration) ) } 当函数有重写时,还需要检查与被重写函数是否一致,即 Composable 函数的重写实现也必须是 Composable 函数,反之普通函数的重写函数必须是普通函数。不一致时会报下面的错误: 相关 check 代码如下: if(descriptor.overriddenDescriptors.isNotEmpty()){ //找到当前函数重写的父函数 valoverride=descriptor.overriddenDescriptors.first() //检查父子函数的一致性 if(override.hasComposableAnnotation()!=hasComposableAnnotation){ context.trace.report( ComposeErrors.CONFLICTING_OVERLOADS.on( declaration, listOf(descriptor,override) ) ) } //... } checkTypeprivatefuncheckType( type:KotlinType, element:PsiElement, context:DeclarationCheckerContext ){ if(type.hasComposableAnnotation()type.isSuspendFunctionType){ context.trace.report( COMPOSABLE_SUSPEND_FUN.on(element) ) } } 上面 checkType 方法可以对函数类型的参数进行检查,不能同时是 suspend 和 Composable 但是令人不解的是,当函数作为变量类型时,没有调用 checkType 进行检查,个人感觉应该是 Compiler 的 bug,期待后续修正。 checkProperty@Composable 可以修饰属性的 get() 方法,但是此时不允许次属性有幕后字段 valinitializer=declaration.initializer valname=declaration.nameIdentifier //property如果有初始化值,意味着有默认幕后字段,其get不能是Composable函数 if(initializer!=nullname!=null){ context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name)) } //property如果是var的,意味着有幕后字段,get不能是Composable函数 if(descriptor.isVarname!=null){ context.trace.report(COMPOSABLE_VAR.on(name)) } 上述检查逻辑的效果如下: ComposeDiagnosticSuppressorDiagnosticSuppressor与其他 Checker 不同,它不是发现错误,而是屏蔽一些不必要的检查。有些 Kotlin Compiler 默认的诊断检查对于 Compose 的场景并不适用。 ComposeDiagnosticSuppressor继承自 DiagnosticSuppressor,重写isSuppressed方法,参数 diagnostic 获得当前发现的错误,返回 true 则可以屏蔽这个错误 NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSIONopenclassComposeDiagnosticSuppressor:DiagnosticSuppressor{ //... overridefunisSuppressed(diagnostic:Diagnostic,bindingContext:BindingContext?):Boolean{ if(diagnostic.factory==Errors.NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION){ for( entryin( diagnostic.psiElement.parentasKtAnnotatedExpression ).annotationEntries ){ if(bindingContext!=null){ valannotation=bindingContext.get(BindingContext.ANNOTATION,entry) if(annotation!=nullannotation.isComposableAnnotation)returntrue } elseif(entry.shortName?.identifier=="Composable")returntrue } } //... returnfalse } } 上面逻辑中屏蔽了NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION,当遇到 @Composable 时不报错。通常什么情况下报这种错呢? 上面的例子中 foo 是一个接受 lambda 参数的 inline 函数。我们在 foo 调用处为 lambda 添加 @MyAnnotation ,此时编译报错 The lambda expression here is an inlined argument so this annotation cannot be stored anywhere 这就是所谓的 NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION。这并非是说注解添加错了地方,AnnotationTarget.FUNCTION可以修饰 lambda ,无论是声明处还是调用处。错误的原因是因为 @MyAnnotation 没有添加@Retention(AnnotationRetention.SOURCE),这意味着注解需要在编译后被保留,而 inline lambda 在编译后就不存在了,为了避免注解失效,编译期报错。 可以通过将注解声明为AnnotationRetention.SOURCE来解决此问题,当然,也可以通过添加 @Suppress 注解来屏蔽报错: @Suppress("NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION") 那么 Compose Compiler 为什么不需要这个检查呢? 如上,ComposeDiagnosticSuppressor 的作用下, @Composable 并非 AnnotationRetention.SOURCE,但是同样修饰 inline lambda 没有报错。因为 inline 函数的调用方是 Composale,所以即使 inline lambda 的 @Composable 在编译后丢失也不影响整个内部的 codegen。 但是个人感觉对 inline lambda 诊断屏蔽意义不大,这本身就不是常见 case,而且如果 inline 函数的调用方不是 @Composable 函数时,编译期没有提醒可能会造成运行时异常。 NAMED_ARGUMENTS_NOT_ALLOWED另一个屏蔽的错误是 NAMED_ARGUMENTS_NOT_ALLOWED if(diagnostic.factory==Errors.NAMED_ARGUMENTS_NOT_ALLOWED){ if(bindingContext!=null){ valcall=(diagnostic.psiElement.parent.parent.parent.parentasKtCallExpression) .getCall(bindingContext).getResolvedCall(bindingContext) if(call!=null){ returncall.isComposableInvocation() } } } returnfalse Kotlin 中允许使用“命名参数”, 即在调用函数时可以基于 name 指定参数,不必拘泥于原本参数在函数签名中的位置。但这有个例外,即当函数作为类型使用时,函数的参数不能通过 name 指定,否则会报错: Named arguments are not allowed for function types. 这就是所谓的NAMED_ARGUMENTS_NOT_ALLOWED。 如果 foo 的函数类型添加是 @Composable ,则不再报这个错误。 Composable 函数编译期原本就需要修改函数签名,可以处理对 named arguments 的调用,而且 Compose 的 DSL 语法中类似的基于 name 的参数指定出现的频率更高,因此屏蔽此类错误有利于提升开发效率。 本文带大家简单了解了 Compose Compiler 在前端主要做了哪些事情,更多前端的逻辑大家有兴趣可以自行去阅读。下一篇文章起我们进入到编译器后端的领域,看一下 Compose 代码是如何生成的。 阅读原文

上一篇:2022-12-21_可扩展、可解释,新框架从预训练语言模型中高效提取知识图谱 下一篇:2025-06-15_谢赛宁、贾扬清获奖!牛津华人博士生拿下CVPR 2025最佳论文

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价