[译] 格子拼贴 — 关于模块化的故事

插图来自 Virginia Poltrack前端

咱们为何以及如何进行模块化,模块化后会发生什么?

这篇文章深刻探讨了 Restitching Plaid 模块化部分。java

在这篇文章中,我将全面介绍如何将一个总体的、庞大的、普通的应用转化为一个模块化应用束。如下是咱们已取得的成果:android

  • 总体体积减小超过 60%
  • 极大地加强代码健壮性
  • 支持动态交付、按需打包代码

咱们作的全部事情,都不会影响用户体验。ios

Plaid 初印象

导航 Plaidgit

Plaid 是一个具备使人感到愉悦的 UI 的应用。它的主屏幕显示的新闻来自多个来源。 这些新闻被点击后展现详情,从而出现分屏效果。 该应用同时具备搜索功能和一个关于模块。基于这些已经存在的特征,咱们选择一些进行模块化。github

新闻来源(Designer News 和 Dribbble)成为了它本身拥有的动态功能模块。关于和搜索特征一样被模块化为动态功能。npm

动态功能容许在不直接于基础应用包含代码状况下提供代码。正由于此,经过连续步骤可实现按需下载功能。后端

接下来介绍 Plaid 结构

如许多安卓应用同样,Plaid 最初是做为普通应用构建的单一模块。它的安装体积仅 7MB 一下。然而许多数据并未在运行时用到。api

代码结构

从代码角度来看,Plaid 基于包从而有明确边界定义。但随大量代码库的出现,这些边界会被跨越且依赖会潜入其中。模块化要求咱们更加严格地限定这些边界,从而提升和改善代码分离。缓存

本地库

最大未用到的数据块来自 Bypass,一个咱们用来在 Plaid 呈现标记的库。它包括用于多核 CPU 体系架构的本地库,这些本地库最终在普通应用占大约 4MB 左右。应用束容许仅交付设备架构所需的库,将所需体积减小1MB左右。

可提取资源

许多应用使用栅格化资产。它们与密度有关且一般占应用文件体积很大一部分。应用可从配置应用中受益不浅,配置应用中每一个显示密度都被放在一个独立应用中,容许设备定制安装,也大大减小下载和体积。

Plaid 显示图形资源时,很大程度依赖于 vector drawables。因这些与密度无关且已保存许多文件,故此处数据节省对咱们并不是太有影响。

拼贴起来

在模块化中,咱们最初把 ./gradlew assemble 替换为 ./gradlew bundle。Gradle 如今将生成一个 Android App Bundle(aab),替换生成应用。一个安卓应用束需用到动态功能 Gradle 插件,咱们稍后介绍。

安卓应用束

相对单个应用,安卓应用束生成许多小的配置应用。这些应用可根据用户设备定制,从而在发送过程和磁盘上保存数据。应用束也是动态功能模块先决条件。

在 Google Play 上传应用束后,可生成配置应用。随着应用束成为开放规范,其它应用商店也可实现该交付机制。为 Google Play 生成并签署应用,应用必须注册到由 Google Play 签名的应用程序

优点

这种封装改变给咱们带来了什么?

Plaid 如今设备减小 60% 以上体积,等同大约 4MB 数据。

这意味每一位用户都能为其它应用预留更多空间。 同时下载时间也因文件大小缩小而改善。

无需修改任何一行代码便可实现这一大幅度改进。

实现模块化

咱们为实现模块化所选的方法:

  1. 将全部代码和资源块移动到核心模块中。
  2. 识别可模块化功能。
  3. 将相关代码和资源移动到功能模块中。

绿色:动态功能 | 深灰色:应用模块 | 浅灰色:库

上面图表向咱们展现了 Plaid 模块化现状:

  • 旁路模块 和外部 分享依赖 包含在核心模块当中
  • 应用 依赖于 核心模块
  • 动态功能模块依赖于 应用

应用模块

应用 模块基本上是现存的应用,被用来建立应用束且向咱们展现 Plaid。许多用来运行 Plaid 的代码不必必须包含在该模块中,而是可移至其它任何地方。

Plaid 的 核心模块

为开始重构,咱们将全部代码和资源都移动至一个 com.android.library 模块。进一步重构后,咱们的核心模块仅包含各个功能模块间共享所须要代码和资源。这将使得更加清晰地分离依赖项。

外部库

经过旁路模块将一个第三方依赖库包含在核心模块中。此外经过 gradle api 依赖关键字,将全部其它 gradle 依赖从 应用 移动至 核心模块

Gradle 依赖声明:api vs implementation_

经过 api 代替 implementation 可在整个程序中共享依赖项。这将减小每个功能模块体积大小,因本例 核心模块 中依赖项仅需包含在单一模块中。此外还使咱们的依赖关系更加易于维护,由于它们被声明在一个单一文件而非在多个 build.gradle 文件间传播。

动态功能模块

上面我提到了咱们识别的可被重构为 com.android.dynamic-feature 的模块。它们是:

:about
:designernews
:dribbble
:search
复制代码

动态功能介绍

一个动态功能模块本质上是一个 gradle 模块,可从基础应用模块被独立下载。它包含代码、资源、依赖,就如同其它 gradle 模块同样。虽然咱们还没在 Plaid 中使用动态交付,但咱们但愿未来可减小最初下载体积。

伟大的功能改革

将全部东西都移动至核心模块后,咱们将“关于”页面标记为具备最少依赖项的功能,故咱们将其重构为一个新的 关于 模块。这包括 Activties、Views、代码仅用于该功能的内容。一样,咱们把全部资源例如 drawables、strings 和动画移动至一个新模块。

咱们对每一个功能模块进行重复操做,有时须要分解依赖项。

最后,核心模块包含大部分共享代码和主要功能。因为主要功能仅显示于应用模块中,咱们把相关代码和资源移回 应用

功能结构剖析

编译后代码可在包中进行结构优化。强烈建议在将代码分解成不一样编译单元前,将代码移动至与功能对应包中。幸运的是咱们不用必须重构,由于 Plaid 已很好地对应了功能。

功能和核心模块以及各自体系结构层级

正如我提到的,Plaid 许多功能都经过新闻源提供。它们由远程和本地 data 资源、domainUI 这些层级组成。

数据源不但显示在主要功能提示中,也显示在与对应功能模块自己相关详情页中。域名层级在一个单一包中惟一。它必须分为两部分:一部分在应用中共享,另外一部分仅用在一个功能模块中。

可复用部分被保存在核心模块,其它全部内容都在各自功能模块。数据层和大部分域名层至少与其它一个模块共享,而且同时也保存在核心模块。

包变化

咱们还对包名进行了优化,从而反映新的模块化结构体系。 仅与 :dribbble 相关代码从 io.plaidapp 移动至 io.plaidapp.dribbble。经过各自新的模块名称,这一样运用于每个功能。

这意味着许多导包必须改变。

对资源进行模块化会产生一些问题,由于咱们必须使用限定名称消除生成的 R 类歧义。例如,导入本地布局视图会致使调用 R.id.library_image,而在核心模块相同文件中使用一个 drawable 会致使

io.plaidapp.core.R.drawable.avatar_placeholder
复制代码

咱们使用 Kotlin 导入别名特性减轻了这一点,它容许咱们以下导入核心 R 文件:

import io.plaidapp.core.R as coreR
复制代码

容许将呼叫站点缩短为

coreR.drawable.avatar_placeholder
复制代码

相较于每次都必须查看完整包名,这使得阅读代码变得简洁和灵活得多。

资源移动准备

资源不一样于代码,没有一个包结构。这使得经过功能划分它们变得异常困难。可是经过在你的代码中遵循一些约定,也何尝不可能。

经过 Plaid,文件在被用到的地方做为前缀。例如,资源仅用于以 dribbble_ 为前缀的 :dribbble

未来,一些包含多个模块资源的文件,例如 styles.xml 将在模块基础上进行结构化分组,而且每个属性同时也做为前缀。

举个例子:在单块应用中,strings.xml 包含了总体所用大部分字符串。 在一个模块化应用内中,每个功能模块仅包含对应模块自己字符串资源。 字符串在模块化前进行分组将更容易拆分文件。

像这样遵循约定,能够更快地、更容易地将资源转移至正确地方。这一样也有助于避免编译错误和运行时序错误。

过程挑战

同团队良好沟通,对使得一个重要的重构任务像这样易于管理而言,十分重要。传递计划变动并逐步实现这些变动将帮助咱们合并冲突,而且将阻塞降到最低。

善意提醒

本文前面依赖关系图表显示,动态功能模块了解应用模块。另外一方面,应用模块不能轻易地从动态功能模块访问代码。但他们包含必须在某一时间执行的代码。

应用对功能模块没足够了解时访问代码,这将没办法在 Intent(ACTION_VIEW, ActivityName::class.java) 方法中经过它们的类名启动活动。 有多种方式启动活动。咱们决定显示地指定组件名。

为实现它,咱们在核心模块开发了 AddressableActivity 接口。

/**
 * An [android.app.Activity] that can be addressed by an intent.
 */
interface AddressableActivity {
    /**
     * The activity class name.
     */
    val className: String
}
复制代码

使用这种方式,咱们建立了一个函数来统一活动启动意图建立:

/**
 * Create an Intent with [Intent.ACTION_VIEW] to an [AddressableActivity].
 */
fun intentTo(addressableActivity: AddressableActivity): Intent {
    return Intent(Intent.ACTION_VIEW).setClassName(
            PACKAGE_NAME,
            addressableActivity.className)
}
复制代码

最简单实现 AddressableActivity 方式为仅需一个显示类名做为一个字符串。经过 Plaid,每个 活动 都经过该机制启动。对一些包含意图附加部分,必须经过应用各个组件传递到活动中。

以下文件查看咱们的实现过程:

Styleing 问题

相对于整个应用单一清单文件而言,如今对每个动态功能模块,对清单文件进行了分离。 这些清单文件主要包含与它们组件实例化相关的一些信息,以及经过 dist: 标签反应的一些与它们交付类型相关的一些信息。 这意味着活动和服务都必须声明在包含有与组件对应的相关代码的功能模块中。

咱们遇到了一个将样式模块化的问题;咱们仅将一个功能使用的样式提取到与该功能相关的模块中,可是它们常常是经过隐式构建在核心模块之上。

PLaid 样式结构部分

这些样式经过模块清单文件以主题形式被提供给组件活动使用。

一旦咱们将它们移动完毕,咱们会遇到像这样编译时问题:

* What went wrong:

Execution failed for task ‘:app:processDebugResources’.
> Android resource linking failed
~/plaid/app/build/intermediates/merged_manifests/debug/AndroidManifest.xml:177: AAPT:
error: resource style/Plaid.Translucent.About (aka io.plaidapp:style/Plaid.Translucent.About) not found.
error: failed processing manifest.
复制代码

清单文件合并视图将全部功能模块中清单文件合并到应用模块。合并失败将致使功能模块样式文件在指定时间对应用模块不可用。

为此,咱们在核心模块样式文件中为每同样式以下建立一份空声明:

<! — Placeholders. Implementations in feature modules. →

<style name=”Plaid.Translucent.About” />
<style name=”Plaid.Translucent.DesignerNewsStory” />
<style name=”Plaid.Translucent.DesignerNewsLogin” />
<style name=”Plaid.Translucent.PostDesignerNewsStory” />
<style name=”Plaid.Translucent.Dribbble” />
<style name=”Plaid.Translucent.Dribbble.Shot” />
<style name=”Plaid.Translucent.Search” />
复制代码

如今清单文件合并在合并过程当中抓取样式,尽管样式的实际实现是经过功能模块样式引入。

另外一种避免如上问题作法是保持样式文件声明在核心模块。但这仅做用于全部资源引用同时也在核心模块中状况。这就是咱们为什么决定经过上述方式的缘由。

动态功仪器测试

经过模块化,咱们发现测试工具目前不能驻留在动态功能模块中,而是必须包含在应用模块中。对此咱们将在即将发布的有关测试工做博客文章中进行详细介绍。

接下来还会发生什么?

动态代码加载

咱们经过应用束使用动态交付,但初次安装后不要经过 Play Core Library 下载这些文件。例如这将容许咱们将默认未启用的新闻源(产品搜索)标记为仅在用户容许该新闻源后安装。

进一步增长新闻源

经过模块化过程,咱们保持考虑进一步增长新闻源可能性。分离清洁模块工做以及实现按需交付可能性使得这一点更加剧要。

模块精细化

咱们在模块化 Plaid 方面取得很大进展。但仍有工做要作。产品搜索是一个新的新闻源,如今咱们并未放到动态功能模块当中。同时一些已提取的功能模块中的功能可从核心模块中移除,而后直接集成到各自功能中。

为什么我决定模块化 Plaid?

经过该过程,Plaid 如今是一个高度模块化应用。全部这些都不会改变用户体验。咱们在平常开发中确实从这些努力中得到了一些益处。

安装体积

PLaid 如今用户设备平均减小 60% 体积。 这使得安装更快,而且节省宝贵网络开销。

编译时间

一个没有缓存的调试构建如今需 32 秒而不是 48 秒。 同时任务从 50 项增加到 250 项。

这样的时间节省,主要是因为增长并行构建以及因为模块化而避免编译。

未来,单个模块变化不需对全部单个模块进行编译,而且使得连续编译速度更快。

  • 做为引用,这些是我构建 beforeafter timing 的一些提交。

可维护性

咱们在过程当中分离可各类依赖项,这使得代码更加简洁。同时,反作用愈来愈小。咱们的每一个功能模块均可在愈来愈少交互下独立工做。但主要益处是咱们必须解决的冲突合并愈来愈少。

结语

咱们使得应用体积减小超过 60%,完善了代码结构而且将 PLaid 模块化成动态功能模块以及增长了按需交付潜力。

整个过程,咱们老是将应用保持在一个可随时发送给用户状态。您今天可直接切换你的应用发出一个应用束以节省安装体积。模块化须要一些时间,但鉴于上文所见好处,这是值得付出努力的,特别是考虑到动态交付。

去查看 Plaid’s source code 了解咱们全部的变化和快乐模块化过程!

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索