jetpack compose原理解析

jetpack compose原理解析

jetpack compse

Jetpack Compose是Google在2019 I/O大会上公布开源的一个非捆绑工具包。Jetpack Compose是用于构建原生Android UI的现代工具包。 Jetpack Compose使用更少的代码,强大的工具和直观的Kotlin API,简化并加速了Android上的UI开发。最为重要的是jetpack compose基于响应式架构构建,完美支持响应式开发。不过目前仅有预览版,正式版还没确定,本文也是基于当前预览版对其原理进行简单分析。

声明式ui开发

在我们了解其原理之前,我们需要明白一个概念–声明式ui开发。何为声明式ui,与之相对应的还有一个命令式ui开发,目前我们安卓ui开发的大多数模式即为命令式ui开发,即我们创建了一个widget之后,需要重新获取这个widget实例,然后通过调用相关函数(即命令)改变其属性,比如以下我们常见的代码

var times = 0
val button:Button = findViewById(R.id.button)
val textView:TextView = findViewById(R.id.text)
button.setOnClickListener{
   
			++times
           textView.text="click times:${
     times}"
           }

上诉代码非常简单,实现功能也非常清楚,即一个textview用来展示button的点击次数,我们可以发现每次改变textview的文本,我们需要获取textView的实例然后调用setText这个命令去改变其文本,而声明式ui则不同,通常他会有一个状态(如flutter的widget)用来描述当前界面(状态通常是不可变的,每次变化均会产生新的实例),然后我们只需要根据其状态声明下当前界面,比如使用flutter来实现上诉功能的代码如下

class MyHomePage extends StatefulWidget {
   
  MyHomePage({
   Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
   
  int _counter = 0;

  void _incrementCounter() {
   
    setState(() {
   
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
   
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'click times:',
            ),
            Container(
                width: 20,
                height: 20,
                child: Text(
                  '$_counter',
                  style: Theme.of(context).textTheme.headline4,
                )),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

(这里为什么使用flutter代码来展示,因为我觉得jetpack compose通过注解隐藏了太多实现细节,初学者可能不太好理解,而flutter则不同,从直观上更好理解)
可以看到每次状态改变,我们都是通过build函数重新声明了界面布局,然后重新展示即可(这里StatefulWidget和State感兴趣的同学可以去了解下flutter开发,不属于本文所要讲解的范围,只需要通过该例子了解声明式开发的特点即可,可以简单的理解下每次button click的setState都会触发State.build函数重新声明布局)。这就是声明式ui和命令式ui的不同之处,从中我们也可以很快的发现声明式ui的好处,可以让我们开发更聚焦于状态的变化以及逻辑的实现,而不是在界面上,我们只需要根据当前呢的状态声明对应的ui布局即可,而且我们可以根据状态方便的进行界面重建等工作,这对于移动式开发非常友好。不过我们也可以发现其缺点,即每次重建带来的性能损耗,即使所有声明式框架都会有对应的算法在底层对组件(这里指的是渲染以及布局相关的组件,不是flutter的widget,对应于flutter即是element和renderobject)进行最大程度的复用(如react的virutal dom ,flutter的element diff 以及jetpack compose的gap buffert等),不过即使如此,在性能上对比命令式ui还是有一定损耗(不过目前来说,性能已经不是界面开发的首要考虑因素)。
接下来我们就来探索下安卓未来的ui构建方式jetpack compose的底层实现原理,不过为了更好的了解本文,大家可以先去阅读下面这篇文章
jetpack compose思想介绍
这篇文章从实现原理的层面讲解了jetpack compose,我也是基于这篇文章以及源码来去探究jetpack compose的。同时大家也会发现jetpack compose和flutter一些api的相似之处,本篇文章我也会对两者进行对比来分析下两者实现的异同

原理分析

jetpack compose从名字就可以看出,该框架基于组合优于继承的理念构建,这也是函数式响应式编程所提倡的。一开始接触jetpack compose,我第一直觉是基于安卓现有的view体系架构进行封装,尽可能的复用当前组件,但是了解后发现其实不然,jetpack compose抛弃了原有安卓view的体系,完全重新实现了一套新的ui体系(目前jetpack compose不过也提供了使用兼容原有view的方法),对此谷歌给出的解释是原有的view体系过于庞大,并且理念过于陈旧,不如借当前机会不破不立,完全基于新的理念来重新实现一套现代的ui体系。

整体框架介绍

我们大家清楚,ui最终可以使用树的形式来描述,compose也是一样,他最终是LayoutNode的一棵树,不过为了实现声明式ui的特点我们需要将配置和绘制进行分离,同时还需要在适当的时机进行node复用,所以compose借助于注解用来实现相关细节,对于compose来说整体的ui框架大致是这样的

1.@Composable 注解的函数 这里是ui的声明和配置,这也是直接面向开发者的
2.基于注解和Composer 的具体细节实现 这里实现了LayoutNode的缓存以及复用,同时也实现了对于属性变化的监听
3.LayoutNode主要用于布局和绘制

熟悉flutter开发的同学都知道,flutter中有重要的三棵树 widget 、element和renderobject。

1.widget是对ui的描述
2.element是对组件的复用
3.renderobject用于布局和绘制

我们将两者进行对比,其实发现两者有很多的相似之处,虽然实现的方式天差地别,但是其思想却是相通的(不过我个人觉得flutter的实现方式更容易理解点,compose隐藏了太多细节和运用了太多高级封装,在进行原理分析上难度可能比较大)

compose LayoutNode布局介绍

其实flutter和compose两者不仅仅思想上比较类似,在布局的实现上两者也都差不多,compose采用了和flutter一样的布局方式即盒约束(盒约束是指widget可以按照指定限制条件来决定自身如何占用布局空间,所谓的“盒”即指自身的渲染框。有关于盒约束介绍,可以查看这篇文章flutter盒约束),这个我们可以在LayoutNode的源码中看到
在这里插入图片描述
这里即用来计算布局空间约束,同样在布局过程中传递的参数Constraints 可以看出在这里插入图片描述
这里和flutter的盒约束类似,了解flutter的同学应该知道,flutter布局过程中有一个relayout boundary(重布局边界约束)这个优化条件用来加快布局,即我们的view在重布局过程中如果遇到重布局边界,将不会继续向上传递布局请求,因为这个view无论怎么变化,将不会影响父view的布局,所以父view将不需要重新布局,一开始基于两者布局的相似性我也认为compose也会采用相关优化方法,不过继续追踪代码发现并没有,这里不清楚为什么,或者后续正式版有可能会加上这个优化,这部分代码在MeasureAndLayoutDelegate 的requestRelayout中可以看到

  /**
     * Requests remeasure for this [layoutNode] and nodes affected by its measure result.
     *
     * @return returns true if the [measureAndLayout] execution should be scheduled as a result
     * of the request.
     */
    fun requestRemeasure(layoutNode: LayoutNode): Boolean {
   
        return trace("AndroidOwner:onRequestMeasure") {
   
            layoutNode.requireOwner()
            if (layoutNode.isMeasuring) {
   
                // we're already measuring it, let's swallow. example when it happens: we compose
                // DataNode inside WithConstraints, this calls onRequestMeasure on DataNode's
                // parent, but this parent is WithConstraints which is currently measuring.
                return false
            }
            if (layoutNode.needsRemeasure) {
   
                // requestMeasure has already been called for this node
                return false
            }
            if (layoutNode.isLayingOut) {
   
                // requestMeasure is currently laying out and it is incorrect to request remeasure
                // now, let's postpone it.
                layoutNode.markRemeasureRequested()
                postponedMeasureRequests.add(layoutNode)
                consistencyChecker?.assertConsistent()
                return false
            }

            // find root of layout request:
            var layout = layoutNode
            while (layout.affectsParentSize && layout.parent != null) {
   
                val parent = layout.parent!!
                if (parent.isMeasuring || parent.isLayingOut) {
   
                    if (!layout.needsRemeasure) {
   
                        layout.markRemeasureRequested()
                        // parent is currently measuring and we set needsRemeasure to true so if
                        // the parent didn't yet try to measure the node it will remeasure it.
                        // if the parent didn't plan to measure during this pass then needsRemeasure
                        // stay 'true' and we will manually call 'onRequestMeasure' for all
                        // the not-measured nodes in 'postponedMeasureRequests'.
                        postponedMeasureRequests.add(layout)
                    }
                    consistencyChecker?.assertConsistent()
                    return false
                } else {
   
                    layout.markRemeasureRequested()
                    if (parent.needsRemeasure) {
   
                        // don't need to do anything else since the parent is already scheduled
                        // for a remeasuring
                        consistencyChecker?.assertConsistent()
                        return false
                    }
                    layout = parent
                }
            }
            layout.markRemeasureRequested()

            requestRelayout(layout.parent ?: layout)
        }
    }

虽然在往上传递布局请求时候会有affectsParentSize判断,但是这个属性赋值代码如下

        // The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
        affectsParentSize = parent != null && parent.isMeasuring == true

经过分析代码发现这个属性只是简单判断父亲有没有正在测量布局,并不是重布局边界,并且我定义了一个固定大小的Text当改变其属性时,依然会将测量请求传递至rootview验证了我的结论(这里我不是非常确定,只是基于我的代码和所看到的进行分析)

@Composeable注解实现细节

接下来我们再来看下@Composeable注解到底做了啥,这部分代码不好直接查看,因为他是基于koltin注解去动态生成的,我在studio中并没有直接找到生成的相关代码,我是采用这种方法去查看的,先编译出一个apk 然后将其中的classes.dex文件进行反编译成jar文件,再将jar文件引入任意一个安卓工程中,即可查看相关代码
我们先来看下Layout所对应的代码,这是compose布局的基础类,如Column都是基于它实现,它对应的原函数如下

/**
 * [Layout] is the main core component for layout. It can be used to measure and position
 * zero or more children.
 *
 * Intrinsic measurement blocks define the intrinsic sizes of the current layout. These
 * can be queried by the parent in order to understand, in specific cases, what constraints
 * should the layout be measured with:
 * - [minIntrinsicWidthMeasureBlock] defines the minimum width this layout can take, given
 *   a specific height, such that the content of the layout will be painted correctly
 * - [minIntrinsicHeightMeasureBlock] defines the minimum height this layout can take, given
 *   a specific width, such that the content of the layout will be painted correctly
 * - [maxIntrinsicWidthMeasureBlock] defines the minimum width such that increasing it further
 *   will not decrease the minimum intrinsic height
 * - [maxIntrinsicHeightMeasureBlock] defines the minimum height such that increasing it further
 *   will not decrease the minimum intrinsic width
 *
 * For a composable able to define its content according to the incoming constraints,
 * see [WithConstraints].
 *
 * Example usage:
 * @sample androidx.ui.core.samples.LayoutWithProvidedIntrinsicsUsage
 *
 * @param children The children composable to be laid out.
 * @param modifier Modifiers to be applied to the layout.
 * @param minIntrinsicWidthMeasureBlock The minimum intrinsic width of the layout.
 * @param minIntrinsicHeightMeasureBlock The minimum intrinsic height of the layout.
 * @param maxIntrinsicWidthMeasureBlock The maximum intrinsic width of the layout.
 * @param maxIntrinsicHeightMeasureBlock The maximum intrinsic height of the layout.
 * @param measureBlock The block defining the measurement and positioning of the layout.
 *
 * @see Layout
 * @see WithConstraints
 */
@Composable
/*inline*/ fun Layout(
    /*crossinline*/
    children: @Composable () -> Unit,
    /*crossinline*/
    minIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,
    /*crossinline*/
    minIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,
    /*crossinline*/
    maxIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,
    /*crossinline*/
    maxIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,
    modifier: Modifier 
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值