Skip to content

Latest commit

 

History

History
339 lines (264 loc) · 10.9 KB

0013-gradle-support-in-aosp-build.asc

File metadata and controls

339 lines (264 loc) · 10.9 KB

Gradle Support in AOSP build

自从Android Studio 1.0的正式发布,开发Android应用的首选IDE非其莫属。 Android官方也一直在为Android Studio开发新功能,使得Android应用开发更加便捷和高效。 然而,在AOSP代码中,所有Apps的构建却依然基于AOSP makefile。 AOSP的二次开发者无法利用Android Studio/Gradle的强大功能和新特性来开发应用,这是一件令人痛苦的事情。 本文介绍一个在AOSP build中支持Android Studio/Gradle项目的方法,供参考。

文章更新历史:

  • 2016/8/29 文章发布


1. 基本思路

由于Android Studio/Gradle项目的构建是通过自身完成的,仅依赖于Android SDK和网络库。 如果我们能够通过某种方式触发Gradle项目的构建,再把其生成的APK以AOSP的*BUILD_PREBUILT*方式编译进ROM, 那么即可大功告成。

按照这种思路,关键点在于,如何使BUILD_PREBUILT依赖的APK包去依赖另外一个target X, 而这个target X可以触发Gradle项目的构建。为了便于管理Gradle项目,应当尽量减少Gradle项目的个数 (例如,我们仅有一个,每个应用都是Gradle项目的一个module)。

另外一点,在AOSP二次开发时,特别是为多设备构建时,我们往往需要使用Overlay来做一些定制。 这个可以通过Gradle的buildTypes或者productFlavors的Overlay机制来完成。

2. 实现细节

2.1. Gradle项目组织

假设我们仅有一个Gradle项目,并假定其在AOSP代码中的目录为*vendor/xxx/YYYProject*, 该项目中有多个app module(假设为AppAaa,AppBbb, AppCcc等)。

第一件事情,我们在Gradle项目root目录中为其定义一个公共的build.gradle文件 (假设叫“yyy_project_apps_common.gradle”),这个文件中定义和约定一些公共的构建规则。 例如:

vendor/xxx/YYYProject/yyy_project_apps_common.gradle
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    if (project.plugins.hasPlugin("com.android.application")) {
        signingConfigs {
            platformKey {
                storeFile file("${rootDir}/keystores/platform.keystore")
                storePassword "android"
                keyAlias "platform"
                keyPassword "android"
            }
        }

        buildTypes {
            debug {
                signingConfig signingConfigs.platformKey
            }

            release {
                signingConfig signingConfigs.platformKey

                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt')
                if (new File("${project.projectDir}/proguard-project.txt").exists()) {
                    proguardFiles 'proguard-project.txt'
                } else if (new File("${project.projectDir}/proguard-rules.pro").exists()) {
                    proguardFiles 'proguard-rules.pro'
                } else {
                    println "no ProGuard file found for project: " + project.name
                }
            }
        }
    }

    lintOptions {
        textReport true
        textOutput 'stdout'

        abortOnError true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
}

然后在各app module的build.gradle中,引入yyy_project_apps_common.gradle:

vendor/xxx/YYYProject/AppAaa/build.gradle
apply plugin: 'com.android.application'
apply from: "${rootDir}/yyy_project_apps_common.gradle"
...

2.1.1. 为ROM构建APK

默认情况下,Gradle构建生成的APK文件都在各module自己的build目录下, 为了便于在Android.mk文件中引用Gradle生成的APK文件, 我们最好把Gradle生成的APK文件统一输出到一个目录中(假设为${rootDir}/rom_apps_out)。 因此,我们添加一个特殊的buildType “romApps”,并做一些定制:

vendor/xxx/YYYProject/yyy_project_apps_common.gradle
android {
        ...

        buildTypes {
            ...

            romApps {
                initWith(buildTypes.release)
            }
        }

        applicationVariants.all { variant ->
            variant.outputs.each { output ->
                if (variant.buildType.name.equals('romApps')) {
                    output.outputFile = new File(output.outputFile.parent,
                            "${project.name}.apk")
                    output.assemble.doLast {
                        copy {
                            from output.outputFile.getAbsolutePath()
                            into "${rootDir}/rom_apps_out"
                        }
                    }
                }
            }
        }

        ...
}

2.2. 编写AOSP makfile规则

在Gradle项目root目录中,添加一个Android.mk文件,用于触发APK的构建,并最终打包进ROM中。

先看最终的Android.mk,后面再解释:

vendor/xxx/YYYProject/Android.mk
LOCAL_PATH := $(call my-dir)

# Gradle project info
GRADLE_PROJECT_ROOT := $(LOCAL_PATH)
ROM_APPS_DIR := rom_apps_out

# Rules to build apps
.PHONY: buildRomApps
buildRomApps:
	$(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) assembleRomApps

$(GRADLE_PROJECT_ROOT)/$(ROM_APPS_DIR)/%.apk: buildRomApps
	@echo "APK file is ready for $@"

# Rules to add prebuilt APKs to ROM
# AppAaa
include $(CLEAR_VARS)
LOCAL_MODULE := XxxAppAaa
LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppAaa.apk
LOCAL_CERTIFICATE := platform
LOCAL_MODULE_CLASS := APPS
include $(BUILD_PREBUILT)

# AppBbb
include $(CLEAR_VARS)
LOCAL_MODULE := XxxAppBbb
LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppAbb.apk
LOCAL_CERTIFICATE := platform
LOCAL_MODULE_CLASS := APPS
include $(BUILD_PREBUILT)

# AppCcc
include $(CLEAR_VARS)
LOCAL_MODULE := XxxAppCcc
LOCAL_SRC_FILES := $(ROM_APPS_DIR)/AppCcc.apk
LOCAL_CERTIFICATE := platform
LOCAL_MODULE_CLASS := APPS
include $(BUILD_PREBUILT)

在上面的Android.mk中,每一个BUILD_PREBUILT,都依赖于rom_apps_out目录下一个APK文件; 而rom_apps_out目录下的每一个APK文件,都依赖于“buildRomApps”这个target; 而“buildRomApps”这个target是一个伪目标,其会去执行gradle构建命令。 就这样,我们触发了Gradle项目的构建,并把其生成的APK文件打包进了ROM。

2.2.1. 依赖规则细节

在前面的Android.mk文件中,只要当Gradle项目构建完毕后,才会去触发所有的BUILD_PREBUILT构建; 并且,构建任何一个BUILD_PREBUILT,都需要先构建整个Gradle项目。 我们能否只去建构所需要的Gradle module?能做到,但不能这样做!

先看如何做到:

vendor/xxx/YYYProject/Android.mk
$(GRADLE_PROJECT_ROOT)/$(ROM_APPS_DIR)/%.apk:
	$(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) :$*:assembleRomApps

为什么不能?我们在构建ROM时,一般会使用多个线程来并行构建,以提升构建速度(例如,make -j10)。 这导致多个APK的BUILD_PREBUILT可能被同时触发,从而导致Gradle项目同时被多次触发。 如果Gradle项目中有共用的library module,那么可能会出现建构异常。

2.3. 支持Overlay

我们知道,Android Gradle Plugin是可以通过buildTypes或者productFlavors来支持Overlay的; 另一方面,在执行Gradle构建命令时,也可以通过命令行参数向build.gradle脚本传递参数。 因此,我们可以把AOSP的Overlay目录,通过Gradle构建命令的参数传入到build.gradle脚本中, 并通过buildTypes或者productFlavors来实现Overlay。

首先,为了能够在AOSP和Gradle之间来协作支持Overlay,我们需要做一些约定:

  1. 在AOSP中定义一个变量ROM_APPS_OVERLAY_DIR,指向Overlay目录;

  2. 在Overlay目录下,是各应用的目录名;

  3. 在各应用目录下,有“res”等目录(一般我们只会用到res的Overlay)。

有了上述约定,支持Overlay的示例代码如下:

vendor/xxx/YYYProject/Android.mk
...

# Overlay path (must be relative path because of Android Gradle Plugin)
ifneq ($(ROM_APPS_OVERLAY_DIR),)
    GRADLE_OVERLAY_RES_DIR_PARAM := -PROM_RES_OVERLAY_DIR=../../../$(ROM_APPS_OVERLAY_DIR)
endif

...

.PHONY: buildTicwearApps
buildTicwearApps:
	$(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) assembleRomApps $(GRADLE_OVERLAY_RES_DIR_PARAM)

...

在上面Android.mk示例中,由于ROM_APPS_OVERLAY_DIR变量指向的目录名是相对AOSP代码根目录的, 而Gradle的Overlay又需要相对路径,因此在定义GRADLE_OVERLAY_DIR_PARAM变量时, 需要是一个有效的相对路径(跟上面这个Android.mk的相对路径相关)。

vendor/xxx/YYYProject/yyy_project_apps_common.gradle
android {
    ...

    sourceSets {
        romApps {
            if (project.hasProperty('ROM_RES_OVERLAY_DIR')) {
                def modulePath = projectDir.path.substring(rootDir.getParent().length());
                def resOverlayDir = "${ROM_RES_OVERLAY_DIR}${modulePath}/res"
                if (new File(rootDir, resOverlayDir).exists()) {
                    println "Overlay res dir found: " + resOverlayDir
                    res.srcDir resOverlayDir
                }
            }
        }
    }

    ...
}

2.4. 支持Clean

为了支持“make clean”,我们需要把Gradle的clean动作加进去:

build/core/main.mk
...

.PHONY: clean
clean:
        @rm -rf $(OUT_DIR)/*
        vendor/xxx/YYYProject/gradlew -p vendor/xxx/YYYProject clean
        @echo "Entire build directory removed."

...

另外,为了便于单独clean Gradle项目,可以为其添加一个clean target:

vendor/xxx/YYYProject/Android.mk
...

.PHONY: cleanGradleApps
cleanGradleApps:
	$(GRADLE_PROJECT_ROOT)/gradlew -p $(GRADLE_PROJECT_ROOT) clean

...

3. 优劣分析

先说好处,例举如下:

  1. 在开发应用时,不再需要同时支持Gradle和AOSP makefile两种构建方式;

  2. 开发调试应用的构建方式和最终ROM构建方式一致,不会出现因构建方式差异导致的问题;

  3. 仅支持Gradle构建方式,可以使我们充分利用Android Studio、Android Gradle Plugin、 Android Support Library、Android SDK等工具的新特性,来提升开发效率与质量 (例如,Jack工具链、Java 8语言特性)。

再来看看不利的地方:

  1. ROM构建时需要依赖于外部工具和网络(Android SDK和一些从网络上下载的工具包);

  2. ROM构建速度可能变慢(Gradle构建步骤比AOSP makefile复杂很多);

  3. 未知风险,需要实践才可知。。。