背景
安卓开源项目(AOSP)中各个应用模块的开发编译方式采用的是 make 编译方式,每个模块需要通过定义 Android.mk 或者 Android.bp 文件配置编译规则。相对于主流的普通 App 的 gradle 编译方式有很多的缺点。比如:
- 编译速度慢,每次 make 编译都要遍历查询编译所有依赖模块,即使使用 mm 仅编译当前模块的编译方式也没有 gradle 速度快。
- 集成依赖第三方库不够灵活。make 编译方式只能采用本地依赖,不像 gradle 的远程依赖那么灵活。开发过程中经常依赖了很多远程 maven 库,每次升级,都要手动下载集成到 libs 目录下,不仅麻烦,而且使得 git 仓库变得越来越大。另外make 编译方式不支持直接引用 aar 的库。
- make 编译不支持主流的 android 开发语言 kotlin。
由此可见,系统应用的 make 编译方式对于习惯了使用 gradle 编译方式的开发是非常不友好的,为了帮助普通开发者能够快速的参与到系统应用的开发,本文探索了支持 make 和 gradle 编译两种方式的最佳途径。
思考
我们的目的是像开发普通应用一样来开发系统应用,并且开发的应用可以随着系统编译集成到 ROM 中。
先看看有哪些可能的实现方式:
方式一,完全采用 make 的编译方式。相对与 gradle 编译方式很很多缺陷(如背景介绍),势必对开发效率影响很大。
方式二,只采用 gradle 的编译方式。由于系统编译采用的 make 编译,开发的应用无法随着 ROM 编译集成到系统中。需要通过预置应用的方式将提前编译好的apk 文件放在预置应用目录进行集成。带来的问题是:
- 由于应用的迭代比较频繁,手动去更新预置 apk 实现繁琐,维护复杂;
- 人工操作容易出错,忘记更新,更新错误都会导致效率降低;
- apk 更新带来大量无用的 git 历史记录,导致 git 仓库变得越来越大。
有的公司采用的方式是,实现jenkins自动化,监控应用代码入库,自动编译上传 apk。ROM 编译时,自动下载最新 apk 到预置应用目录。这种方案可以解决上面的问题,但实现成本较高,需要搭建自动化配置,服务器云存储,版本管理等。
方式三,实现同时支持 make和 gradle 编译方式。支持 make 编译便于 ROM 编译集成,支持 gradle 编译,提升应用开发效率,支持独立编译(不需要下载aosp 其他源码)。
实现同时支持 make和 gradle 编译方案实现
实现思路,通过修改 Android.mk 脚本,触发 gradle 进行编译,将编译后的 apk 输出到预置应用路径。
比如下面是 HomepadLauncher 应用的 Android.mk 文件内容,为方便理解,本文添加了中文注释介绍了实现的关键点。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# Module name should match apk name to be installed
LOCAL_MODULE := HomepadLauncher
# 根据 ROM 编译类型是否为 userdebug 来决定 gradle 编译 assembleDebug 或 assembleRelease 的版本。
ifeq ($(TARGET_BUILD_VARIANT), userdebug)
LOCAL_BUILD_TYPE = assembleDebug
else
LOCAL_BUILD_TYPE = assembleRelease
endif
# 触发 checkChanges.sh 脚本检查本地代码是否变化,仅当代码有变化后才执行 gradle 编译,避免重复编译。
LOCAL_CHANGE_RESULT := $(shell cd $(LOCAL_PATH) && ./checkChanges.sh $(LOCAL_BUILD_TYPE))
$(info "check changes return $(LOCAL_CHANGE_RESULT)")
ifneq ($(LOCAL_CHANGE_RESULT), 0)
# run gradle script for generating apk file
$(info "==================== START build $(LOCAL_MODULE) $(LOCAL_BUILD_TYPE)")
# buildApk.sh 脚本传入编译类型参数,执行 gradle 的编译
$(info $(shell cd $(LOCAL_PATH) && ./buildApk.sh $(LOCAL_BUILD_TYPE)))
$(info "==================== END build $(LOCAL_MODULE) $(LOCAL_BUILD_TYPE)")
endif
LOCAL_MODULE_TAGS := optional
# 指定直接使用 gradle 编译生成的 apk,不会真正通过 make 编译源码
LOCAL_SRC_FILES := app/build/outputs/apk/$(LOCAL_BUILD_TYPE).apk
LOCAL_MODULE_CLASS := APPS
LOCAL_CERTIFICATE := PRESIGNED
include $(BUILD_PREBUILT)
重点任务在 buildApk.sh 脚本中,这个脚本可不是简单的执行 ./gradlew assembleDebug 或 ./gradlew assembleRelease 命令那么简单。我们在开发时使用 android studio 会自动下载 android sdk 和 gradle 为我们提前初始化好应用的运行环境。但是安卓系统源码的编译通常部署在远程的编译服务器中,如果没有提前下载好 android sdk 和 gradle 是无法执行 gradle 编译的。因此 buildApk.sh 内部的执行流程如下图所示。
我们逐一介绍下部分脚本的功能实现。
下载安装安卓 sdk 的脚本,installAndroidSDK.sh:
#!/bin/bash
echo "start installAndroidSdk.sh..."
ANDROID_SDK_PATH=$HOME/Android/Sdk
echo "use android sdk path: $ANDROID_SDK_PATH \n"
SDK_MGR_PATH=$HOME/Android/sdkmgr
mkdir -p $SDK_MGR_PATH && cd $SDK_MGR_PATH || exit 1
if [ ! -e "${SDK_MGR_PATH}/commandlinetools-linux-6200805_latest.zip" ]; then
echo "start download sdk manager ..."
#wget https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip
# 墙的原因,国内采用了腾讯的镜像
wget https://mirrors.cloud.tencent.com/AndroidSDK/commandlinetools-linux-6200805_latest.zip
if [ $? -ne 0 ]; then
echo "Download android sdkmanager failed!!!"
rm commandlinetools-linux-6200805_latest.zip
exit 1
fi
unzip commandlinetools-linux-6200805_latest.zip
if [ $? -ne 0 ]; then
echo "unzip commandlinetools failed!!!"
rm commandlinetools-linux-6200805_latest.zip
exit 1
fi
fi
export PATH="$SDK_MGR_PATH/tools/bin:${PATH}"
echo "start download android sdk..."
#PROXY_OPTS="--no_https --proxy=http --proxy_host=mirrors.neusoft.edu.cn --proxy_port=80"
PROXY_OPTS="--no_https"
yes | sdkmanager --sdk_root=$ANDROID_SDK_PATH --licenses $PROXY_OPTS
# 此处下载的 android sdk 版本需要根据项目具体使用的版本进行调整
sdkmanager --verbose --sdk_root=$ANDROID_SDK_PATH "platform-tools" "platforms;android-29" "build-tools;28.0.3" $PROXY_OPTS
if [ $? -ne 0 ]; then
echo "download android sdk failed!!!"
exit 1
fi
echo "download android sdk success~~"
cd -
echo "return directory: " && pwd
# 生成本地的 local.properties 文件,配置 sdk 路径
echo sdk.dir=$ANDROID_SDK_PATH > local.properties
下载 gradle 的脚本 downloadGradle.sh (本人项目中用的是 5.4.1版本,如需借鉴,请根据项目依赖的实际版本进行调整):
#!/bin/bash
echo "start downloadGradle.sh"
GRADLE_DOWNLOAD_PATH=~/.gradle/wrapper/dists/gradle-5.4.1-all/3221gyojl5jsh0helicew7rwx
if [ -e ${GRADLE_DOWNLOAD_PATH}/gradle-5.4.1-all.zip.ok ];then
echo "gradle already readly"
exit 0
fi
rm -r $GRADLE_DOWNLOAD_PATH
echo "begin download gradle..."
wget -P $GRADLE_DOWNLOAD_PATH https://mirrors.cloud.tencent.com/gradle/gradle-5.4.1-all.zip
if [ $? -ne 0 ]; then
rm -r $GRADLE_DOWNLOAD_PATH
echo "download gradle failed!!!"
exit 1
else
echo "download gradle success"
fi
检查代码是否变化的脚本 checkChanges.sh:
#!/bin/bash
# check current repo changes, if no changes, return 0
if [ ! -e app/build/outputs/apk/$1.apk ]; then
exit 1
fi
if [ -e app/build/lastBuildMD5.txt ]; then
LAST_MD5=`head app/build/lastBuildMD5.txt`
#echo "last md5 is $LAST_MD5"
CURRENT_MD5=`find . -type f -not -path "*/build/*" -not -path "*./.*" -exec stat \{\} -c "%y\n" \; | sort -n -r | md5sum | cut -f 1 -d ' '`
#echo "current md5 is $CURRENT_MD5"
if [ $LAST_MD5 == $CURRENT_MD5 ]; then
# no changes
echo "0"
exit 0
else
echo "code changed"
exit 1
fi
else
exit 1
fi
buildApk.sh 脚本:
#!/bin/bash
function useJdk()
{
export JAVA_HOME=$1
export ANDROID_JAVA_HOME=$1
}
grep sdk.dir local.properties || ./installAndroidSDK.sh
if [ $? -ne 0 ]; then
echo "install android sdk failed!!!"
exit 1
fi
./downloadGradle.sh
if [ $? -ne 0 ]; then
echo -e "downloadGradle failed!!!"
exit 1
fi
origin_java_path=$JAVA_HOME
root_path=$(dirname $(dirname $(dirname "$PWD")))
jdk8_path=$root_path"/prebuilts/jdk/jdk8/linux-x86"
# android 10 运行环境用的 java9,gradle 需要使用 java8,此处切换 java 环境
useJdk $jdk8_path
./gradlew clean
#rm -fr ./app/build/outputs
./gradlew $1
if [ $? -ne 0 ]; then
echo -e "build $PWD $1 failed!!!"
useJdk $origin_java_path
exit 1
fi
cp ./app/build/outputs/apk/*/*.apk ./app/build/outputs/apk/$1.apk
# save current files snapshot md5
find . -type f -not -path "*/build/*" -not -path "*./.*" -exec stat \{\} -c "%y\n" \; | sort -n -r | md5sum | cut -f 1 -d ' ' > app/build/lastBuildMD5.txt
echo "build $PWD $1 success"
useJdk $origin_java_path
所有脚本需要放在与应用项目的Android.mk 文件同级目录下。
遇到的问题
- 脚本中的部分命令比如(wget, yes)无法执行,需要配置下文件 build/soong/ui/build/paths/config.go, 允许使用这2个命令。
- gradle 编译时报错
Exception in thread "main" java.lang.RuntimeException: Could not create parent directory for lock file /nonexistent/.gradle/wrapper/dists/gradle-5.4.1-all/3221gyojl5jsh0helicew7rwx/gradle-5.4.1-all.zip.lck
at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:43)
at org.gradle.wrapper.Install.createDist(Install.java:48)
at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:107)
at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)
解决方法,需要禁用 sandbox,修改文件 build/soong/ui/build/sandbox_linux.go:
由于真实编译采用的是 gradle 编译,基于 android sdk 而非系统源码,因为无法直接引用系统私有 api 以及新增的系统 api。本方案对于系统私有接口依赖不强的收益比较大。针对新增系统接口,可以采用占位编译 framework.jar 的方式实现,或者搭建系统级的基础库。本文不再赘述。
小结
本文介绍了aosp 系统应用支持 gradle 编译集成的方案,方便普通应用开发者无缝的参与系统应用的开发,支持独立编译。大大提高了开发效率。