[VSCode 源码阅读笔记] VSCode 的启动流程

Posted by Nicodechal on 2020-03-07

本文总结 VSCode 的大致启动过程,即从启动到打开第一个界面之间的过程。阅读版本为官方当前 最新代码 的 master 分支。

启动 CodeMain

下面介绍从启动到运行 CodeMain 的过程。

VSCode 的启动脚本为 src/main.js。其在完成一些必要的设置后 (包括启用 ASAR、设置一些路径和缓存、注册 Scheme、启动全局监听器以及解析启动的命令行参数和配置本地化),等待 app 的 ready 触发运行 onReady 加载 VSCode 的代码 ( 之后的代码均经过精简 ):

1
2
3
app.once('ready', function () {
onReady();
});

onReady 函数在缓存和本地化完成后用得到的缓存目录 cachedDataDir 和本地化配置 nlsConfig 作为参数运行 startup

1
2
3
4
5
6
7
async function onReady() {
const [cachedDataDir, nlsConfig] = await Promise.all([
nodeCachedDataDir.ensureExists(),
resolveNlsConfiguration()
]);
startup(cachedDataDir, nlsConfig);
}

startup 继续做一些缓存和本地化的工作后,使用 AMD 方式加载文件 vs/code/electron-main/main,该文件即为 CodeMain 所在文件。

1
2
3
4
5
6
function startup(cachedDataDir, nlsConfig) {
// do some work for nls & cache
// ...
// Load main in AMD
require('./bootstrap-amd').load('vs/code/electron-main/main');
}

进入该文件以后,会直接执行文件底部的启动代码创建一个 CodeMain 实例并调用它的 main 方法。

1
2
3
// Main Startup
const code = new CodeMain();
code.main();

启动 CodeApplication

main 方法主要调用实例方法 startup 来继续启动过程

1
2
3
4
5
6
7
8
9
10
class CodeMain {
main(): void {
// Launch
this.startup(args);
}

private async startup(args: ParsedArgs): Promise<void> {
//...
}
}

创建 instantiationService

startup 需要进行一个比较重要的步骤,即启动实例化服务 instantiationService

VSCode 中,将有独立功能的可复用的基础代码作为服务 ( Service ) 提供给需要这些功能的实例,实例化服务 instantiationService 也是其中之一。这一步中,需要对其实例化,用于对程序中实例尤其是服务进行管理。

startup 首先会调用 createServices 来得到 instantiationService 和一个实例坏境 instanceEnvironmentinstantiationService 用于管理服务们,因此其实例化时需要提供一个服务列表servicescreateServices 主要就是完成 services 的构建并使用得到的 services 去构造 instantiationService

构造 services

services 是一个 ServiceCollection 的实例,ServiceCollection 在内部使用一个 Map 保存服务标识符 ( ServiceIdentifier ) 和服务实例或描述符 ( SyncDescriptor ) 的映射:

1
2
3
4
export class ServiceCollection {
private _entries = new Map<ServiceIdentifier<any>, any>();
// ...
}

对外提供 getset 等方法对这个 Map 进行操作。

下面对 ServiceIdentifierSyncDescriptor 进行说明。

ServiceIdentifier

服务标识符用来唯一表示一个服务,只能使用 createDecorator 创建。该函数接受一个唯一的服务ID ( serviceId ) 创建一个标识符,该标识符同时也是一个装饰器,该装饰器用于实现依赖注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {

// 这里根据 id 直接查找标识符,如果 Map 中已经存在直接返回
if (_util.serviceIds.has(serviceId)) {
return _util.serviceIds.get(serviceId)!;
}

// 这里实现装饰器
const id = <any>function (target: Function, key: string, index: number): any {
// 确保是 parameter decorator
if (arguments.length !== 3) {
throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
}
// 记录服务依赖
storeServiceDependency(id, target, index, false);
};

// 这里确保 toString 返回 serviceId
id.toString = () => serviceId;

// 用 Map 记录该服务
_util.serviceIds.set(serviceId, id);
return id;
}

function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
// 已经初始化过直接添加新的依赖到 target 函数上 (一般就是构造函数)
if ((target as any)[_util.DI_TARGET] === target) {
(target as any)[_util.DI_DEPENDENCIES].push({ id, index, optional });
} else {
// 初始化
(target as any)[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
// 这里可以标志已经初始化过
(target as any)[_util.DI_TARGET] = target;
}
}

SyncDescriptor

SyncDescriptor 用于保存创建一个实例所需参数,包括构造函数和一些参数等。主要用于延迟实例化某些服务,之后如果需要使用该服务时,再根据 SyncDescriptor 中的参数进行实例化。

综上,instantiationService 中使用一个 ServiceCollection 实例 services 保存了一个 ServiceIdentifierSyncDescriptor 或服务实例的映射。因此通过 instantiationService 即可访问到所有服务的实例 (如果是 SyncDescriptor 会在访问时创建实例)。

services 创建后,它会用来实例化 instantiationService,此时使用已经注册的服务继续进行启动。接下来的目标是启动 CodeApplication

运行 CodeApplication

启动前需要进行两个主要的准备工作:

  1. 初始化需要初始化的服务,这里主要是 EnvironmentServiceConfigurationServiceStateService
  2. 创建用于实例化 CodeApplication 的 IPC 服务器 mainIpcServer

上面两步都使用 instantiationService 提供的 invokeFunction 方法获得对实例的访问能力,该方法需传入一个回调函数,通过函数的参数 accessorget 的方法可以得到受其管理的实例,此时如果还未创建的实例也会创建。

完成后,再使用 instantiationServicecreateInstance 方法传入 mainIpcServerinstanceEnvironment 创建 CodeApplication 实例,并调用其 startup 方法启动 CodeApplication

1
instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();

CodeApplication 的构造

上面创建 CodeApplication 时,只传递了两个参数,但其构造函数来看,远不止需要两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export class CodeApplication extends Disposable {
constructor(
private readonly mainIpcServer: Server,
private readonly userEnv: IProcessEnvironment,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILogService private readonly logService: ILogService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStateService private readonly stateService: IStateService
) {
super();
this.registerListeners();
}
}

之所以不需要传递所有的参数,是因为其他有装饰器的参数将通过依赖注入得到。

依赖注入

前面提到过,使用 createDecorator 可以创建一个装饰器,装饰器可以装饰函数参数 ( 此处是类的构造函数 ) ,同时记录该参数对应的类为该类的依赖项。因此,利用这个信息,我们就可以在创建一个对象时,查询并创建所有该类对象构建时所需的对象们,并将这些参数传递到该类的构造函数中创建对象。下面举例说明 instantiationService 是如何注入 CodeApplication 的。

首先,CodeMain 会使用下面语句创建 CodeApplication

1
instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment);

这里首先调用了 instantiationService.createInstance,然后该函数调用私有函数 instantiationService._createInstance 创建对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any {
let _trace: Trace, result: any;
_trace = Trace.traceCreation(ctorOrDescriptor);
result = this._createInstance(ctorOrDescriptor, rest, _trace);
_trace.stop();
return result;
}

private _createInstance<T>(ctor: any, args: any[] = [], _trace: Trace): T {
// arguments defined by service decorators
// 从构造函数和取到之前记录的依赖并按参数顺序排序
let serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
let serviceArgs: any[] = [];
for (const dependency of serviceDependencies) {
// 创建或者直接拿到 ( 如果已经创建过了 ) 服务的实例
let service = this._getOrCreateServiceInstance(dependency.id, _trace);
// 没找到
if (!service && this._strict && !dependency.optional) {
throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`);
}
// 加入参数表
serviceArgs.push(service);
}

// 找到注入参数前的其他非注入参数的结束位置,用于确保参数对应正确。
let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;

// check for argument mismatches, adjust static args if needed
if (args.length !== firstServiceArgPos) {
// 参数数量不对报警
console.warn(`[createInstance] First service dependency of ${ctor.name} at position ${
firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);

// 这里强行补齐
let delta = firstServiceArgPos - args.length;
if (delta > 0) {
args = args.concat(new Array(delta));
} else {
args = args.slice(0, firstServiceArgPos);
}
}

// 向构造函数中按顺序传入1. 需要填写的参数和 2. 通过注入得到的参数得到对象
return <T>new ctor(...[...args, ...serviceArgs]);
}

这里做的事:

  1. 取得注入项参数表 serviceArgs,这里主要使用 instantiationService_getOrCreateServiceInstance 得到这些注入项 serviceArgs
  2. 将传入 _createInstanceargs 补齐长度,确保注入项作为正确的参数传入构造函数。

构造一个对象需要构造函数 ctor 和参数表,这里的参数表分为两类一类是传递给 _createInstance 的参数 args,另一类就是注入的服务 serviceArgs

得到这些后就可以构造对象:

1
return <T>new ctor(...[...args, ...serviceArgs]);

这样就完成了 CodeApplication 的构建,并且实现了对构造函数中的服务的注入。这里也能看到,注入的参数放在构造函数的参数表尾部。

总结依赖注入构建对象的过程:

  1. 对象首先使用参数中需要注入的对象对应的装饰器注解该参数,该注解完成依赖记录用于之后根据依赖图创建依赖对象。
  2. 所有对象使用 instantiationServicecreateInstance 方法构建,此时需要传入构造函数 ctor 和非注入项的参数 args
  3. 根据之前记录的依赖情况,分析注入项依赖,取得或构建该对象的依赖,得到注入参数表 serviceArgs
  4. 使用构造函数 ctor 、非注入项的参数 args 和注入参数表 serviceArgs 构造对象。

创建 CodeWindow

准备

CodeApplication 构造完成立即调用其 startup 方法。该方法做了三件主要的事:

  1. 创建 Electron 的 IPC 服务器 electronIpcServer
  2. 启动和共享进程通信的客户端 sharedProcessClient
  3. 创建自己的实例化服务 appInstantiationService,该服务是 instantiationService 的子服务,之后打开窗口时需要访问这些服务。

完成上面步骤和一些其他的准备后,使用下面语句打开窗口:

1
2
// 获得 appInstantiationService 的访问能力并传入相关参数来打开一个窗口
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));

openFirstWindow

openFirstWindow 在注册了一系列 IPC 信道后,使用 windowsMainServiceopen 方法打开窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const app = this;
urlService.registerHandler({
async handleURL(uri: URI): Promise<boolean> {
// Check for URIs to open in window,解析 URI。
const windowOpenableFromProtocolLink = app.getWindowOpenableFromProtocolLink(uri);
if (windowOpenableFromProtocolLink) {
// 调用 windowsMainService 的 open 方法
windowsMainService.open({
context: OpenContext.API,
cli: { ...environmentService.args },
urisToOpen: [windowOpenableFromProtocolLink],
gotoLineMode: true
});
return true;
}
return false;
}
});

windowsMainService 位于 src/vs/platform/windows/electron-main/windowsMainService.ts

windowsMainService 首先收集所有需要打开的工作区、文件夹和文件,然后调用 doOpen 进行下一步操作,这步会返回一个 CodeWindow 实例数组。

1
const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, fileInputs, foldersToAdd);

这里考虑在一个新的窗口打开文件的过程,这里会调用 windowsMainService.openInBrowserWindow 实现新窗口的打开。

1
2
3
usedWindows.push(this.openInBrowserWindow({
//...
}));

usedWindows 是该函数返回的 CodeWindow 实例数组,这里使用 openInBrowserWindow 创建新的窗口并放入返回的数组中。

openInBrowserWindow 同样使用 instantiationService 来实例化一个 CodeWindow 对象,并将该对象加入 WindowsMainService 的窗口数组中且绑定相关事件。

1
2
3
4
// Create the window
const createdWindow = window = this.instantiationService.createInstance(CodeWindow, {
// ...
});

创建好窗口后,使用 doOpenInBrowserWindowBrowserWindow 打开窗口,doOpenInBrowserWindow 主要调用 CodeWindowload 方法进行加载。

CodeWindow 在创建的时候会首先创建一个 BrowserWindow 对象 _win,调用 load 时,该函数调用 _win.loadURL 来加载指定文件。

1
this._win.loadURL(this.getUrl(configuration));

getUrl 在设置相关的配置以后,调用 doGetUrl 得到真正打开的 URL。

1
2
3
4
5
6
class CodeWindow {
// ...
private doGetUrl(config: object): string {
return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}
}

至此,成功加载了第一个页面,该页面位于 src/vs/code/electron-browser/workbench/workbench.html

小结

本文总结了从启动到加载第一个页面的大致过程,大概总结如下:

  1. 进入 main.js,使用异步方式加载 CodeMain
  2. CodeMain 创建各种服务尤其是实例化服务 instantiationService,并使用 instantiationService 使用依赖注入构造 CodeApplication
  3. CodeApplication 创建 IPC 服务器等进程通讯功能然后使用 windowsMainService 创建 CodeWindow
  4. CodeWindow 最终创建 BrowserWindow 并加载页面。