Android Studio是如何运行app的?

你在Android Stuiod中点击绿色三角形图标(Run)时,背后发生了什么?点击黄色闪电图标(Apply Change)呢?

前言

我们的Android项目有一个奇怪的问题:在Android Studio使用Instant Run运行时,无论修改一行代码还是几行代码,永远都是cold swap,没有见到传说中的hot swap或warm swap。

如果对instant run或hot swap这些概念不是太熟,可以快速浏览一下官方文档

这个问题本身没有太多影响,无非是多增加几秒中的开发时间。但问题就是问题,不是吗?

我花了一些时间跟进这个问题。本文是从Android Studio如何运行应用的角度来看这个问题的,比较粗线条,一些细节还有待深入。

主要的参考资料是

涉及到的代码包括

运行过程

用户在Android Studio中点击Run或Apply Change运行应用的过程可以简单总结为如下几个阶段:

  • 用户点击Run按钮(pre build阶段)
  • 开始Gradle构建(build阶段)
  • 部署应用并启动(post build阶段)

点一下按钮很简单,但背后的过程其实比较复杂:

  • 用户点击Run按钮(pre build阶段) - Android Studio调用AndroidRunConfigurationBase.getState()方法,该方法创建AndroidRunState实例
  • 开始Gradle构建(build阶段) - 跟普通的Gradle构建本质上一样,但Android Gradle Plugin提供了许多Android特定的Task,构建中会用到这些Task
  • 部署应用并启动(post build阶段) - 这个阶段又回到Android Studio,Android Studio调用AndroidRunState.execute()方法,该方法执行一系列的LaunchTasksLaunchTasks会调用aminstall等adb命令安装apk或启动应用

pre build阶段有几个重要的操作:

  • InstantRunBuilder.computeBuildCause() - 计算本次构建的原因(其中包含build mode)
  • InstantRunBuilder.getInstantRunArguments() - 生成用于Gradle中InstantRun相关Task的参数
  • AndroidRunConfigurationBase.getState() - 保存build options到env 。build options最重要的参数是build mode。

build阶段会从读取到上述build options,build options作为运行Gradle命令时的参数(比如,android.optional.compilation参数)。build阶段的结果会写入到build-info.xml

小提示: build-info.xml的两种查看方式

  • 项目的/build/intermediates/build-info/debug/build-info.xml
  • Android Studio > Show Log in Finder,然后在flr目录下找到build-info.xml (这里会有很多build-info.xml,分别对应于每一次构建)

post build阶段读取build-info.xml,根据其内容决定如何部署和启动应用。

先用这张图简单总结一下,再来逐个分析。

由于涉及到日常app开发中不常见的一些东西,为尽可能容易理解,对每个阶段都按这个套路进行分析。

  • 属于哪个阶段,涉及到的工具
  • 对应的日志
  • 对应的代码
  • 相关的代码

提示:一些代码很难完全看懂,但根据对应的日志不难猜出大概

Pre build

Pre build阶段发生在Android Studio中用户点击Run按钮时。

对应的重要日志见Android Studio的idea.log (日志位置 Android Studio > Show Log in Finder)。日志如下,通过该日志可以知道当前构建的原因以及构建的类型。

1
2018-10-17 14:27:33,770 [thread 476]   INFO - ools.idea.fd.InstantRunBuilder - BuildCause: APP_NOT_RUNNING, BuildMode: COLD 

对应的代码见:

这里只看三个较重要的方法。

AndroidRunConfigurationBase.getState()主要有以下功能:

  • 选择手机或模拟器设备
  • 保存build options

图片来源

InstantRunBuilder.computeBuildCause()方法中计算BuildCauseBuildCause定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum BuildCause {
// reasons for full build
NO_DEVICE(BuildMode.FULL),
APP_NOT_INSTALLED(BuildMode.FULL),
MISMATCHING_TIMESTAMPS(BuildMode.FULL),
API_TOO_LOW_FOR_INSTANT_RUN(BuildMode.FULL),
FIRST_INSTALLATION_TO_DEVICE(BuildMode.FULL), // first installation in this Android Studio session
FREEZE_SWAP_REQUIRES_API21(BuildMode.FULL),

// reasons for forced cold swap build
USER_REQUESTED_COLDSWAP(BuildMode.COLD), // User pressed Run, and only Run button was enabled (name not ideal, but matches existing proto)
USER_CHOSE_TO_COLDSWAP(BuildMode.COLD), // Both Run and Hotswap were enabled, and user chose Run
APP_NOT_RUNNING(BuildMode.COLD),
APP_USES_MULTIPLE_PROCESSES(BuildMode.COLD),
ANDROID_TV_UNSUPPORTED(BuildMode.COLD),
MANIFEST_RESOURCE_CHANGED(BuildMode.COLD),

INCREMENTAL_BUILD(BuildMode.HOT),
}

如果在使用Instant Run过程中遇到问题或疑惑,或许可以从computeBuildCause()的代码找到答案。以这个精简后的代码为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NotNull
private BuildCause computeBuildCause(@Nullable IDevice device) {
...
if (manifestResourceChanged(device)) {
return BuildCause.MANIFEST_RESOURCE_CHANGED;
}
...
if (myRunContext.isForceColdswap()) {
return myRunContext.couldHaveInvokedHotswap() ? BuildCause.USER_CHOSE_TO_COLDSWAP : BuildCause.USER_REQUESTED_COLDSWAP;
}
...
if (myInstantRunContext.usesMultipleProcesses()) {
return BuildCause.APP_USES_MULTIPLE_PROCESSES;
}
...
}

通过代码我们可以知道:

  • 修改应用的manifest后不可能进行hot swap,至少会是cold swap。因为修改manifest后返回MANIFEST_RESOURCE_CHANGED,它对应BuildMode.COLD
  • 多进程的应用不可能进程hot swap,至少会是cold swap。因为多进程应用时返回APP_USES_MULTIPLE_PROCESSES,它对应BuildMode.COLD

再看一下InstantRunBuilder.getInstantRunArguments()方法,它为build过程生成参数。

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
private static List<String> getInstantRunArguments(@NotNull BuildMode buildMode) {
StringBuilder sb = new StringBuilder(50);
sb.append("-P");
sb.append(PROPERTY_OPTIONAL_COMPILATION_STEPS);
sb.append("=");
sb.append(OptionalCompilationStep.INSTANT_DEV.name());

switch (buildMode) {
case HOT:
break;
case COLD:
sb.append(",").append(OptionalCompilationStep.RESTART_ONLY.name());
break;
case FULL:
sb.append(",").append(OptionalCompilationStep.FULL_APK.name());
break;
}

String compilationSteps = sb.toString();

// Starting with Studio 2.3, we always do a split APK install on cold swaps
String coldSwapMode = AndroidGradleSettings.createProjectProperty(AndroidProject.PROPERTY_SIGNING_COLDSWAP_MODE, "MULTIAPK");

return ImmutableList.of(compilationSteps, coldSwapMode);
}

这段代码的意思就是根据不同的buildMode,会生成不同的参数传给Gradle:

  • HOT - INSTANT_DEV, 传-Pandroid.optional.complilation=INSTANT_DEV
  • COLD - RESTART_ONLY, 传-Pandroid.optional.complilation=INSTANT_DEV,RESTART_ONLY
  • FULL - FULL_APK, 传-Pandroid.optional.complilation=INSTANT_DEV,FULL_APK

Gradle收到的参数不同,运行的Task也将有所不同。

Build

Build阶段发生在Gradle中。

对应的重要日志见build.log (日志位置 Android Studio > Show Log in Finder, 然后找flr目录)。日志如下,通过该日志知道重新编译的class文件跟前一个版本是兼容的,所以接下来可以进行hot swap:

1
2
Receiving verifier result: COMPATIBLE. Current Verifier/Build mode is NO_CHANGES/HOT_WARM.
Verifier result is now : COMPATIBLE. Build mode is now HOT_WARM.

Build阶段还会生成一个重要的文件build-info.xml (日志位置 Android Studio > Show Log in Finder, 然后找flr目录)

对应的代码见com.android.build.gradle.internal.InstantRunBuildContext.setVerifierStatus() Github

相关的代码有:

  • TaskManager.createPostCompilationTasks()
  • InstantRunTaskManager.createPreColdswapTask(),这个方法会读取上述android.optional.complilation参数
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// InstantRunTaskManager.java
/** Creates all InstantRun related transforms after compilation. */
@NonNull
public PreColdSwapTask createPreColdswapTask(@NonNull ProjectOptions projectOptions) {

TransformVariantScope transformVariantScope = variantScope.getTransformVariantScope();
InstantRunBuildContext context = variantScope.getInstantRunBuildContext();

if (transformVariantScope.getGlobalScope().isActive(OptionalCompilationStep.FULL_APK)) {
context.setVerifierStatus(InstantRunVerifierStatus.FULL_BUILD_REQUESTED);
} else if (transformVariantScope.getGlobalScope().isActive(
OptionalCompilationStep.RESTART_ONLY)) {
context.setVerifierStatus(InstantRunVerifierStatus.COLD_SWAP_REQUESTED);
}

PreColdSwapTask preColdSwapTask =
taskFactory.create(
new PreColdSwapTask.ConfigAction(
"preColdswap", transformVariantScope, variantScope));

if (verifierTask != null) {
preColdSwapTask.dependsOn(verifierTask);
}

return preColdSwapTask;
}

// InstantRunBuildContext.java
/**
* Sets the verifier status for the current build.
*
* @param verifierStatus
*/
public void setVerifierStatus(@NonNull InstantRunVerifierStatus verifierStatus) {

LOG.info(
"Receiving verifier result: {}. Current Verifier/Build mode is {}/{}.",
verifierStatus,
currentBuild.getVerifierStatus(),
currentBuild.buildMode);

// get the new build mode for this verifier status as it may change the one we
// currently use.
InstantRunBuildMode newBuildMode =
currentBuild.buildMode.combine(
verifierStatus.getInstantRunBuildModeForPatchingPolicy(patchingPolicy));

// save the verifier status, even if it does not end up being used as the main status,
// this can be useful to check later on that certain condition were not met.
currentBuild.allStatuses.add(verifierStatus);

// if our current status is not set, or the new build mode is higher, reset everything.
if (currentBuild.getVerifierStatus() == InstantRunVerifierStatus.NO_CHANGES
|| currentBuild.getVerifierStatus() == InstantRunVerifierStatus.COMPATIBLE
|| newBuildMode != currentBuild.buildMode) {
currentBuild.verifierStatus = verifierStatus;
currentBuild.buildMode = newBuildMode;
}
Preconditions.checkNotNull(
patchingPolicy, "setApiLevel should be called before setVerifierStatus");

LOG.info(
"Verifier result is now : {}. Build mode is now {}.",
currentBuild.getVerifierStatus(),
currentBuild.buildMode);
}

Post Build

Post Build阶段发生在Android Studio。

对应的代码见com.android.tools.idea.run.AndroidLaunchTasksProviderFactory,它读取build-info.xml确定要执行的Task。

这里不展开分析,只上一张图

图片来源

注意:这张图跟最新的代码不完全对应,但不影响理解。

关键代码

这里记录一下分析问题过程中遇到的一些关键代码,以备查找。

JetBrains/android

build相关:

  • com.android.tools.idea.fd.BuildMode - 构建模式。分为HOT, COLD, FULL三种
  • com.android.tools.idea.fd.BuildCause - 构建原因
  • com.android.tools.idea.run.AndroidRunConfigurationBase
  • com.android.tools.idea.run.AndroidRunConfiguration
  • com.android.tools.idea.fd.InstantRunBuilder

task相关:

Android Gradle Plugin

  • com.android.build.gradle.internal.incremental.BuildInfoWriterTask - 创建build-info.xml
  • com.android.build.gradle.internal.incremental.InstantRunBuildContext - 记录了构建相关的信息,这些信息将写入build-info.xml
  • com.android.build.gradle.internal.transforms.InstantRunVerifierTransform - 比较同一个class文件的两个版本是否支持instant run
  • com.android.build.gradle.internal.transforms.InstantRunTransform - 比class进行增强,以支持instant run的hot swap

总结

用户在Android Studio中点击Run或Apply Change运行应用时,Android Studio会保存build options并为Gradle生成参数。Gradle进行实际构建,构建结果记录在build-info.xml文件中。Android Studio分析构建结果后调用Android SDK相关工具部署并启动应用。

参考

android/README.md at master · JetBrains/android

Run Configurations / IntelliJ Platform SDK DevGuide

Android Developer Tools - Instant Run