Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
qingmei2 opened this issue May 5, 2020 · 0 comments
Open

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

qingmei2 opened this issue May 5, 2020 · 0 comments
Labels
Thinking in Android Thinking in Android

Comments

@qingmei2
Copy link
Owner

qingmei2 commented May 5, 2020

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

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

起源

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

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

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

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

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

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

官方文档:Repo入门及基本使用
https://source.android.com/]source/downloading.html

本文大纲如下:

核心思想

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为:

https://android.googlesource.com/platform/manifest

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 罗升阳:
https://blog.csdn.net/Luoshengyang/article/details/18195205

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

2.《Android Local Manifests机制》 by ZhangJianIsAStark:
https://blog.csdn.net/gaugamela/article/details/78593000

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

3.AOSP Google 官方文档:
https://source.android.com/source/developing.html

4.《Google Git-Repo 多仓库项目管理》 by 郑晓鹏-Rocko:
https://juejin.im/post/5bf5913fe51d457dd7800a73

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

关于我

Hello,我是 却把清梅嗅,女儿奴,源码的眷者,观众途径序列1,杀人游戏信徒,大头菜投机者,端茶递水工程师。欢迎关注我的 博客 或者 GitHub

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

@qingmei2 qingmei2 added the Thinking in Android Thinking in Android label May 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Thinking in Android Thinking in Android
Projects
None yet
Development

No branches or pull requests

1 participant