Jetpack Compose 导致的编译劣化 | KCP 简介

2022-09-02 11:49:48 浏览数 (1)

前言

最近的更新频率很低,虽然有很大一部分原因是我在偷懒,另外是因为想从工作中的一些奇怪的问题出发,写一些可能对大家有帮助的内容。

最近从我们编译的均值数据上发现了编译时间有劣化的现象,然后我们在buildscan排查了下全量编译的情况下任务的耗时。发现个别模块的compileKotlin耗时明显变长了很多,这个奇怪的现象引起了我们的注意。另外这个仓库可能最近最大的改动点就是引入了compose,然后开发了个新的页面。所以我们初步怀疑可能就是由于compose导致的该问题。

验证环节

我们找到了这个模块的一个切片节点,接入compose之前和接入compose之后。因为担心单次编译对其产生的误差影响,我们用相同环境进行了10次的打包验证。

未开启compose情况下编译情况

开启compose情况下编译情况

我们对比下均值数据,可以明显发现开启compose前后的编译时长发生了明显的变化。所以足以得出结论compose会导致编译速度变慢,而且非常大也非常明显。而且平均耗时增加了1min30s左右。

kcp

KCPKotlin Compiler Plugin(Kotlin编译器插件),在 kotlinc 过程中提供 hook 时机,在此期间可以生成代码、修改字节码等。

其中我们很熟悉的kotlin-android-extensions就是一KCP插件,虽然他现在也已经废弃了。

Compose编译情况不同于别的的ksp,它需要深度的参与本次编译,然后修改当前kotlin类的编译产物。将dsl等等的语法信息进行转化。

所以当我们打开gradle内的compose的时候,其实也就相当于给kcp添加了个额外的编译插件。然后在kotlinCompiler的过程中修改当前我们写的compose相关的代码。

代码语言:javascript复制
buildFeatures {
      compose true
  }
复制代码

所以从逻辑上来说,只要我们将当前模块开启compose之后就肯定会让这个模块的编译时间拉长。以下是我找了一个demo工程从0-1完成项目打包,然后对比了下kt代码和实际的class产物。

Sample工程地址 我用了一个大佬的

代码语言:javascript复制
@Composable
fun SplashPage(onNextPage: () -> Unit) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(AppTheme.colors.themeUi), contentAlignment = Alignment.TopCenter
    ) {
        LaunchedEffect(Unit) {
            delay(500)
            onNextPage.invoke()
        }
        Text(
            text = "Wan Android",
            fontSize = 32.sp,
            color = white,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(0.dp, 150.dp, 0.dp, 0.dp)
        )
    }
}
复制代码
代码语言:javascript复制
/* compiled from: SplashPage.kt */
@Metadata(d1 = {"u0000u0010nu0000nu0002u0010u0002nu0000nu0002u0018u0002nu0002bu0002u001au001bu0010u0000u001au00020u00012fu0010u0002u001abu0012u0004u0012u00020u00010u0003Hu0007¢u0006u0002u0010u0004¨u0006u0005"}, d2 = {"SplashPage", "", "onNextPage", "Lkotlin/Function0;", "(Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V", "app_debug"}, k = 2, mv = {1, 5, 1}, xi = 48)
/* loaded from: classes7.dex */
public final class SplashPageKt {
    public static final void SplashPage(Function0<Unit> onNextPage, Composer $composer, int $changed) {
        Object value$iv$iv;
        Intrinsics.checkNotNullParameter(onNextPage, "onNextPage");
        Composer $composer2 = $composer.startRestartGroup(1819087201);
        ComposerKt.sourceInformation($composer2, "C(SplashPage)23@817L6,20@724L481:SplashPage.kt#dcpdb2");
        int $dirty = $changed;
        if (($changed & 14) == 0) {
            $dirty |= $composer2.changed(onNextPage) ? 4 : 2;
        }
        if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            Modifier modifier$iv = BackgroundKt.m110backgroundbw27NRU$default(SizeKt.fillMaxSize$default(Modifier.Companion, 0.0f, 1, null), AppTheme.INSTANCE.getColors($composer2, 0).m4383getThemeUi0d7_KjU(), null, 2, null);
            Alignment contentAlignment$iv = Alignment.Companion.getTopCenter();
            $composer2.startReplaceableGroup(-1990474327);
            ComposerKt.sourceInformation($composer2, "C(Box)P(2,1,3)70@3267L67,71@3339L130:Box.kt#2w3rfo");
            MeasurePolicy measurePolicy$iv = BoxKt.rememberBoxMeasurePolicy(contentAlignment$iv, false, $composer2, ((0 >> 3) & 14) | ((0 >> 3) & 112));
            int $changed$iv$iv = (0 << 3) & 112;
            $composer2.startReplaceableGroup(1376089335);
            ComposerKt.sourceInformation($composer2, "C(Layout)P(!1,2)71@2788L7,72@2843L7,73@2855L389:Layout.kt#80mrfh");
            ComposerKt.sourceInformationMarkerStart($composer2, 103361330, "C:CompositionLocal.kt#9igjgp");
            Object consume = $composer2.consume(CompositionLocalsKt.getLocalDensity());
            ComposerKt.sourceInformationMarkerEnd($composer2);
            Density density$iv$iv = (Density) consume;
            ComposerKt.sourceInformationMarkerStart($composer2, 103361330, "C:CompositionLocal.kt#9igjgp");
            Object consume2 = $composer2.consume(CompositionLocalsKt.getLocalLayoutDirection());
            ComposerKt.sourceInformationMarkerEnd($composer2);
            LayoutDirection layoutDirection$iv$iv = (LayoutDirection) consume2;
            Function0 factory$iv$iv$iv = ComposeUiNode.Companion.getConstructor();
            Function3 skippableUpdate$iv$iv$iv = LayoutKt.materializerOf(modifier$iv);
            int $changed$iv$iv$iv = ($changed$iv$iv << 9) & 7168;
            if (!($composer2.getApplier() instanceof Applier)) {
                ComposablesKt.invalidApplier();
            }
            $composer2.startReusableNode();
            if ($composer2.getInserting()) {
                $composer2.createNode(factory$iv$iv$iv);
            } else {
                $composer2.useNode();
            }
            $composer2.disableReusing();
            Composer $this$Layout_u24lambda_u2d0$iv$iv = Updater.m898constructorimpl($composer2);
            Updater.m905setimpl($this$Layout_u24lambda_u2d0$iv$iv, measurePolicy$iv, ComposeUiNode.Companion.getSetMeasurePolicy());
            Updater.m905setimpl($this$Layout_u24lambda_u2d0$iv$iv, density$iv$iv, ComposeUiNode.Companion.getSetDensity());
            Updater.m905setimpl($this$Layout_u24lambda_u2d0$iv$iv, layoutDirection$iv$iv, ComposeUiNode.Companion.getSetLayoutDirection());
            $composer2.enableReusing();
            skippableUpdate$iv$iv$iv.invoke(SkippableUpdater.m889boximpl(SkippableUpdater.m890constructorimpl($composer2)), $composer2, Integer.valueOf(($changed$iv$iv$iv >> 3) & 112));
            $composer2.startReplaceableGroup(2058660585);
            int $changed$iv = ($changed$iv$iv$iv >> 9) & 14;
            $composer2.startReplaceableGroup(-1253629305);
            ComposerKt.sourceInformation($composer2, "C72@3384L9:Box.kt#2w3rfo");
            if ((($changed$iv & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
                BoxScopeInstance boxScopeInstance = BoxScopeInstance.INSTANCE;
                $composer2.startReplaceableGroup(1601293652);
                ComposerKt.sourceInformation($composer2, "C25@910L66,25@889L87,29@985L214:SplashPage.kt#dcpdb2");
                if ((((((0 >> 6) & 112) | 6) & 81) ^ 16) != 0 || !$composer2.getSkipping()) {
                    Unit unit = Unit.INSTANCE;
                    int i = $dirty & 14;
                    $composer2.startReplaceableGroup(-3686930);
                    ComposerKt.sourceInformation($composer2, "C(remember)P(1):Composables.kt#9igjgp");
                    boolean invalid$iv$iv = $composer2.changed(onNextPage);
                    Object it$iv$iv = $composer2.rememberedValue();
                    if (!invalid$iv$iv && it$iv$iv != Composer.Companion.getEmpty()) {
                        value$iv$iv = it$iv$iv;
                        $composer2.endReplaceableGroup();
                        EffectsKt.LaunchedEffect(unit, (Function2) value$iv$iv, $composer2, 0);
                        long sp = TextUnitKt.getSp(LiveLiterals$SplashPageKt.INSTANCE.m4958x266b4559());
                        long white = ColorKt.getWhite();
                        FontWeight bold = FontWeight.Companion.getBold();
                        int $this$dp$iv = LiveLiterals$SplashPageKt.INSTANCE.m4954x579b3509();
                        float f = Dp.m2966constructorimpl($this$dp$iv);
                        int $this$dp$iv2 = LiveLiterals$SplashPageKt.INSTANCE.m4955x709c86a8();
                        float f2 = Dp.m2966constructorimpl($this$dp$iv2);
                        int $this$dp$iv3 = LiveLiterals$SplashPageKt.INSTANCE.m4956x899dd847();
                        float f3 = Dp.m2966constructorimpl($this$dp$iv3);
                        int $this$dp$iv4 = LiveLiterals$SplashPageKt.INSTANCE.m4957xa29f29e6();
                        TextKt.m868TextfLXpl1I(LiveLiterals$SplashPageKt.INSTANCE.m4960x3de3a5a7(), PaddingKt.m283paddingqDBjuR0(Modifier.Companion, f, f2, f3, Dp.m2966constructorimpl($this$dp$iv4)), white, sp, null, bold, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 384, 64, 65488);
                    }
                    value$iv$iv = (Function2) new SplashPageKt$SplashPage$1$1$1(onNextPage, null);
                    $composer2.updateRememberedValue(value$iv$iv);
                    $composer2.endReplaceableGroup();
                    EffectsKt.LaunchedEffect(unit, (Function2) value$iv$iv, $composer2, 0);
                    long sp2 = TextUnitKt.getSp(LiveLiterals$SplashPageKt.INSTANCE.m4958x266b4559());
                    long white2 = ColorKt.getWhite();
                    FontWeight bold2 = FontWeight.Companion.getBold();
                    int $this$dp$iv5 = LiveLiterals$SplashPageKt.INSTANCE.m4954x579b3509();
                    float f4 = Dp.m2966constructorimpl($this$dp$iv5);
                    int $this$dp$iv22 = LiveLiterals$SplashPageKt.INSTANCE.m4955x709c86a8();
                    float f22 = Dp.m2966constructorimpl($this$dp$iv22);
                    int $this$dp$iv32 = LiveLiterals$SplashPageKt.INSTANCE.m4956x899dd847();
                    float f32 = Dp.m2966constructorimpl($this$dp$iv32);
                    int $this$dp$iv42 = LiveLiterals$SplashPageKt.INSTANCE.m4957xa29f29e6();
                    TextKt.m868TextfLXpl1I(LiveLiterals$SplashPageKt.INSTANCE.m4960x3de3a5a7(), PaddingKt.m283paddingqDBjuR0(Modifier.Companion, f4, f22, f32, Dp.m2966constructorimpl($this$dp$iv42)), white2, sp2, null, bold2, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 384, 64, 65488);
                } else {
                    $composer2.skipToGroupEnd();
                }
                $composer2.endReplaceableGroup();
            } else {
                $composer2.skipToGroupEnd();
            }
            $composer2.endReplaceableGroup();
            $composer2.endReplaceableGroup();
            $composer2.endNode();
            $composer2.endReplaceableGroup();
            $composer2.endReplaceableGroup();
        } else {
            $composer2.skipToGroupEnd();
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new SplashPageKt$SplashPage$2(onNextPage, $changed));
    }
}
复制代码

从上述可以发现,虽然是一段非常简单的compose写的逻辑ui,但是实际上编译出来的代码其实远比想象中的多得多,而且同样也会对调用点进行修改。因此导致编译速度变慢也就合情合理。

编译方面的抉择

我们定位到问题之后,就是如何选择优化了。如果各位大佬让我优化compose的编译速度的话,那么在下肯定无能为力了。

但是如果在我们之前的快编框架体系内的话,我们会选择将新增的compose相关的逻辑抽取出来,写成一个新的模块,然后将新旧代码进行隔离。因为对我们来说compose只是试水而已,他的改动可能并不会这么频繁.如果直接在原来的业务模块内添加compose的代码,因为本身模块都已经比较大了,然后又需要增加kotlinCompiler的时间,则会导致开发体验直线下降,大家都会说ci团队做的不好,导致编译速度变慢了。

一点点小看法

项目原先对于编译相关的监控投入的其实也并不多,大部分都要依靠与buildscan。我们也在尝试记录这部分相关的数据。和之前介绍过的一样,我们这次也是从BuildOperationNotificationListenerExecuteTaskBuildOperationDetails去获取编译任务耗时相关的数据。

这次没有考虑使用TaskExecutionListener,则还是因为复合构建的问题,由于复合构建每个gradle实例都是独立的,需要全部注册TaskExecutionListener,还有需要实例的同步,所以就有非常多的问题。

另外还写了个很挫的Top3耗时Project分析,哈哈哈。

代码语言:javascript复制
if (notification.notificationOperationDetails is ExecuteTaskBuildOperationDetails) {
          try {
              val d: ExecuteTaskBuildOperationDetails =
                  notification.notificationOperationDetails as ExecuteTaskBuildOperationDetails
              val result: ExecuteTaskBuildOperationType.Result =
                  notification.notificationOperationResult as ExecuteTaskBuildOperationType.Result
              map[notification.notificationOperationId]?.also { get ->
                  val stringBuffer = StringBuffer()
                  val exTime =
                      notification.notificationOperationFinishedTimestamp - get.notificationOperationStartedTimestamp
                
                  if (result.originExecutionTime != null) {
                      result.originExecutionTime?.apply {
                          if (exTime > this) {
                              stringBuffer.append("!!!比上次cache任务多执行 "   TimeUtils.toDec(exTime - this))
                          } else {
                              stringBuffer.append("比上次cache任务减少执行 "   TimeUtils.toDec(this - exTime))
                          }
                      }
                      stringBuffer.append("n")
                  }
                  val taskBuildData = d.create(exTime, stringBuffer.toString())
                  val taskKey = taskBuildData.getKey()
                  if (!values.containsKey(taskKey)) {
                      values[taskKey] = ModuleTasksData(taskKey)
                  }
                  values[taskKey]?.addTaskBuildData(taskBuildData)
              }
          } catch (t: Throwable) {
              t.printStackTrace()
          }
      }
复制代码

monitor 工程传送门

代码基本如下,因为在monitor下执行,就可以监控到所有工程的编译耗时了。

总结

我个人看法虽然compose的接入会对编译组产生一定的挑战,但是整体来说还是非常值得我们去尝试和挑战的一个技术。

加油各位打工人。

0 人点赞