Compose的诞生

在2019年的谷歌IO大会上,Compose作为Android新一代UI开发亮相,因为声明式开发越来越流行了,对标IOS开发SwiftUi,Compose的立项也为Android开发新加了声明式ui的开发选项,在2021年7月1.0正式版本的诞生,也意味着Compose即将进入生产环节,国际app巨头Twitter就首当其冲,在新页面上用上了Compose

Compose好处

与传统的xml相比,Compose不仅吸收了其优点,摒弃了糟粕,还具有以下几个优点

声明式 兼容性 跨平台 布局效率
不同于传统的命令式,ui的刷新需要调用者主动调用刷新方法,比如TextView需要特定的setText进行文本变化,而compose在定义好声明状态后,由框架自主调用刷新,减少状态不一致 compose最低兼容到android api 21,不但可以在原来View体系中嫁接使用,也可以在compose中使用原来View体系的xml 跨平台,目前支持macos等多个平台,跨平台由Jetbrain团队在做,compose未来会实现ui多跨平台,同时也搭配逻辑跨平台KMM项目(有关kmm的以后有机会可以再说说,比起说跨平台,更不如说是多平台,因为编译出来的代码是直接符合原平台开发规范的,比如ios编译出来的就是framework),未来实现ui跟逻辑都跨平台也不在遥远 compose 是严格遵循LayoutNode的单次测量,不会出现View的多次测量导致的问题,在ui卡顿或者ui规范上,是非常重要的改进

因为Compose是Android团队与JetBrain在推,国内外的学习热情都挺好,目前国内也有不少大厂进行了尝试阶段,比如字节。

Compose 架构

说了这么多,那么Compose是怎么做到在原有View体系做到兼容并改善的呢?

我们从一个例子出发:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
             xxx组件
        }
    }
}

我们可以看到,Compose在Activity中,用了setContent方法代替了原有的setContentView方法,那么setContent做了什么呢?

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView
    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

可以看到,android.R.id.content的第一个孩子被替换成了ComposeView,而这个ComposeView,就是Compose环境的提供者,在这里面,Compose将剔除原本View体系的测量逻辑,从而采用自己的测量架构。如下图包饺子架构所示:

值得注意的是ComposeView是继承于AbstractComposeView(他定义了compose环境的规范),还有就是android中是多window架构的,所以针对Dialog这种,也有特别的环境实现类。针对PopupWindow这种共用window的组件,也同样提供了compose环境实现类

可以看到架构图中,AbstractComposeView其实也是继承于ViewGroup的。所以准确来说,Compose并没有完全脱离AndroidView体系,而是在这之上建立起了中间层,这就印证了一句老话,没有什么架构是不能解决的,如果有,那就加个中间层!而这个中间层,提供了全新的设计,从而让android得以脱胎换骨到一个新架构!

@Composable的背后

说到Compose,那么肯定离不开Compsoable的介绍,我们肯定会有一个疑问,为什么一个函数加上了Composable注解,就变成了一个可见的视图了呢?比如

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

我们进行反编译后

    public static final void Greeting(String name, Composer $composer, int $changed) {
        Intrinsics.checkNotNullParameter(name, "name");
        Composer $composer2 = $composer.startRestartGroup(-154424256);
        ComposerKt.sourceInformation($composer2, "C(Greeting)46@1589L27:MainActivity.kt#8m9ksz");
        int $dirty = $changed;
        if (($changed & 14) == 0) {
            $dirty |= $composer2.changed(name) ? 4 : 2;
        }
        if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4567String$0$str$arg0$callText$funGreeting()   name   LiveLiterals$MainActivityKt.INSTANCE.m4584String$2$str$arg0$callText$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
    }

可以看到,实际上,编译器为我们的带有@Composable的Greeting方法添加了两个参数,

Composer $composer, int $changed

Composer就是我们真的compose执行时的操作者,changed就是一个参与是否重组的标识之一,这种通过注解在编译时生成对应参数的方案,在Coroutine里面也用到,当然我们也可以通过ASM等方法去编译时判断注解并更改相应的函数方法,只不过这部分由compose编译器帮我们做了。

我们重点关注一下composer.startRestartGroup这个方法

override fun startRestartGroup(key: Int): Composer {
    start(key, null, false, null)
    addRecomposeScope()
    return this
}

可以看到传入了一个key为Int类型的标识,所有Composable函数区域会通过特定key去判断重组的范围以及当前范围是否进行更新(范围就是startRestartGroup - endRestartGroup 之间的状态),当然一个Composable函数里面可能存在多个重组范围,我们还是拿上面的Greeting函数做个例子,不过这次有点变化

@Composable
fun Greeting(name: String) {
    val  state by remember {
        mutableStateOf(true)
    }
    if (state){
        Text(text = "true")
    }else{
        Text(text = "false")
    }
}

此时反编译后就会多一些代码

    public static final void Greeting(String name, Composer $composer, int $changed) {
        Object value$iv$iv;
        Intrinsics.checkNotNullParameter(name, "name");
        Composer $composer2 = $composer.startRestartGroup(-154424404);
        ComposerKt.sourceInformation($composer2, "C(Greeting)43@1455L45:MainActivity.kt#8m9ksz");
        if (($changed & 1) == 0 && $composer2.getSkipping()) {
            $composer2.skipToGroupEnd();
        } else {
            $composer2.startReplaceableGroup(-492369756);
            ComposerKt.sourceInformation($composer2, "C(remember):Composables.kt#9igjgp");
            Object it$iv$iv = $composer2.rememberedValue();
            if (it$iv$iv == Composer.Companion.getEmpty()) {
                value$iv$iv = SnapshotStateKt__SnapshotStateKt.mutableStateOf$default(Boolean.valueOf(LiveLiterals$MainActivityKt.INSTANCE.m4554x2b38c863()), null, 2, null);
                $composer2.updateRememberedValue(value$iv$iv);
            } else {
                value$iv$iv = it$iv$iv;
            }
            $composer2.endReplaceableGroup();
            MutableState state$delegate = (MutableState) value$iv$iv;
            // 关注这里
            if (m4607Greeting$lambda1(state$delegate)) {
                $composer2.startReplaceableGroup(-154424297);
                ComposerKt.sourceInformation($composer2, "47@1525L19");
                TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4597String$arg0$callText$branch$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
                $composer2.endReplaceableGroup();
            } else {
                $composer2.startReplaceableGroup(-154424258);
                ComposerKt.sourceInformation($composer2, "49@1564L20");
                TextKt.m1219TextfLXpl1I(LiveLiterals$MainActivityKt.INSTANCE.m4598String$arg0$callText$else$if$funGreeting(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
                $composer2.endReplaceableGroup();
            }
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
    }

可以看到,我们状态state变量改变的时候,compose会在if里面生成先startReplaceableGroup,代表着这个范围是可以被替换的,也就是说当if成立的时候会生成一个composition节点,此时如果state变成了false,那么这个if里面的范围group就会被移除,此时父group(即在之前调用startgroup的范围)以替换的方式载入新的else,子group,这就是startReplaceableGroup的含义。

总之,我们可以注意到,key就是一个非常关键的点,就是让我们compose识别到哪些范围能够进行重组,哪些不能!在if else语句中,如果我们依赖一个state,就会在相应的if语句里面插入一个新的子范围group,这就是compose架构中的的智能重组

智能重组真的那么智能吗

上面我们提到了智能重组的这个概念,那么智能重组真的如字面所说,那么“智能”吗?我们很容易想到,既然compose能在if else这种有作用域的关键字上加上子group,如果是list这种呢?存在着循环语句的条件呢?就算在循环语句里面加上子group,并不能满足我们想要的需求呀!举个🌰

@Composable
fun CustomList(list: List<CustromData>) {
   Column {
       for (item in list){
           CustromView(item)
       }
   }
}

我们想要for循环里面的list,如果list改变了数据,我们希望compose只重组改变部分的数据,而不是全部list里面的CustromView,那么compose能做到吗?compose:老子不干了! 是的!做不到!因为已经生成的CustromView的key只能依靠着当前的数据决定了,比如list的index,如果下次index更改,那么key就会因为index的不一致,导致了list中每一个生成的CustromView再次重组

CustomList部分反编译代码

  ColumnScopeInstance columnScopeInstance = ColumnScopeInstance.INSTANCE;
            int $changed2 = ((0 >> 6) & 112) | 6;
            // 只加了一个startReplaceableGroup
            $composer2.startReplaceableGroup(-1487261693);
            ComposerKt.sourceInformation($composer2, "C*52@1739L17:MainActivity.kt#8m9ksz");
            if ((($changed2 & 81) ^ 16) != 0 || !$composer2.getSkipping()) {
                Iterator<CustromData> it = list.iterator();
                // 循环没办法加入子group
                while (it.hasNext()) {
                    Iterator<CustromData> it2 = it;
                    CustromData item = it.next();
                    CustromView(item, $composer2, 0);
                    $changed$iv = $changed$iv;
                    it = it2;
                }
            } else {
                $composer2.skipToGroupEnd();
            }
            $composer2.endReplaceableGroup();
    public static final void CustromView(CustromData data, Composer $composer, int $changed) {
        Intrinsics.checkNotNullParameter(data, "data");
        Composer $composer2 = $composer.startRestartGroup(-201540877);
        ComposerKt.sourceInformation($composer2, "C(CustromView)59@1825L23:MainActivity.kt#8m9ksz");
        int $dirty = $changed;
        if (($changed &amp; 14) == 0) {
            $dirty |= $composer2.changed(data) ? 4 : 2;
        }
        if ((($dirty &amp; 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            TextKt.m1219TextfLXpl1I(data.getTest2(), null, 0L, 0L, null, null, null, 0L, null, null, 0L, 0, false, 0, null, null, $composer2, 0, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
        if (endRestartGroup == null) {
            return;
        }
        endRestartGroup.updateScope(new MainActivityKt$CustromView$1(data, $changed));
    }

所以一旦出现增删改查list的情况,那么不好意思,compose也就只能把group里面的list再重组一次,如图:

这也是为什么list系列的compose函数,比如LazyRow等会被诟病为有性能问题。那么compose就没有相关解决方法吗?嗯!官方肯定准备了,就是我们可以主动设置当前composable函数的key!从而避免额外的重组!

@Composable
inline fun <T> key(
    @Suppress("UNUSED_PARAMETER")
    vararg keys: Any?,
    block: @Composable () -> T
) = block()

回到我们自定义的list,就可以这样使用

for (item in list){
    key(keys = arrayOf(item.test) ) {
        CustromView(item)
    }
}

当然,我们的LazyList系列也可以直接指定,在item函数及其他items函数中直接使用

interface LazyListScope {
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
...

最后

好的!我们的神奇的compose第一篇架构片就到此结束啦!接下来也会不定期更新其他的compose篇章,让我们拥抱全新的compose吧!更多关于Android开发Compose框架的资料请关注Devmax其它相关文章!

Android开发Compose框架使用开篇的更多相关文章

  1. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  3. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  4. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  5. xcode6.1 – Xcode 6.1中项目模板中缺少类前缀

    项目模板上曾经有一个类前缀字段,这有助于区分项目类和框架类.Xcode6.1项目模板中不再提供此功能.这背后的意图是什么?

  6. ios – 伞框架

    错误.应用程序,通常位于…错误仍然存在你也可以在这里添加(子)框架的路径.

  7. ios – UIView框架大小的问题

    我正在开发一个iPad项目,目前正在使用Landscape视图.我试着这样做:为什么这总是返回960?虽然在景观中,视图本身的高度尺寸应为768对吗?

  8. 安装自定义cocoa框架的最佳方法

    我有一个自定义框架,遵循Apple的框架编程指南>>中的建议.Installingyourframework我在/Library/Frameworks中安装.我通过使用以下脚本添加RunScript构建阶段来完成此操作:在我的项目中,我然后链接/Library/Frameworks/MyFramework并将其导入我的类中,如下所示:这非常有效,除了我总是在调试器控制台中看到以下消息:Loadin

  9. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  10. ios – 在设备上构建和运行时,仅将嵌入式框架与其他动态框架链接失败

    TL;博士将您的嵌入式框架与其他框架链接,并且不将其他框架与您的应用程序链接,导致Build&在设备上运行.描述:建立:我的设置非常简单(Swift2.3&XcodeXcode8.0;Build版本8S162m):>使用Carthage(0.17.2)我用xcodebuild8.0和TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3carthagebui

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部