首页 文章 精选 留言 我的

精选列表

搜索[深度集成],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 深度解析 Jetpack Compose 布局

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速让应用生动而精彩。Compose 使用全新的组件——可组合项 (Composable) 来布局界面,使用 修饰符 (Modifier) 来配置可组合项。 本文会为您讲解由可组合项和修饰符提供支持的组合布局模型,并深入探究其背后的工作原理以及它们的功能,让您更好地了解所用布局和修饰符的工作方式,和应如何以及在何时构建自定义布局,从而实现满足确切应用需求的设计。 如果您更喜欢通过视频了解本文内容,请 点击这里 观看。 布局模型 Compose 布局系统的目标是提供易于创建的布局,尤其是 自定义布局。这要求布局系统具备强大的功能,使开发者能创建应用所需的任何布局,并且让布局具备优异的性能。接下来,我们来看看 Compose 的布局模型 是如何实现这些目标的。 Jetpack Compose 可将状态转换为界面,这个过程分为三步: 组合、布局、绘制。组合阶段执行 可组合函数,这些函数可以生成界面,从而创建界面树。例如,下图中的 SearchResult 函数会生成对应的界面树: △ 可组合函数生成对应的界面树 可组合项中可以包含逻辑和控制流,因此可以根据不同的状态生成不同的界面树。在布局阶段,Compose 会遍历界面树,测量界面的各个部分,并将每个部分放置在屏幕 2D 空间中。也就是说,每个节点决定了其各自的宽度、高度以及 x 和 y 坐标。在绘制阶段,Compose 将再次遍历这棵界面树,并渲染所有元素。 本文将深入探讨布局阶段。布局阶段又细分为两个阶段: 测量和放置。这相当于 View 系统中的 onMeasure 和 onLayout。但在 Compose 中,这两个阶段会交叉进行,因此我们把它看成一个布局阶段。将界面树中每个节点布局的过程分为三步: 每个节点必须测量自身的所有子节点,再决定自身的尺寸,然后放置其子节点。如下例,单遍即可对整个界面树完成布局。 △ 布局过程 其过程简述如下: 测量根布局 Row; Row 测量它的第一个子节点 Image; 由于 Image 是一个不含子节点的叶子节点,它会测量自身尺寸并加以报告,还会返回有关如何放置其子节点的指令。Image 的叶子节点通常是空节点,但所有布局都会在设置其尺寸的同时返回这些放置指令; Row 测量它的第二个子节点 Column; Column 测量其子节点,首先测量第一个子节点 Text; Text 测量并报告其尺寸以及放置指令; Column 测量第二个子节点 Text; Text 测量并报告其尺寸以及放置指令; Column 测量完其子节点,可以决定其自身的尺寸和放置逻辑; Row 根据其所有子节点的测量结果决定其自身尺寸和放置指令。 测量完所有元素的尺寸后,将再次遍历界面树,并且会在放置阶段执行所有放置指令。 Layout 可组合项 我们已经了解这个过程涉及的步骤,接下来看一下它的实现方式。先看看组合阶段,我们采用 Row、Column、Text 等更高级别的可组合项来表示界面树,每个高级别的可组合项实际上都是由低级别的可组合项构建而成。以 Text 为例,可以发现它由若干更低级别的基础构建块组成,而这些可组合项都会包含一个或多个 Layout 可组合项。 △ 每个可组合项都包含一个或多个 Layout Layout 可组合项是 Compose 界面的基础构建块,它会生成 LayoutNode。在 Compose 中,界面树,或者说组合 (composition) 是一棵 LayoutNode 树。以下是 Layout 可组合项的函数签名: @Composable fun Layout( content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ) { … } △ Layout 可组合项的函数签名 其中,content 是可以容纳任何子可组合项的槽位,出于布局需要,content 中也会包含子 Layout。modifier 参数所指定的修饰符将应用于该布局,这在下文中会详细介绍。measurePolicy 参数是 MeasurePolicy 类型,它是一个函数式接口,指定了布局测量和放置项目的方式。一般情况下,如需实现自定义布局的行为,您要在代码中实现该函数式接口: @Composable fun MyCustomLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables: List<Measurable>, constraints: Constraints -> // TODO 测量和放置项目 } } △ 实现 MeasurePolicy 函数式接口 在 MyCustomLayout 可组合项中,我们调用 Layout 函数并以 Trailing Lambda 的形式提供 MeasurePolicy 作为参数,从而实现所需的 measure 函数。该函数接受一个 Constraints 对象来告知 Layout 它的尺寸限制。Constraints 是一个简单类,用于限制 Layout 的最大和最小宽度与高度: class Constraints { val minWidth: Int val maxWidth: Int val minHeight: Int val maxHeight: Int } △ Constraints measure 函数还会接受 List<Measurable> 作为参数,这表示的是传入的子元素。Measurable 类型会公开用于测量项目的函数。如前所述,布局每个元素需要三步: 每个元素必须测量其所有子元素,并以此判断自身尺寸,再放置其子元素。其代码实现如下: @Composable fun MyCustomLayout( content: @Composable () -> Unit, modifier: Modifier = Modifier ) { Layout( modifier = modifier, content = content ) { measurables: List<Measurable>, constraints: Constraints -> // placeables 是经过测量的子元素,它拥有自身的尺寸值 val placeables = measurables.map { measurable -> // 测量所有子元素,这里不编写任何自定义测量逻辑,只是简单地 // 调用 Measurable 的 measure 函数并传入 constraints measurable.measure(constraints) } val width = // 根据 placeables 计算得出 val height = // 根据 placeables 计算得出 // 报告所需的尺寸 layout (width, height) { placeables.foreach { placeable -> // 通过遍历将每个项目放置到最终的预期位置 placeable.place( x = … y = … ) } } } } △ 布局每个元素的代码示例 上述代码中使用了 Placeable 的 place 函数,它还有一个 placeRelative 函数可用于从右到左的语言设置中,当使用该函数时,它会自动对坐标进行水平镜像。 请注意,API 在设计上可阻止您尝试放置未经测量的元素,place 函数只适用于 Placeable,也就是 measure 函数的返回值。在 View 系统中,调用 onMeasure 以及 onLayout 的时机由您决定,而且调用顺序没有强制要求,但这会产生一些微妙的 bug 以及行为上的差异。 自定义布局示例 MyColumn 示例 △ Column Compose 提供一个 Column 组件用于纵向排布元素。为了理解这个组件背后的工作方式及其使用 Layout 可组合项的方式,我们来实现自己的一个 Column。暂且将其命名为 MyColumn,其实现代码如下: @Composable fun MyColumn( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Layout( modifier = modifier, content = content ) { measurables, constraints -> // 测量每个项目并将其转换为 Placeable val placeables = measurables.map { measurable -> measurable.measure(constraints) } // Column 的高度是所有项目所测得高度之和 val height = placeables.sumOf { it.height } // Column 的宽度则为内部所含最宽项目的宽度 val width = placeables.maxOf { it.width } // 报告所需的尺寸 layout (width, height) { // 通过跟踪 y 坐标放置每个项目 var y = 0 placeables.forEach { placeable -> placeable.placeRelative(x = 0, y = y) // 按照所放置项目的高度增加 y 坐标值 y += placeable.height } } } } △ 自定义 Column VerticalGrid 示例 △ VerticalGrid 我们再来看另一个示例: 构建常规网格。其部分代码实现如下: @Composable fun VerticalGrid( modifier: Modifier = Modifier, columns: Int = 2, content: @Composable () -> Unit ) { Layout( content = content, modifier = modifier ) { measurables, constraints -> val itemWidth = constraints.maxWidth / columns // 通过 copy 函数保留传递下来的高度约束,但设置确定的宽度约束 val itemConstraints = constraints.copy ( minWidth = itemWidth, maxWidth = itemWidth, ) // 使用这些约束测量每个项目并将其转换为 Placeable val placeables = measurables.map { it.measure(itemConstraints) } … } } △ 自定义 VerticalGrid 在该示例中,我们通过 copy 函数创建了新的约束。这种为子节点创建新约束的概念就是实现自定义测量逻辑的方式。创建不同约束来测量子节点的能力是此模型的关键,父节点与子节点之间并没有协商机制,父节点会以 Constraints 的形式传递其允许子节点的尺寸范围,只要子节点从该范围中选择了其尺寸,父节点必须接受并处理子节点。 这种设计的优点在于我们可以单遍测量整棵界面树,并且禁止执行多个测量循环。这是 View 系统中存在的问题,嵌套结构执行多遍测量过程可能会让叶子视图上的测量次数翻倍,Compose 的设计能够防止发生这种情况。实际上,如果您对某个项目进行两次测量,Compose 会抛出异常: △ 重复测量某个项目时 Compose 会抛出异常 布局动画示例 由于具备更强的性能保证,Compose 提供了新的可能性,例如为布局添加动画。Layout composable 不仅可以创建通用布局,还能创建出符合应用设计需求的专用布局。以 Jetsnack 应用中的自定义底部导航为例,在该设计中,如果某项目被选中,则显示标签;如果未被选中,则只显示图标。而且,设计还需要让项目的尺寸和位置根据当前选择状态执行动画。 △ Jetsnack 应用中的自定义底部导航 我们可以使用自定义布局来实现该设计,从而对布局变化的动画处理进行精确控制: @Composable fun BottomNavItem( icon: @Composable BoxScope.() -> Unit, text: @Composable BoxScope.() -> Unit, @FloatRange(from = 0.0, to = 1.0) animationProgress: Float ) { Layout( content = { // 将 icon 和 text 包裹在 Box 中 // 这种做法能让我们为每个项目设置 layoutId Box( modifier = Modifier.layoutId(“icon”) content = icon ) Box( modifier = Modifier.layoutId(“text”) content = text ) } ) { measurables, constraints -> // 通过 layoutId 识别对应的 Measurable,比依赖项目的顺序更可靠 val iconPlaceable = measurables.first {it.layoutId == “icon” }.measure(constraints) val textPlaceable = measurables.first {it.layoutId == “text” }.measure(constraints) // 将放置逻辑提取到另一个函数中以提高代码可读性 placeTextAndIcon( textPlaceable, iconPlaceable, constraints.maxWidth, constraints.maxHeight, animationProgress ) } } fun MeasureScope.placeTextAndIcon( textPlaceable: Placeable, iconPlaceable: Placeable, width: Int, height: Int, @FloatRange(from = 0.0, to = 1.0) animationProgress: Float ): MeasureResult { // 根据动画进度值放置文本和图标 val iconY = (height - iconPlaceable.height) / 2 val textY = (height - textPlaceable.height) / 2 val textWidth = textPlaceable.width * animationProgress val iconX = (width - textWidth - iconPlaceable.width) / 2 val textX = iconX + iconPlaceable.width return layout(width, height) { iconPlaceable.placeRelative(iconX.toInt(), iconY) if (animationProgress != 0f) { textPlaceable.placeRelative(textX.toInt(), textY) } } } △ 自定义底部导航 使用自定义布局的时机 希望以上示例能帮助您了解自定义布局的工作方式以及这些布局的应用理念。标准布局强大而灵活,但它们也需要适应很多用例。有时,若您知道具体的实现需求,使用自定义布局可能更加合适。 当您遇到以下场景时,我们推荐使用自定义布局: 难以通过标准布局实现的设计。虽然可以使用足够多的 Row 和 Column 构建大部分界面,但这种实现方式有时难以维护和升级; 需要非常精确地控制测量和放置逻辑; 需要实现布局动画。我们正在开发可对放置进行动画处理的新 API,未来可能不必自行编写布局就能实现; 需要完全控制性能。下文会详细介绍这一点。 修饰符 至此,我们了解了 Layout 可组合项以及构建自定义布局的方式。如果您使用 Compose 构建过界面,就会知道 修饰符 在布局、配置尺寸和位置方面发挥着重要作用。通过前文的示例可以看到,Layout 可组合项接受修饰符链作为参数。修饰符会装饰它们所附加的元素,可以在布局自身的测量和放置操作之前参与测量和放置。接下来我们来看看它的工作原理。 修饰符分很多不同的类型,可以影响不同的行为,例如绘制修饰符 (DrawModifier)、指针输入修饰符 (PointerInputModifier) 以及焦点修饰符 (FocusModifier)。本文我们将重点介绍布局修饰符 (LayoutModifier),该修饰符提供了一个 measure 方法,该方法的作用与 Layout 可组合项基本相同,不同之处在于,它只作用于单个 Measurable 而不是 List<Measurable>,这是因为修饰符的应用对象是单个项目。在 measure 方法中,修饰符可以修改约束或者实现自定义放置逻辑,就像布局一样。这表示您并不总是需要编写自定义布局,如果只想对单个项目执行操作,则可以改用修饰符。 以 padding 修饰符为例,该工厂函数以修饰符链为基础,创建能够捕获所需 padding 值的 PaddingModifier 对象。 fun Modifier.padding(all: Dp) = this.then(PaddingModifier( start = all, top = all, end = all, bottom = all ) ) private class PaddingModifier( val start: Dp = 0.dp, val top: Dp = 0.dp, val end: Dp = 0.dp, val bottom: Dp = 0.dp ) : LayoutModifier { override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val horizontal = start.roundToPx() + end.roundToPx() val vertical = top.roundToPx() + bottom.roundToPx() // 按 padding 尺寸收缩外部约束来修改测量 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { // 按所需的 padding 执行偏移以放置内容 placeable.placeRelative(start.roundToPx(), top.roundToPx()) } } } △ padding 修饰符的实现 除了通过上例中的方式覆写 measure 方法实现测量,您也可以使用 Modifier.layout,在无需创建自定义布局的情况下直接通过修饰符链向任意可组合项添加自定义测量和放置逻辑,如下所示: Box(Modifier .background(Color.Gray) .layout { measurable, constraints -> // 通过修饰符在竖直方向添加 50 像素 padding 的示例 val padding = 50 val placeable = measurable.measure(constraints.offset(vertical = -padding)) layout(placeable.width, placeable.height + padding) { placeable.placeRelative(0, padding) } } ) { Box(Modifier.fillMaxSize().background(Color.DarkGray)) } △ 使用 Modifier.layout 实现布局 虽然 Layout 接受单个 Modifier 参数,该参数会建立一个按顺序应用的修饰符链。我们通过示例来了解它与布局模型的交互方式。我们将分析下图修饰符的效果及其工作原理: △ 修饰符链的效果示例 首先,我们为 Box 设置尺寸并将其绘制出来,但这个 Box 放置在了父布局的左上角,我们可以使用 wrapContentSize 修饰符将 Box 居中放置。wrapContentSize 允许内容测量其所需尺寸,然后使用 align 参数放置内容,align 参数的默认值为 Center,因此可以省略这个参数。但我们发现,Box 还是在左上角。这是因为大多数布局都会根据其内容自适应调整尺寸,我们需要让测量尺寸占据整个空间,以便让 Box 在空间内居中。因此,我们在 wrapContentSize 前面添加 fillMaxSize 布局修饰符来实现这个效果。 △ 修饰符链的应用过程 我们来看一下这些修饰符是如何实现此效果的。您可以借助下图动画来辅助理解该过程: △ 修饰符链的工作原理 假设这个 Box 要放入最大尺寸为 200*300 像素的容器内,容器会将相应的约束传入修饰符链的第一个修饰符中。fillMaxSize 实际上会创建一组新约束,并设置最大和最小宽度与高度,使之等于传入的最大宽度与高度以便填充到最大值,在本例中是 200*300 像素。这些约束沿着修饰符链传递以测量下一个元素,wrapContentSize 修饰符会接受这些参数,它会创建新的约束来放宽对传入约束的限制,从而让内容测量其所需尺寸,也就是宽 0-200,高 0-300。这看起来只像是对 fillMax 步骤的反操作,但请注意,我们是使用这个修饰符实现项目居中的效果,而不是重设项目的尺寸。这些约束沿着修饰符链传递到 size 修饰符,该修饰符创建具体尺寸的约束来测量项目,指定尺寸应该正好是 50*50。最后,这些约束传递到 Box 的布局,它执行测量并将解析得到的尺寸 (50*50) 返回到修饰符链,size 修饰符因此也将其尺寸解析为 50*50,并据此创建放置指令。然后 wrapContent 解析其大小并创建放置指令以居中放置内容。因为 wrapContent 修饰符知道其尺寸为 200*300,而下一个元素的尺寸为 50*50,所以使用居中对齐创建放置指令,以便将内容居中放置。最后,fillMaxSize 解析其尺寸并执行放置操作。 修饰符链的执行方式与布局树的工作方式非常相像,差异在于每个修饰符只有一个子节点,也就是链中的下一个元素。约束会向下传递,以便后续元素用其测量自身尺寸,然后返回解析得到的尺寸,并创建放置指令。该示例也说明了 修饰符顺序的重要性。通过使用修饰符对功能进行组合,您可以很轻松地将不同的测量和布局策略组合在一起。 高级功能 接下来将介绍布局模型的一些高级功能,虽然您不一定总是需要这些功能,但它们能够帮助您构建更高级的功能。 固有特性测量 (Intrinsic Measurement) 前文提到过,Compose 使用单遍布局系统。这个说法并不完全正确,布局并不总是能通过单遍操作就得以完成,有时我们也需要了解有关子节点尺寸的信息才能最终确定约束。 以弹出式菜单为例。假设有一个包含五个菜单项的 Column,如下图所示,它的显示基本上是正常的,但是可以看到,每个菜单项的尺寸却不相同。 △ 菜单项的尺寸不相同 我们很容易想到,让每个菜单项都占用允许的最大尺寸即可: △ 每个菜单项都占有允许的最大尺寸 但这么做也没能完全解决问题,因为菜单窗口会扩大到其最大尺寸。有效的解决方法是使用最大固有宽度来确定尺寸: △ 使用最大固有宽度来确定尺寸 这里确定了 Column 会尽力为每个子节点提供所需的空间,对 Text 而言,其宽度是单行渲染全部文本所需的宽度。在确定固有尺寸后,将使用这些值设置 Column 的尺寸,然后,子节点就可以填充 Column 的宽度了。 如果使用最小值而非最大值,又会发生什么呢? △ 使用最小固有宽度来确定尺寸 它将确定 Column 会使用子节点的最小尺寸,而 Text 的最小固有宽度是每行一个词时的宽度。因此,我们最后得到一个按词换行的菜单。 如需详细了解固有特性测量,请参阅 Jetpack Compose 中的布局 Codelab 中的 "固有特性" 部分。 ParentData 到目前为止,我们看到的修饰符都是通用修饰符,也就是说,它们可以应用于任何可组合项。有时,您的布局提供的一些行为可能需要从子节点获得一些信息,这便要用到 ParentDataModifier。 我们回到前面那个在父节点中居中放置蓝色 Box 的示例。这一次,我们将这个 Box 放在另一个 Box 中。Box 中的内容在一个称为 BoxScope 的接收器作用域内排布。BoxScope 定义了只在 Box 内可用的修饰符,它提供了一个名为 Align 的修饰符。这个修饰符刚好能够提供我们要应用到蓝色 Box 的功能。因此,如果我们知道蓝色 Box 位于另一个 Box 内,就可以改用 Align 修饰符来定位它。 △ 在 BoxScope 中可以改用 Align 修饰符来定位内容 Align 是一个 ParentDataModifier 而不是我们之前看到的那种布局修饰符,因为它只是向其父节点传递一些信息,所以如果不在 Box 中,该修饰符便不可用。它包含的信息将提供给父 Box,以供其设置子布局。 您也可以为自己的自定义布局编写 ParentDataModifier,从而允许子节点向父节点告知一些信息,以供父节点在布局时使用。 对齐线 (Alignment Lines) 我们可以使用对齐线根据布局顶部、底部或中心以外的标准来设置对齐。最常用的 对齐线 是文本基线。假设需要实现这样一个设计: △ 需要实现设计图中的图标和文本对齐 我们很自然就能想到这样来实现它: Row { Icon(modifier = Modifier .size(10. dp) .align(Alignment.CenterVertically) ) Text(modifier = Modifier .padding(start = 8.dp) .align(Alignment.CenterVertically) ) } △ 有问题的对齐实现 仔细观察,会发现图标并没有像设计稿那样对齐在文本的基线上。 △ 图标和文本居中对齐,图标底部没有落在文本基线上 我们可以通过以下代码进行修复: Row { Icon(modifier = Modifier .size(10. dp) .alignBy { it.measuredHeight } ) Text(modifier = Modifier .padding(start = 8.dp) .alignByBaseline() ) } △ 正确的对齐实现 首先,对 Text 使用 alignByBaseline 修饰符。而图标既没有基线,也没有其他对齐线,我们可以使用 alignBy 修饰符让图标对齐到我们需要的任何位置。在本例中,我们知道图标的底部是对齐的目标位置,因此将图标的底部进行对齐。最终便实现了期望的效果: △ 图标底部与文本基线完美对齐 由于对齐功能会穿过父节点,因此,处理嵌套对齐时,只需设置父节点的对齐线,它会从子节点获取相应的值。如下例所示: △ 未设置对齐的嵌套布局 △ 通过父节点设置对齐线 您甚至可以在自定义布局中创建自己的自定义对齐,从而允许其他可组合项对齐到它。 BoxWithConstraints BoxWithConstraints 是一个功能强大且很实用的布局。在组合中,我们可以根据条件使用逻辑和控制流来选择要显示的内容,但是,有时候可能希望根据可用空间的大小来决定布局内容。 从前文中我们知道,尺寸信息直到布局阶段才可用,也就是说,这些信息一般无法在组合阶段用来决定要显示的内容。此时 BoxWithConstraints 便派上用场了,它与 Box 类似,但它将内容的组合推迟到布局阶段,此时布局信息已经可用了。BoxWithConstraints 中的内容在接收器作用域内排布,布局阶段确定的约束将通过该作用域公开为像素值或 DP 值。 @Composable fun BoxWithConstraints( ... content: @Composable BoxWithConstraintsScope.() -> Unit ) // BoxWithConstraintsScope 公开布局阶段确定的约束 interface BoxWithConstraintsScope : BoxScope { val constraints: Constraints val minWidth: Dp val maxWidth: Dp val minHeight: Dp val maxHeight: Dp } △ BoxWithConstraints 和 BoxWithConstraintsScope 它内部的内容可以使用这些约束来选择要组合的内容。例如,根据最大宽度选择不同的呈现方式: @Composable fun MyApp(...) { BoxWithConstraints() { // this: BoxWithConstraintsScope when { maxWidth < 400.dp -> CompactLayout() maxWidth < 800.dp -> MediumLayout() else -> LargeLayout() } } } △ 在 BoxWithConstraintsScope 中根据最大宽度选择不同的布局 性能 我们介绍了单遍布局模型如何防止在测量或放置方面花费过多时间,也演示了布局阶段两个不同的子阶段: 测量和放置。现在,我们将介绍性能相关的内容。 尽量避免重组 单遍布局模型的设计效果是,任何只影响项目的放置而不影响测量的修改都可以单独执行。以 Jetsnack 为例: △ Jetsnack 应用中产品详情页的协调滚动效果 这个产品详情页包含协调滚动效果,页面上的一些元素根据滚动操作进行移动或缩放。请注意标题区域,这个区域会随着页面内容而滚动,最后固定在屏幕的顶部。 @Composable fun SnackDetail(...) { Box { val scroll = rememberScrollState(0) Body(scroll) Title(scroll = scroll.value) ... } } @Composable fun Body(scroll: ScrollState) { Column(modifier = Modifier.verticalScroll(scroll)) { … } } △ 详情页的大致实现 为了实现此效果,我们将不同元素作为独立的可组合项叠放在一个 Box 中,提取滚动状态并将其传入 Body 组件。Body 会使用滚动状态进行设置以使内容能够垂直滚动。在 Title 等其他组件中可以观察滚动位置,而我们的观察方式会对性能产生影响。例如,使用最直接的实现,简单地使用滚动值对内容进行偏移: @Composable fun Title(scroll: Int) { Column( modifier = Modifier.offset(scroll) ) { … } } △ 简单地使用滚动值偏移 Title 的内容 这种方法的问题是,滚动是一个可观察的状态值,读取该值所处的作用域规定了状态发生变化时 Compose 需要重新执行的操作。在此示例中,我们要读取组合中的滚动偏移值,然后使用它来创建偏移修饰符。只要滚动偏移值发生变化,Title 组件都需要重新组合,也就需要创建并执行新的偏移修饰符。由于滚动状态是从组合中读取的,任何更改都会导致重组,在重组时,还需要进行布局和绘制这两个后续阶段。 不过,我们不是要更改显示的内容,而是更改内容的位置。我们还可以进一步提高效率,通过修改一下实现,不再接受原始滚动位置,而是传递一个可以提供滚动位置的函数: @Composable fun Title(scrollProvider: () -> Int) { Column( modifier = Modifier.offset { val scroll = scrollProvider() val offset = (maxOffset - scroll).coerceAtLeast(minOffset) IntOffset(x = 0, y = offset) } ) { … } } △ 使用提供滚动位置的函数代替原始滚动位置 这时,我们可以在不同的时间只调用此 Lambda 函数并读取滚动状态。这里使用了 offset 修饰符,它接受能提供偏移值的 Lambda 函数作为参数。这意味着在滚动发生变化时,不需要重新创建修饰符,只在放置阶段才会读取滚动状态的值。所以,当滚动状态变化时我们只需要执行放置和绘制操作,不需要重组或测量,因此能够提高性能。 再回到底部导航的示例,它存在同样的问题,我们可以用相同方法加以修正: @Composable fun BottomNavItem( icon: @Composable BoxScope.() -> Unit, text: @Composable BoxScope.() -> Unit, animationProgress: () -> Float ) { … val progress = animationProgress() val textWidth = textPlaceable.width * progress val iconX = (width - textWidth - iconPlaceable.width) / 2 val textX = iconX + iconPlaceable.width return layout(width, height) { iconPlaceable.placeRelative(iconX.toInt(), iconY) if (animationProgress != 0f) { textPlaceable.placeRelative(textX.toInt(), textY) } } } △ 修正后的底部导航 我们使用了能提供当前动画进度的函数作为参数,因此不需要重组,只执行布局即可。 您需要掌握一个原则: 只要可组合项或修饰符的参数可能频繁发生更改,都应当保持谨慎,因为这种情况可能导致过度组合。只有在更改显示内容时,才需要重组,更改显示位置或显示方式则不需要这么做。 BoxWithConstraints 可以根据布局执行组合,是因为它会在布局阶段启动子组合。出于性能考虑,我们希望尽量避免在布局期间执行组合。因此,相较于 BoxWithConstraints,我们倾向于使用会根据尺寸更改的布局。当信息类型随尺寸更改时才使用 BoxWithConstraints。 提高布局性能 有时候,布局不需要测量其所有子节点便可获知自身大小。举个例子,有如下构成的卡片: △ 布局卡片示例 图标和标题构成标题栏,剩下的是正文。已知图标大小为固定值,标题高度与图标高度相同。测量卡片时,就只需要测量正文,它的约束就是布局高度减去 48 DP,卡片的高度则为正文的高度加上 48 DP。 △ 测量过程只测量正文尺寸 系统识别出只测量了正文,因此它是决定布局尺寸的唯一重要子节点,图标和文本仍然需要测量,但可以在放置过程中执行。 △ 放置过程测量图标和文本 假设标题是 "Layout",当标题发生变化时,系统不必重新执行布局的测量操作,因此不会重新测量正文,从而省去不必要的工作。 △ 标题发生变化时不必重新测量 总结 在本文中,我们介绍了自定义布局的实现过程,还使用修饰符构建和合并布局行为,进一步降低了满足确切功能需求的难度。此外,还介绍了布局系统的一些高级功能,例如跨嵌套层次结构的自定义对齐,为自有布局创建自定义 ParentDataModifier,支持自动从右向左设置,以及将组合操作推迟到布局信息已知时,等等。我们还了解如何执行单遍布局模型,如何跳过重新测量以使其只执行重新放置操作的方法,熟练使用这些方法,您将能编写出通过手势进行动画处理的高性能布局逻辑。 对布局系统的理解能够帮助您构建满足确切设计需求的布局,从而创建用户喜爱的优秀应用。如需了解更多,请查阅以下列出的资源: Jetpack Compose 使用入门文档 Jetpack Compose 学习路线图 Jetpack Compose 相关示例 欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

优秀的个人博客,低调大师

深度剖析数仓CN增量备份技术

摘要:为了解决Roach的性能问题,提出了CN增量备份手段,从而达到进一步优化RPO目的。 本文分享自华为云社区《GaussDB(DWS)备份容灾之CN增量备份》,作者: zxy_db 。 1. 摘要 在数据量增大时,如果CN每次都做全量备份,则会导致每次的备份数据量很大,不仅会降低备份的性能,也从造成备份集恢复性能的降低。如果改成CN增量备份,则备份集只会备份差异数据,这样不仅会使得备份数据量变小,而且也会提升备份集恢复的性能。 2. CN备份原理 对于主备集群CN备份与恢复的过程,如下图所示: 在备份过程中,只备份主CN的数据,且只发送到备集群对应的主CN所在的节点上。 在恢复过程中,非主CN节点从主CN节点上拷贝rch文件,然后再将备份数据的rch文件恢复到实例目录。 CN备份同集群备份一样,先进行行存备份,后进行列存备份。 对于行存备份过程,首先是准备列表,然后备份文件。 准备列表主要分为3个步骤:第一步是获取CN备份类型,第二步是根据备份类型,决定LSN区间,第三步是根据LSN区间,准备备份列表(全量备份列表和增量备份列表)。 对于列存备份过程,同上述行存。 行存和列存区别在于增量备份LSN区间的取法: 行存文件来说,增量是上次startLSN到本次startLSN之间 列存文件来说,增量是上次barrierLSN到本次barrierLSN之间 3. CN备份判断逻辑 首先,CN增量需要有一个基础备份,因此,集群在做全量备份时,CN仍然做全量备份。 其次,集群在两次增量备份过程中,CN发生删除和加回后,新增的CN需要做全量备份。 对于支持异构的情况下,如果ID最小的CN发生变化,同样需要对CN做一次全量备份。 整体的备份逻辑如下图所示。 3.为了实现上述判断逻辑,通过创建标志文件backup_label.old来控制CN做全量备份还是增量备份。backup_label.old在Python侧创建。即在Python侧,调用gs_roach备份前,在最小的CN上,即要进行备份的CN上,创建backup_label.old文件。根据backup_label.old的修改时间和priorBackupKey转化的时间,判断CN做增量备份还是全量备份。流程图如下图所示。如右半部分所示,如果backup_label.old文件的修改时间比prriorBackupKey转换获得的时间大,则进行全量备份。否则,进行增量备份。 4. CN备份技术应用实测 4.1 CN删除和加回后做全量备份 初始状态,ecs-env-3038节点上的CN实例是最小CN编号,即主CN 第一步:修改XML配置文件xml,将主CN对应主机上的cooNum值从1改为0 第二步:使用gs_om工具执行删除CN操作 gs_om -t managecn -m delete -X /data1/xml/3_node_3.xml 第三步:将要加回CN对应主机上的cooNum值从0改为1 第四步:使用gs_om工具执行加回CN操作 gs_om -t managecn -m add -X /data1/xml/3_node_3.xml 删除和加回后,主CN的变化情况: 主CN由节点ecs-env-3038变为节点ecs-env-2998. 此时查看日志可以发现,由于CN发生了增删,集群做增量备份时,CN做全量备份。 4.2 备份集大小变化 第一步:拉起容灾,CN增量备份阶段停止容灾; 第二步:创建大量数据库和空表; 第三步:连续执行增量备份,增量备份中途不插入任何数据。 如下图所示,不增加数据,增量备份集大小小于全量备份集大小 5. 技术总结 本文主要从技术价值、应用场景、技术原理、技术实测展示几个维度对GaussDB(DWS) CN增量备份技术进行了剖析,可以看到增量备份是对已有全量备份恢复的一个有效的增强,可以节省宝贵的备份存储空间和cpu资源,同时达到进一步优化RPO目的,因此该技术拥有较为广阔的前景和深远的意义。 点击关注,第一时间了解华为云新鲜技术~​

优秀的个人博客,低调大师

用几张图深度剖析Java内存模型

听说微信搜索《Java鱼仔》会变更强哦! 本文收录于github和gitee ,里面有我完整的Java系列文章,学习或面试都可以看看哦 (一)基础概念介绍 首先看一张图:下图是Java虚拟机运行时数据区,JVM的内存模型可以分为方法区、虚拟机栈、本地方法栈、堆和程序计数器。 首先还是介绍一下基本概念 程序计数器: 程序计数器的作用可以看成是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变计数器的值来选择下一条需要执行的字节码的指令。java虚拟机的多线程是通过线程轮流切换来分配处理器执行时间的方式实现的,为了线程切换之后能恢复到正确的执行位置,每个线程就需要一个独立的程序计数器。 java虚拟机栈: java虚拟机栈线程私有,每个方法被执行的同时都会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息。 本地方法栈: 本地方法栈的功能和虚拟机栈类似,本地方法栈为虚拟机用到的Native方法服务,一个Native Method就是一个java调用非java代码的接口,本地方法栈也会抛出StackOverFlow和OutOfMemoryError异常 java堆 java堆可以说是java虚拟机中所管理的内存最大的一块,java堆被所有线程共享,虚拟机启动的时候创建。java堆中存放的对象实例的数组。几乎所有的对象实例以及数组都在堆上分配,java堆也是垃圾回收器管理的主要区域,java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,这和磁盘空间很相似。当一个堆无法再扩展时,会抛出OutOfMemoryError异常。 方法区: 方法区也是线程共享的内存区域,用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,java虚拟机对方法区的限制十分宽松,和java堆一样不需要连续的内存外,还可以选择不实现垃圾回收。 (二)Java虚拟机栈与程序计数器 虚拟机栈中保存的主要内容是栈帧,每次方法调用就会使得一个栈帧被压入到虚拟机栈。比如上一篇文章讲JVM字节码的时候,我们用到了这样一段代码: public class Main { public static int calculate(){ int a=1; int b=2; int c=(a+b)*10; return c; } public static void main(String[] args) { System.out.println(calculate()); } } 在JVM虚拟机栈中,结构就是这样的: 局部变量表存放了编译器可知的八个基本数据类型、对象引用、和returnAddress类型(指向了一条字节码指令的地址)。 操作数栈主要用来进行一系列出入栈的数值操作。 有关局部变量表、操作数栈、程序计数器更加详细的应用可以看我的前一篇文章:两张图让你快速读懂JVM字节码指令 动态链接:动态链接是值在程序运行期间将符号引用转换为直接引用。在一段代码中,类名、常量名、修饰符、对象名等都是符号引用,而如何通过符号找到具体的引用就需要动态链接做一层转换,将符号引用转换为直接引用。比如:JVM就能通过对象名就链接到堆中真实的对象。 动态链接的工作内容和类加载过程中解析这一步是一样的,只不过解析是将一些静态方法或变量(比如main()方法、static变量)替换为指向数据所存内存的直接引用。 方法出口:当一个方法执行之后,返回的方式可能是正常执行结束返回,也可能是抛出异常的返回。无论采用哪种方式,在方法退出之后都需要回到方法调用的位置,因此这些信息就会被保存到方法出口中。 JVM模式每个线程的虚拟机栈大小是1M,也可以通过-Xss调整虚拟机栈的大小。 (三)方法区 方法区是线程共享的内存区域,用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。关于方法区的概念,很多人其实都是浑的。方法区、永久代、元空间之间的区别大家都知道吗? 其实方法区是JVM规范中的一个概念,在不同的Java虚拟机以及不同版本的Java虚拟机中,方法区都有各自的实现。以最流行的HotSpot 虚拟机为例,在JDK1.8之前,方法区的实现叫做永久代,放在JVM内存之中。到了JDK1.8,方法区的实现变成了元空间,放在直接内存中。 (四)本地方法栈 本地方法栈的作用是在Java中调用非Java代码,他的功能和虚拟机栈类似。本地方法栈的存在应该算是历史遗留问题,Java刚出来的时候C、C++是当时绝对的统治者,因此在Java中免不了会去调用这一类代码,也因此需要本地方法栈。 (五)堆 堆是JVM虚拟机中最核心的部分了,绝大部分的垃圾回收都在堆中完成。 JVM堆分为新生代和老年代,默认比例为1:2,其中新生代又分为Eden区和两个survivor区,比例为8:1:1。关于堆的大部分内容我已经放到垃圾回收部分进行讲解。 (六)总结 当真正去深入理解JVM虚拟机的时候,会发现它并不像想象中那么难,一个JVM虚拟机实际上就包含了这几样东西,最后再画张图描述一下常用JVM参数针对的位置。

优秀的个人博客,低调大师

ZAO 背后的深度学习算法原理浅析

ZAO最近火爆,成为现象级产品之一,引起大家的广泛关注,ATA上面已经有同学做了一些说明分析,链接如下: https://www.atatech.org/articles/148375?spm=ata.13269325.0.0.27ad49fa0Vr2gG 上面文章介绍了ZAO是基于deep fake算法演变出来的一种产品,并提供了deepFake lab的下载地址,计算机硬件的要求等等。本文从更底层的算法角度出发,带大家深入到算法本质,去理解ZAO究竟是如何基于GAN来进行换脸的。 首先,我们给出一张换脸的整体流程图: 图片来源:Exposing DeepFake Videos By Detecting FaceWarping Artifacts 上图展示了基于deepFake换脸算法的一般流程,首先对于输入图片(a)原图做人脸检测(b),检测出人脸后进行关键点检测(c)。之后(c)通过变换矩阵(d)来实现人脸摆正,之后将摆正后的人脸进入DeepFake(GAN/CycleGAN)来实现人脸替换,之后将替换后的人脸(g)通过变换矩阵的反变换来做关键点对齐,最后替换回原图进行融合最终得到(i)和(h)。 这里我们给出的是图像上人脸替换的一般流程,那对于短视频而言,就需要先对视频进行截帧,然后逐帧进行人脸替换,在视频帧替换过程中要有人脸识别的网络来保证替换的对象保持统一性(具体理解就是比如我们要替换一段视频中小燕子的脸,那就要识别出检测的人脸是不是小燕子的,不能将紫薇的人脸也替换了),当然由于是视频逐帧的替换,那么在其中为了保证视频随时间前后帧替换的人脸的自然和连贯性,就需要对前后帧的人脸进行转移平滑操作,从而保证较强的视觉效果。 以上就是图像换脸,视频换脸的通用流程,当然对于ZAO而言,我们发现它的换脸效果要好于我们一般的换脸算法,尤其是在头部旋转的(低头,回头,仰头)上面,效果很是不错,所以我们有理由相信,ZAO的算法内部应该是使用3D人脸关键点的检测,这样在替换的过程中就会换的更为自然。 好的,现在我们了解了流程,下面我们更加细节的介绍上面说的DeepFake(GAN/CycleGAN)的算法工作原理。为了简化大家对于GAN/CycleGAN的理解,我们同样以图的方式展现: 首先,上图表示了最简单的人脸替换网络,对于输出人脸(左边),通过神经网络编码得到中间状态(往往是一个向量或者很小的图像),之后再进入解码器还原得到重建的人脸(右边)。我们注意,中间的编码态相当于保存的人脸的全部信息。在上图我们并没有做人脸替换的相关操作,即A脸编码后解码的还是A脸,B脸编码后解码的还是B脸。 下面,如果我们将B脸编码的向量用A脸的解码去解,会发生什么呢?是的,B的脸会出现在原本A的脸的位置,但面部的表情和一些细节会保留A的。这样就实现了换脸。 从上图还有一点需要注意,因为编码的可替换要求,我们必须让所有脸的编码器保持一致性,也就是所有替换前的人脸用统一的编码器去编码(上图统一的红色编码器),但对于每个不同的人脸要实用不同的解码器去解码(上图不同的蓝色和绿色解码器),这样才能完成换脸。 但是如果仅仅使用上面的算法结构,生成的人脸会比较假,可以看出相当明显的人为替换痕迹,而为了让替换发生的更为真实,CycleGan应运而生,还是简单的一张图去理解CycleGan的算法本质: 我们可以看出,说到底,CycleGan不过在换脸后生成的假脸和真脸的之间多增加了一个损失来减小两者的差距,同时让相较于之前的A-->B, CycleGan还同时实现了B-->A的生成和缩小差距,而这整个过程呈现了一个闭环,故而名为Cycle。 CycleGan的循环训练可以明显的缩小直接将B脸用A解码器解码所产生的不真实性。 当然,在真实场景中,在完成换脸后可能还需要一些后处理来保证结果更加平滑自然,比如在换脸边缘做一些模糊处理,在人脸区域做一些和原脸的风格迁移等等。而这些就是实现技术代差的关键技术了,我们今天只是浅析一下ZAO的算法,更深入的一些产品算法设计细节我们这里不做更深的探讨了。 最后面对ZAO,虽然他有着霸王条款,单但从技术角度看,我认为ZAO确实做的很好,技术上值得我们学习,但换脸本身是否符合道德,究竟是不是一件有意义有价值的事情,还需要时间给出我们最终的答案。 感谢您阅读,希望对您有所帮助。 --溪鹤 原文链接 本文为阿里云原创内容,未经允许不得转载。

优秀的个人博客,低调大师

GPU虚拟机创建时间深度优化

桔妹导读:GPU虚拟机实例创建速度慢是公有云面临的普遍问题,由于通常情况下创建虚拟机属于低频操作而未引起业界的重视,实际生产中还是存在对GPU实例创建时间有苛刻要求的业务场景。本文将介绍滴滴云在解决该问题时的思路、方法、并展示最终的优化成果。 从公有云服务商那里购买过虚拟主机的资深用户,一般会发现这么个规律:创建一台CPU虚拟主机是比较快的,但是要创建一台包含GPU卡的虚拟主机通常需要等比较长的时间,整个创建过程短则数十秒钟,长则数分钟。对于绝大多少的用户来说,虚拟主机的创建时间长一点对他们影响并不大,因为创建虚拟机属于相对低频操作。但是也会有一些特定的用户由于其业务场景交互性比较强,会对虚拟主机的创建时间有相对苛刻的要求,因为过长的创建时间会导致其业务用户体验很差。本文将从虚拟化的角度来介绍GPU虚拟主机创建时间长背后的原因,以及相关的优化方法。 通过分析Libvirt, QEMU以及Guest 内的相关日志及对应的时间戳,可以获取GPU虚拟主机在创建过程中的耗时情况,这里我们主要关心几个关键的时间点: a) Libvirt 开始创建QEMU 进程;b) Libvirt 执行 Resume启动VCPU ; c) Guest kernel 打印第一条日志. 在本文中,我们把a和 b 之间的时间间隔称为QEMU初始化时间, 把b 和c 之间的时间间隔称为 BIOS执行时间。以下数据是在滴滴云的线上环境中采集到的创建一台包含8个CPU核虚拟机实例的相关数据: 从上面的数据可以看到,对于规格相同的虚拟机实例,带1块P40卡的GPU实例相比同规格的CPU实例在QEMU初始化及BIOS执行部分的时间都明显要长, 在带4块P40卡以及更大内存规格的场景下,需要的时间会进一步拉长。通过实验我们发现在主机配置和GPU卡型号确定的前提下,GPU实例的创建时间长短主要取决于两个因素:虚拟机的内存大小和GPU卡的数量。 为什么GPU实例的创建过程要比CPU实例的创建过程耗时长?多消耗的时间到底花在哪里?要搞清楚原因需要深入的分析,比较直观的办法就是通过perf采样来生成火焰图,以此来分析虚拟机在创建过程中的热点函数。下图是在滴滴云环境里抓取到的GPU虚拟机启动过程中QEMU进程的火焰图。 通过对代码调用关系的分析,可以得知热点发生在系统分配内存和对内存页面清零的过程中,是由QEMU中的vfio_dma_map函数在执行VFIO_IOMMU_MAP_DMA ioctl 系统调用所触发,该调用会Pin住所有分配给VM当做RAM使用的内存。在Pin 内存的过程中,如果虚拟内存对应的物理页面尚未分配,会先进行物理内存分配并对内存页面内容进行清零。在Linux kernel 中,对分配给应用程序的内存进行清零主要是基于安全方面的考虑,避免Host 内存中的内容泄漏给用户空间的应用程序。这里之所以要将内存Pin 住,目的是为了保证IOMMU IO页表和 host HVA->HPA 映射的一致性,否则Guest 内设备的DMA操作可能会访问到错误的内存页面。 VFIO DMA 映射处理慢可以在一定程度上解释为什么内存的大小和GPU卡的数量会影响到GPU实例的创建时间。虚拟机实例内存规格越大,需要映射和Pin住的内存量也就越大,相关处理的耗时和内存量成正比。另外GPU卡上通常会包含一块比较大的MMIO区域,对MMIO的映射也会耗费较多的时间,卡的数量越多,耗时就会越长。相比之下,CPU实例的创建过程没有VFIO DMA 映射的相关处理流程,因此会比较快。 针对以上的热点,有什么办法可以消除或者缓解呢?已经有业内的同行们提到过这个问题并给出了对应的解决方案,其思路是对分配给VM 用作RAM使用的内存区域做一个标记,在内核中跳过对标记的内存页面进行清零,而将清零的动作留给QEMU来做,在QEMU 中可以利用多线程以及更高效的指令进行清零动作,从而加速Pin内存的过程。该方案的缺陷主要有两点: 一是存在安全性风险,其他应用程序可以利用设定的标记来窥探host 内存中的信息;二是在VM实例的VCPU个数比较少的情况下,优化效果不是很好。 我们采用了另外一种方案,通过修改Host kernel的内存管理部分, 我们实现了一种对Host 上空闲物理内存提前进行清零的机制,清零动作可以在系统空闲的时候进行,当某个内存页面被清零后,将其对应的 struct page 进行标记,这样在需要对内存进行清零的时候,可以通过检查该标记来判断是否要执行清零动作,如果清零的标记已经被设置,就可以跳过清零的步骤。该方案避免了上述方案中的两个主要问题,同时还有其它方面的好处,主要包括以下几点:a.可以提高缺页异常处理效率,尤其是透明大页的缺页异常处理效率;b. 可以加速需要Pin内存及需要通过mlock 来锁住内存的应用场景,例如使用RDMA, QAT 硬件加速等场合;c. 可以加速内核中其他需要对内存进行清零的场景。相关补丁的RFC版本,我们已经提交到了Linux kernel 社区。 另一个加速Pin内存的有效方法是采用大页,通过开启透明大页可以显著减少缺页处理的调用次数并加速Pin内存的过程。下图展示了开启透明大页以及启用空闲内存预清零机制对GPU实例创创建时间的影响。 以上的数据表明,在开启透明大页以及空闲内存预清零功能后,可以显著的的优化QEMU的初始化时间,但是BIOS部分的耗时依然偏长。通过进一步的分析我们发现主要的时间消耗还是在VFIO 映射DMA的处理过程当中,主要有几个方面的原因:a. 映射DMA Pin内存需要逐页查询页表,开销较大;b. QEMU 存在对部分IOVA区域的反复映射及解除映射的操作。于是我们尝试在这两个方向上进行优化,通过采用批量处理的方法减少查询页表的开销,另外在QEMU中加入VFIO DMA映射区域的管理,有效的规避了效率低下的反复映射及解除映射操作,最终大幅度降低了VFIO DMA映射的时间消耗。 在解决完上述问题后我们并没有止步,对虚拟机实例创建过程中的可优化的其它地方,我们也做了相关的处理,例如关闭BIOS boot menu ,优化VFIO PCI 设备reset 的流程,去掉对GPU实例来说不必要的操作,最终将GPU实例创建过程中虚拟化部分的时间开销减少了90%以上,下面这张图展示了单卡小内存规格实例优化前后的耗时对比: 大内存规格和多GPU卡的效果更加显著,时间减少了95%以上,相关数据如下图: 经过上述的优化,目前在滴滴云上创建一个GPU实例的速度比优化前显著加快,甚至比优化前创建一个CPU实例的速度还要快,如果用户对GPU实例的创建速度有比较强的需求,欢迎到滴滴云上进行体验。 除此之外, 滴滴云GPU产品现已开通企业0元体验,请在滴滴技术公众号后台回复「GPU」立即获取。 团队介绍 ▬ 滴滴云平台事业群滴滴内核团队致力于为公司各种业务提供底层系统软件支撑,负责公司线上服务器Linux操作系统内核的研发并维护虚拟化相关的核心组件。团队针对各个业务的需求,在虚拟化、业务混部、资源隔离、系统性能优化等领域均有广泛深入的研究,在公有云、弹性云等业务线都有相关的产品。 作者介绍 ▬ 专注于系统虚拟化研究,负责解决滴滴云底层虚拟化相关技术问题。曾就职于Intel 开源软件中心虚拟化组,具备丰富的底层系统软件开发经验。 延伸阅读 ▬ 内容编辑 | Charlotte 联系我们 | DiDiTech@didiglobal.com 本文分享自微信公众号 - 滴滴技术(didi_tech)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册