反思|Android源码模块化管理工具Repo分析

5,348 阅读16分钟

「反思」 系列是笔者对于 学习归纳 一种新的尝试,其起源与目录请参考 这里

起源

随着Android项目 模块化插件化 项目业务的愈发复杂,开发流程中通过版本控制工具(比如Git)管理项目的成本越来越高。

以大名鼎鼎的 Android源代码开源项目Android Open-Source Project,下文简称 AOSP)为例,截止2020年初,Android10的源码项目,其模块化分割出的 子项目 已接近800个,而每一个子项目都是一个独立的Git仓库。

这意味着Git的使用成本究竟有多高?如果开发者希望针对AOSP的一个分支进行开发,就需要手动将每个子项目进行checkout操作,如果本地分支尚未创建,开发者便需要手动地在每一个子项目里面去创建分支。

如此高昂的使用成本显然需要一种更自动化的方式去处理。为此,Google的工程师基于Git进行了一系列的代码补充,推出了名为Repo的代码版本管理工具,其本质是通过Python开发出一系列的脚本命令,便于开发者对复杂的模块化源码项目进行统一的调度和切换。

即使对于上文说到的AOSP而言,其同样使用了Repo工具进行项目的管理,由此可见,对于 高度模块化 开发的Android项目而言,Repo工具的确有一定的学习和借鉴意义。

本文以AOSP为例,对Repo工具的 使用流程原理 进行系统性的分析,读者需要对GitRepo工具有一定的了解。

官方文档:Repo入门及基本使用 source.android.com/source/down…

本文大纲如下:

核心思想

Repo 是以 Git 为基础构建的代码库管理工具。其并非用来取代 Git,只是为了让开发者在多模块的项目中更轻松地使用 GitRepo 命令是一段可执行的 Python 脚本,开发者可以使用 Repo 执行跨网络操作。例如,借助单个 Repo 命令,将文件从多个代码库下载到本地工作目录。

那么,Repo幕后原理究竟是怎么样的?想要真正的理解Repo,就必须理解Repo最核心的三个要素:Repo仓库Manifest仓库 以及 项目源码仓库

这里我们先将三者的关系通过一张图进行概括,该图已经将Repo工具本身的结构描述的淋漓尽致:

1、项目源码仓库:底层的被执行者

对于若干个模块化的子项目,也就是 项目源码仓库 而言,它们是开发者希望的 被统一调度的对象

比如,通过一个简单的Repo命令,统一完成所有子项目的分支切换、代码提交、代码远端更新等等。

因此,对于Repo工具整个框架的设计而言,项目源码仓库 明显应该处于最底层,它们是被Repo命令执行操作的最基本元素。

2、Manifest仓库:子项目元信息的容器

Manifest仓库 中最重要的是一个名为manifest.xml的清单文件,其存储了所有子项目仓库的元信息。

Repo命令想要对所有子项目进行对应操作的时候,其总是需要知道 要操作的项目的相关信息——比如,我想要clone AOSP所有子项目的代码,首先我需要知道所有子项目仓库的名称和仓库地址;这时,Repo便会从manifest仓库中获取对应所有仓库的元信息,并进行对应的fetch操作。

对于Android应用的开发者而言这很好理解,对于一个APP而言,其对应的组件通过在manifest中声明进行管理。

因此,想要通过Repo对模块化项目进行管理,项目的管理者必须提供一个对应的manifest清单文件,里面存储所有子项目的相关信息,这样,Repo工具才能通过对其进行解析,然后完成子项目的统一管理。

此外,读者应该知道,AOSP也是在迭代过程中不断变化的,因此,其每一个分支版本所包含的子项目信息可能都是不同的,这意味着Manifest仓库同样也是一个Git仓库,以达到AOSP不同分支版本中,该仓库对应存储的子项目元信息不同的目的。

3、Repo仓库:顶层命令的容器

Repo工具实际上是由一系列的Python脚本组成的,这些Python脚本通过调用Git命令来完成自己的功能。

Repo仓库的本质就是存储了各种各样的Python脚本,当开发者调用相关的Repo命令时,便会从Repo仓库中运行对应的脚本进行处理,并根据脚本中的代码逻辑,找到manifest中所有项目的元信息,然后将其中包含的子项目进行对应命令的处理——因此,我们可以称 Repo仓库是顶层命令的容器

此外,和Manifest仓库相同,组成Repo工具的Python脚本本身也是一个Git仓库;每当开发者执行Repo命令的时候,Repo仓库都会对自己进行一次更新。

读者请务必深刻理解这三者的意义,这也是Repo工具内部最核心的三个概念,也是阅读下文内容的基础。

现在,通过Repo工具完成项目模块化的管理需要分步构建以上三个角色,但是在这之前,我们需要先将Repo工具添加到自己的开发环境中。

一、Repo脚本初始化流程

正如 官方文档 所描述的,通过以下命令安装Repo工具,并确保它可执行:

curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

安装成功后,对应的目录下便会存在一个repo脚本文件,通过将其配置到环境中,开发者可以在终端中使用repo的基本命令。

整个流程如下图所示:

二、Repo仓库创建流程

Repo脚本初始化完毕,接下来针对Repo仓库创建流程进行简单的分析。

1、工欲善其事,必先利其器

AOSP项目为例,开发者通过以下命令来安装一个Repo仓库:

repo init -u https://android.googlesource.com/platform/manifest -b master

这个命令实际上是包含了两个操作:初始化 Repo仓库Manifest仓库,其中Repo仓库完成初始化之后,才会继续初始化Manifest仓库。

这很好理解,Repo仓库的本质就是存储了各种各样的Python脚本,若它没有初始化,就不存在所谓的Repo相关命令,更遑论后面的Manifest仓库初始化和子项目代码初始化的流程了。

这一小节我们先分析 Repo仓库 的安装过程,在下一小节再分析 Manifest仓库 的安装过程。

本小节整体流程如下图所示:

2、init命令分析

上一节我们成功安装了repo脚本文件,这个脚本里面提供了例如versionhelpinit等最基本的命令:

def main(orig_args):
    if cmd == 'help':                   // help命令
      _Help(args)
    if opt.version or cmd == 'version': // version命令
      _Version()
    if not cmd:
      _NotInstalled()
    if cmd == 'init' or cmd == 'gitc-init':  // init命令
      ...

由此可见Repo脚本最初提供的命令确实非常少,前两个命令十分好理解,分别是查看Repo工具相关依赖的版本或者查看帮助,比较重要的是init命令,这个命令的作用便是对本地Repo仓库的初始化。

那么Repo仓库如何才能初始化呢?设计者并没有尝试直接向远端服务器请求拉取代码,而是从当前目录开始 往上遍历直到根目录 ,若在这个过程中找到一个.repo/repo目录,并且该目录本身的确是一个Repo仓库,便尝试从该仓库 克隆一个新的Repo仓库 到执行Repo脚本的目录中。

反之,若从本地向上直到根目录不存在Repo仓库,则尝试向远端克隆一个新的Repo仓库到本地来。

回到本地克隆Repo仓库的流程中,代码是如何判断本地的.repo/repo目录的确是一个Repo仓库的呢,代码中已经描述的非常清晰了:

def _RunSelf(wrapper_path):
  my_dir = os.path.dirname(wrapper_path)
  my_main = os.path.join(my_dir, 'main.py')
  my_git = os.path.join(my_dir, '.git')

  if os.path.isfile(my_main) and os.path.isdir(my_git):
    for name in ['git_config.py',
                 'project.py',
                 'subcmds']:
      if not os.path.exists(os.path.join(my_dir, name)):
        return None, None
    return my_main, my_git
  return None, None

从这里我们就可以看出,判断的依据是对应的需要满足以下条件:  1、存在一个.git目录;   2、存在一个main.py文件;   3、存在一个git_config.py文件;   4、存在一个project.py文件;   5、存在一个subcmds目录。

读到这里,读者可以对Repo仓库进行一个简单的总结了。

3、Repo仓库到底是什么

从上文的源码中,读者了解了Repo脚本源码中判断是否是Repo仓库的五个依据,从这些判断条件中,我们可以简单对Repo仓库的定位进行一个总结。

首先,从条件1中我们得知,组成Repo工具的Python脚本本身也是一个Git仓库;每当开发者执行Repo命令的时候,Repo仓库都会对自己进行一次更新。

其次,Repo仓库本身作为存储Python脚本的容器,其内部必然存在一个入口的main函数可供运行。

对于条件3而言,我们直到Repo工具本质是对Git命令的封装,因此,必须有一个类负责Git相关的配置信息,和提供简单的Git相关工具方法,这便是git_config.py文件的作用。

对于条件4,Repo仓库目录下还需要一个project.py文件,负责Hook相关功能,细心的读者应该注意到,/.repo/repo目录下还有一个/hooks/目录。

最后也是最重要的,/.repo/repo目录下必须还存在一个subcmds目录,顾名思义,这个目录下存储了绝大多数repo重要的命令,比如synccheckoutpullcommit等等;这也说明了,如果没有Repo仓库的初始化,使用Repo命令操作子项目代码仓库便是无稽之谈

三、Manifest仓库创建流程

继续回到上一节我们使用到的命令:

repo init -u https://android.googlesource.com/platform/manifest -b master

读者已经知道,通过init命令,我们在指定的目录下,成功初始化了Repo仓库。当安装好Repo仓库之后,就会调用该Repo仓库下面的main.py脚本,对应的文件为.repo/repo/main.py

这样我们便可以通过init后面的-u -b参数,进行Manifest仓库的创建流程,其中-u指的是manifest文件所在仓库对应的Url地址,-b指的是对应仓库的默认分支。

本小节整体流程如下图所示:

1、定义manifest文件

上文中我们提到,想要通过Repo对模块化项目进行管理,项目的管理者必须提供一个对应的manifest清单文件,里面存储所有子项目的相关信息,这样,Repo工具才能通过对其进行解析,然后完成子项目的统一管理。

对于公司的业务而言,项目的管理者需要根据自己公司的实际业务模块构造出自己的manifest文件,并放置在某个git仓库内,这样开发者便可以通过指定对应的Url构建Manifest仓库。

本文以AOSP项目为例,其项目清单文件所在的Url为:

android.googlesource.com/platform/ma…

2、初始化Manifest仓库

通过init命令和对应的参数,Repo便可以尝试从远端克隆Manifest仓库,然后从指定的Url克隆对应的manifest文件,切换到对应的分支并进行解析。

这里描述比较简单,实际上内部实现逻辑非常复杂;比如,在向远端克隆对应的Manifest仓库之前,会先进行本地是否存在Manifest仓库的判断,若已经存在,则尝试更新本地的Manifest仓库,而非直接向远程仓库中克隆。此外,当未指定分支时,则会checkout一个default分支。

这之后,Repo会根据远端的xml清单文件尝试构建自己本地的Manifest 仓库。

3、Manifest仓库的文件层级

让我们看以下/.repo/目录下文件层级:

上文我们说到,Manifest仓库本身也是一个Git仓库,因此,当我们打开.repo/manifests/目录时,里面会存在一个.git的文件夹,远端的Manifest文件仓库中的所有文件都被克隆到了这个目录下。

这里重点说一下项目的Git仓库目录和工作目录的概念。一般来说,一个项目的Git仓库目录(默认为.git目录)是位于工作目录下面的,但是Git支持将一个项目的Git仓库目录和工作目录分开来存放。

AOSP中,Repo仓库的Git目录位于工作目录.repo/repo下,Manifest仓库的Git目录有两份拷贝,一份.git位于工作目录.repo/manifests下,另外一份位于.repo/manifests.git目录。

同时,我们看到这里还有一个.repo/manifest.xml文件,这个文件是最终被Repo的文件,它是通过将.repo/manifest文件夹下的文件和local_manifest文件进行合并后生成的,关于local_manifest机制我们后文会讲到,这里仅需将.repo/manifest.xml文件视为最终被使用的配置文件即可。

4、解析并生成Projects项目

回到上图,我们知道名字带有manifest相关的文件和文件夹代表了Manifest仓库,其内部存储了所有子项目仓库的元信息;而repo文件夹中存储了repo相关命令的脚本文件。

读者注意到,除此之外,还有一部分名字带有project的文件和文件夹,它们便是代表了Repo解析Manifest后生成的子项目信息和文件。

Repo中,其管理的所有子项目,每一个子项目都被封装成为了一个Project对象,该对象内部存储了一系列相关的信息。

现在,Manifest仓库被创建并初始化完毕,接下来我们分析Reposync流程,看看子项目是如何被统一下载和管理的。

四、子项目仓库Sync流程

执行完成repo init命令之后,我们就可以继续执行repo sync命令来克隆或者同步子项目了:

repo sync

当执行repo sync命令时,会默认尝试拉取远程仓库下载更新本地的Manifest 仓库,下载远端对应的default.xml文件。

下载完成后,会自动解析default.xml文件中项目管理者配置的所有子项目信息,然后每个子项目信息被解析成为一个Project对象,并整合到一个内存的集合中去。

接下来,根据本地是否已经存在对应的子项目源码,针对每一个子项目,Repo都会进行对应的更新操作或者克隆操作,而这些操作的本质,其实就是内部调用了Gitfetchrebase或者merge等等命令。

值得关注的是,和Manifest仓库相似,AOSP子项目的工作目录和Git目录也都是分开存放的,其中,工作目录位于AOSP根目录下,Git目录位于.repo/projects目录下。

此外,每一个AOSP子项目的工作目录也有一个.git目录,不过这个.git目录是一个符号链接,链接到.repo/repo/projects对应的Git目录。这样,我们就既可以在AOSP子项目的工作目录下执行Git命令,也可以在其对应的Git目录下执行Git命令。

本小节整体流程如下图所示:

五、LocalManifest机制

从上文中读者已经知道了,对于源码来讲,manifest.xml只是一个到.repo/manifests/default.xml的文件链接,真正的清单文件是通过manifests这个Git仓库托管起来的。

需要注意的是,在进行Android系统开发时,通常需要对清单文件进行自定义定制。例如,设备厂商会构建自己的manifest库,通常是基于AOSPdefault.xml进行定制,去掉AOSP的一些Git库、增加一些自有的Git库。

这意味着,项目的管理者需要手动的对default.xml文件内容进行修改,然而这种方式在一些场景下存在弊端——对于AOSP而言,其本身可能存在几百个不同的分支,而项目的管理者需要修改的内容却基本是相同的。

比如,国内某个手机厂商需要删除AOSP中某个不受中国支持的功能,就需要对每个分支的default.xml文件内容进行相同的修改——删除某个project标签。

因此,Repo工具提出了另外一种本地的支持,这个机制便是LocalManifest机制。

repo sync下载代码之前,会将.repo/manifests/default.xml、local_manifest.xml.repo/local_manifests/目录下存在清单文件进行合并,再根据融合的清单文件进行代码同步。

这样一来,只需要将清单文件的修改项放到.repo/local_manifests/目录下, 就能够在不修改default.xml的前提下,完成对清单的文件的定制。

LocalManifest机制的原理图如下所示:

参考网上的资料,Local Manifests的隐含规则如下:

  • 1、先解析local_manifest.xml,再解析local_manifests/目录下的清单文件;
  • 2、local_manifests目录下的清单文件是没有命名限制的,但会按照字母序被解析,即字母序靠后的文件内容会覆盖之前的;
  • 3、 所有清单文件的内容必须遵循repo定义的格式才能被正确解析。

参考 & 感谢

1.《Android源代码仓库及其管理工具Repo分析》 by 罗升阳:
blog.csdn.net/Luoshengyan…

罗老师的这篇文章非常经典,文章针对源码进行了非常细致的讲解,本文前四个小节都是参考该文进行的参考总结,强烈建议阅读。

2.《Android Local Manifests机制》 by ZhangJianIsAStark:
blog.csdn.net/gaugamela/a…

针对 LocalManifests机制 进行了非常详细的讲解,本文的第五节内容都是从中截取的,想要仔细了解的可以阅读本文。

3.AOSP Google 官方文档:
source.android.com/source/deve…

4.《Google Git-Repo 多仓库项目管理》 by 郑晓鹏-Rocko:
juejin.cn/post/684490…

一篇非常不错的实践总结,该文并非针对Repo进行系统性的讲述,但是对于实践者而言是一篇不错的参考文章,从基础到集成到jenkins都有讲述。

关于我

Hello,我是 却把清梅嗅,女儿奴,Android,分享者 & 见证者,观众途径序列1,乐于分享的开发者。欢迎关注我的 博客 或者 GitHub

如果您觉得文章对您有价值,欢迎 ❤️,或通过下方打赏功能,督促我写出更好的文章 :)