使用 Windows 主机进行 Android 代码远程编译

avatar
@比心

使用 Windows 主机进行 Android 代码远程编译

1 背景

MacBook Pro 编译速度慢,编译时性能消耗太大,页面卡顿严重。想着能不能把编译过程放到别的主机进行编译,利用性能较好的服务器云编译Android项目,再同步到本地,降低本地主机的性能?

刚好手上有一台 Windows 主机,可以直接利用起来。下文所说的目标主机或者远程主机就是这台 Windows 主机。

2 方案调研

首先进行了简单的方案调研。

2.1 mainframer

多年未更新,原有的插件不再适配新版本的 Android Studio。

2.2 mirakle

目前还在维护。需要导入依赖、需要修改 gradle 内部配置。对项目侵入性较强。

除了以上两个项目。另外还查阅了其它一些相关的文章。发现现有资料写的不够清楚、现有的开源方案配置看起来有点麻烦。大致总结出了实现原理。知道基本原理,自己实现也并不难。

大致原理如下图所示:

  • ① 在本地 MacBook 编写代码。想要运行的时候,将代码同步到远程主机
  • ② 远程主机编译代码
  • ③ 将远程主机的编译产物(apk)同步到本地 MacBook
  • ④ 执行 adb install,将 apk 文件安装到安卓手机

接下来就是开始着手搭建整个编译环境。

3 搭建编译环境

首先为远程 Windows 主机搭建 Android 编译环境。

3.1 JDK

(1)在 Oricle 官网下载我们需要的 JDK 版本

(2)配置环境变量

打开“控制面板”,选择“系统和安全”,然后单击“系统”。在左侧窗格中,选择“高级系统设置”,然后单击“环境变量”按钮。在“系统变量”部分中,找到“Path”变量并单击“编辑”按钮。在编辑环境变量窗口中,单击“新建”按钮,然后输入Java安装目录的路径

(3)最后使用 java -version 检查是否配置成功

3.2 Android SDK

同样的,在 Android 官网下载 SDK,并配置环境变量。

3.3 Chocolatey

3.3.1 Chocolatey 是什么

Chocolatey是一个免费开源的包管理器,可用于在 Windows 操作系统上安装、更新和管理软件包。它类似于Linux上的包管理器,如APT、yum和Homebrew。后面我们安装软件会用到,使用 Chocolatey 会方便很多。

3.3.2 怎么安装

以管理员身份运行 powershell

Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

3.3.3 怎么使用

一些常用的命令

choco search [package name] - 搜索指定名称的软件包。

choco install [package name] - 安装指定名称的软件包。

choco uninstall [package name] - 卸载指定名称的软件包。

choco upgrade [package name] - 升级指定名称的软件包。

choco list - 显示已安装的所有软件包。

choco outdated - 显示需要更新的所有已安装软件包。

choco upgrade all - 升级所有已安装的软件包。

choco sources list - 显示当前配置的所有软件包源。

choco source add -n [source name] -s [source URL] - 添加一个新的软件包源。

choco feature list - 显示所有可用的Chocolatey功能。

示例:

搜索 gradle

3.4 SSH

SSH 大家应该都比较熟悉。这里就不详细介绍了。

和平时我们用的一样,在 Windows 安装 SSH,并将我们本地的SSH公钥放到 Windows 主机上的 authorized_keys 文件中,实现免密验证。

配置成功后,使用以下命令登录到远程主机。

ssh user@ip_ad

3.4.1 遇到的问题

① Connection closed by

检查用户名和 IP 是否正确

② 免密登录无法使用

使用 PING 命令查看是否能PING通远程主机IP(注意先关闭远程Windows主机防火墙,否则 PING 不通)

如果能PING通,SSH 还是连接不上,检查 SSH 安装目录下的 config 文件。

修改 SSH 的 config 文件,将文件中最后两行注释掉。

3.5 Gradle

3.5.1 安装 Gradle

(1)使用 Chocolatey 查找 gradle 所有版本

choco search gradle --exact --all-versions

(2)使用 Chocolatey 安装我们需要的 Gradle 版本

使用 Chocolatey 安装指定版本的 Gradle。

choco install gradl --version 版本号

3.5.2 遇到的问题

(1)./gradlew 命令无法使用

出现如下图报错,是因为根目录下的 gradle.bat 文件没有执行权限。

右键 gradlew.bat 文件,点击属性。

设置项目根目录下的 gradlew.bat 文件权限为"完全控制"或设置为"读取和执行"

注意不要将本地的 gradlew.bat 文件同步到远程,否则会覆盖掉我们的修改(后面可以通过脚本自动设置这个文件的权限,就不需要手动设置了)。

PS:在 macOS 中有时候也会出现这种情况执行 chmod +x gradlew 命令即可。

(2)编译项目日志中的中文显示乱码

① 使用 ./gradlew 命令日志中中文显示乱码:

在项目的根目录找到 gradlew.bat 文件,找到 "DEFAULT_JVM_OPTS"设置字符集参数。

set DEFAULT_JVM_OPTS="-Dfile.encoding=UTF-8"

② 使用 gradle 命令日志中中文显示乱码:

同样找到我们安装 gradle 目录下的 gradle.bat 文件设置字符集参数即可

③ 系统级别输出中文乱码

在 mac 终端输出远程日志时会出现中文乱码。

是因为 Windows 和 Mac 系统使用的字符集不一致导致的。

修改远程 Windows 系统的字符集为 UTF-8。

设置路径:左下角搜索框输入"语言" → 点击打开"语言设置" → 选择"语言" → 点击"管理语言设置" → 点击"更改系统区域设置" → 勾选 UTF-8 → 重启系统

3.6 rsync

3.6.1 同步工具对比

rsync vs scp

  1. 传输方式:scp使用SSH进行传输,rsync也可以使用SSH传输,但也可以使用其他传输方式,如rsh和rsync协议。
  2. 传输速度:rsync的传输速度通常比scp更快,因为它使用了更高效的算法和数据压缩。
  3. 备份方式:rsync可以进行增量备份,即只传输文件的变化部分,而scp需要传输整个文件。
  4. 同步方式:rsync可以进行双向同步,即可以同步本地和远程主机之间的文件,而scp只能进行单向传输。
  5. 可靠性:rsync 在传输过程中具有更好的错误检查和恢复机制,可以更好地保证数据的完整性。

rsync 最大的优势在于可以进行增量传输,对于我们需要减少整个编译流程的耗时来说是非常重要的。

3.6.2 安装 rsync

rsync 是 Linux 系统使用的。有人根据 rsync 重新编写了可以在 Windows 使用的 rsync,名字叫 cwRsync。网上都是让我们去下载软件。但是提供的网址都是无法使用的,找到一些软件网站有提供,但是可能会有病毒。

使用 Windows 包管理工具 choco,执行 choco search 可以看到是有 rsync 的。我们直接使用 choco install rsync 直接就能安装了,很方便。

3.6.3 基本使用

在 Windows 中使用 rsync 基本和在 Linux\Mac 系统一样。

使用过程中遇到一个坑。Windows 系统的文件路径跟 Linux 系统不一样。所以不能直接按照 Linux 的路径使用命令。需要加上 cygdrive 前缀。

例:

/cygdrive/c/Android/CompanyProject_CDisk/

下面是鱼耳项目同步到远程的命令:

echo "开始本地同步远程: "
rsync -tavz --progress --delete \
--exclude 'module_sync_remote.sh' \
--exclude 'build/*' \
--exclude '/app/build/*' \
--exclude '*.idea' \
--exclude '.DS_Store' \
--exclude 'gradlew.bat' \
--exclude 'Doric/*' \
--exclude 'bixin/*' \
--exclude 'local.properties' \
--exclude '*/.gradle' \
--exclude '*.apk' \
../ \
你的远程主机名@$ip_ad:\
/cygdrive/c/Android/CompanyProject_CDisk/

3.7 其它配置

3.7.1 设置主机不关机

在 Windows 左下角打开设置,设置从不关闭即可。

3.7.2 配置检测 IP 变化脚本

放在公司的主机可以在外面打开 VPN 的情况下就可以连接上,但是由于我的主机用的不是固定 IP,有时候 IP 会变化。变化的时候我们需要知道变化后的IP,并更改我们本地的连接的 IP 地址。目前先为此在主机上配置了一个定时执行的脚本,获取变化后的主机IP。具体如下。

(1)Python 脚本

Python 脚本主要做的事情如下:

old_ipaddress = "IPv4 Address. . . . . . . . . . . : xxx.xxx.xxx.xxx"

import os
import re
import time
from smtplib import SMTP
from email.header import Header
from email.mime.text import MIMEText
def main():
    get_ipaddress()
def get_ipaddress():
    ifconfig_txt = os.popen('ipconfig').read()
    # print(ifconfig_txt)
    new_ipaddress = re.search('IPv4.*', ifconfig_txt)
    if new_ipaddress is not None:
        try:
            new_ipaddress = new_ipaddress.group()
            print(new_ipaddress)
        except AttributeError:
            print("ipaddress.group 出错")
            return
    else:
        print("IP 地址正则匹配失败")
        return

    try:
        if old_ipaddress != new_ipaddress:
            with open('send_ip_email.py', 'r+', encoding='utf-8') as fp:
                # send_ip_email.py是本程序脚本文件的文件名
                code = fp.readlines()[1:]
                fp.seek(0, 0)
                fp.truncate(0)
                fp.write('old_ipaddress = "%s"\n' % new_ipaddress)
                for c in code:
                    fp.write(c)
                email_msg = new_ipaddress
                send_msg(email_msg)
        else:
            print('原IP地址不变')
    except AttributeError:
        email_msg = '可能是正则表达式错误,请检查'
        send_msg(email_msg)
    except FileNotFoundError:
        email_msg = '文件名有误,请检查'
        send_msg(email_msg)
    except:
        email_msg = '程序出现错误,请检查'
        send_msg(email_msg)

def send_msg(msg):
    # 请自行修改下面的邮件发送者和接收者
    # 【发送者邮箱】
    sender = '你的邮箱@xx.com'
    # 【接受者邮箱】
    receivers = ['你的邮箱或另一个邮箱@xx.com']
    # 【邮件内容】
    message = MIMEText("remote host ip address:" + msg, 'plain', 'utf-8')
    # 【发件人】
    message['From'] = Header('remote—host', 'utf-8')
    # 【收件人】
    message['To'] = Header('收件人名字', 'utf-8')
    # 【邮件主题】
    message['Subject'] = Header('远程主机 IP,'+time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()), 'utf-8')
    smtper = SMTP('smtp.qq.com')
    # 【将secretpass改成自己的16位授权码】
    smtper.login(sender, '自己邮箱的16位授权码')
    smtper.sendmail(sender, receivers, message.as_string())
    print('邮件发送完成!')

if __name__ == '__main__':
    main()

(2)在 Windows 上配置定时任务

其它步骤网上都可以查到。

这里说一下配置执行任务的选项,这里容易出错。

  • 程序或脚本:选择我们下载到本地的 python.exe 程序(如果找不到执行命令 where.exe python,可以看到相应的路径)
  • 添加参数:选择我们写好的 python 脚本文件

4 在鱼耳项目中使用

在鱼耳项目中需要修改 gradle 文件的 Android SDK 路径。

local.gradle 中使用的本地的组件路径最好使用相对路径。

4.1 完整脚本

使用的时时候执行 sh scripName.sh 即可。

整个脚本文件所做的事情:

  • (1)排除不需要同步的文件
  • (2)本地项目文件同步远程
  • (3)ssh 连接远程并运行 gradle 命令,运行项目
  • (4)将远程编译好的 apk 文件同步到本地并安装
  • (5)调用 Mac 系统通知栏告知运行完成
#远程主机 IP 地址
ip_ad="xxx.xxx.xxx.xxx"
#函数:将毫秒转成分、秒
displaysecs() {
local T=$1
if [[ $T == 0 ]]
then
  echo "0s"
  return 0
fi

local D=$((T/60/60/24))

local H=$((T/60/60%24))

local M=$((T/60%60))

local S=$((T%60))

result=""

if [[ $D -gt 0 ]]
then
  result=$D"d "
fi

if [[ $H -gt 0 ]]
then
  result=$result$H"h "
fi

if [[ $M -gt 0 ]]
then
  result=$result$M"m "
fi

if [[ $S -gt 0 ]]
then
  result=$result$S"s"
fi

echo "$result"
}

#清屏
/usr/bin/osascript -e 'tell application "System Events" to tell process "Terminal" to keystroke "k" using command down'
startTime=$(date "+%Y-%m-%d %H:%M:%S")
startTimeS=$(date +%s)
#====================Start 本地同步远程 Start====================
echo "开始本地同步远程: "
rsync -tavz --progress --delete \
--exclude 'module_sync_remote.sh' \
--exclude 'build/*' \
--exclude '/app/build/*' \
--exclude '*.idea' \
--exclude '.DS_Store' \
--exclude 'gradlew.bat' \
--exclude 'Doric/*' \
--exclude 'bixin/*' \
--exclude 'local.properties' \
--exclude '*/.gradle' \
--exclude '*.apk' \
../ \
你的远程主机名@$ip_ad:\
/cygdrive/c/Android/CompanyProject_CDisk/
echo "同步结束......"
#====================End 本地同步远程 End====================
rsyncEndTimeSeconds=$(date +%s)
#====================Start 远程执行 Gradle 命令 Start====================
echo "远程执行gradle命令: " \

ssh -t 你的远程主机名@$ip_ad \
"
cd 远程主机项目所在路径;
#./gradlew sync
#./gradlew clean
./gradlew assembleInnerArm32Debug
exit;
"
#====================End 远程执行 Gradle 命令 End====================
runEndTimeSeconds=$(date +%s)

#====================Start 远程 APK 同步到本地 Start====================
echo "远程apk同步到本地"
rsync -av --progress \
你的远程主机名@$ip_ad:/cygdrive/c/远程主机apk所在路径/xxx/xxx/app/build/outputs/ \
../本地主机apk要保存的路径
remoteSyncLocalEndTimeSeconds=$(date +%s)
#====================End 远程 APK 同步到本地 End====================
installStartTimeSeconds=$(date +%s)
#====================Start 安装 APK Start====================
adb install-multiple -r -d  apk 文件所在路径
adb shell am start -n "包名/启动Activity"
installEndTimeSeconds=$(date +%s)
#====================End 安装 APK End====================
endTimeS=$(date +%s)
#====================Start 计算耗时 Start====================
rsyncElapsedTimeSeconds=$((rsyncEndTimeSeconds-startTimeS))
runElapsedTimeSeconds=$((runEndTimeSeconds-rsyncEndTimeSeconds))
remoteSyncLocalElapsedTimeSeconds=$((remoteSyncLocalEndTimeSeconds-runEndTimeSeconds))
installElapsedTimeSeconds=$((installEndTimeSeconds-installStartTimeSeconds))
totalElapsedTimeSeconds=$((endTimeS-startTimeS))
#echo $spendTimeS"s"
rsyncElapsedTimeString=$(displaysecs rsyncElapsedTimeSeconds)
runElapsedTimeString=$(displaysecs runElapsedTimeSeconds)
remoteSyncLocalElapsedTimeString=$(displaysecs remoteSyncLocalElapsedTimeSeconds)
installElapsedTimeString=$(displaysecs installElapsedTimeSeconds)
totalElapsedTimeString=$(displaysecs totalElapsedTimeSeconds)
echo $installElapsedTimeSeconds
echo "\033[32m 本次文件同步耗时: \033[0m \033[33m$rsyncElapsedTimeString\033[0m"
echo "\033[32m 本次运行项目耗时: \033[0m \033[33m$runElapsedTimeString\033[0m"
echo "\033[32m 远程APK同步本地: \033[0m \033[33m$remoteSyncLocalElapsedTimeString\033[0m"
echo "\033[32m 安装APK耗时    : \033[0m \033[33m$installElapsedTimeString\033[0m"
echo "\033[31m 本次运行总耗时  : \033[0m \033[33m$totalElapsedTimeString\033[0m"
#====================End 计算耗时 End====================
# macOS 右上角通知弹窗
/usr/bin/osascript -e "display notification " 远程运行完成,耗时:$totalElapsedTimeString " with title "远程运行完成""

4.2 编译耗时

具体耗时跟所使用的主机性能有关。以下是编译耗时的参考数据。Windows 栏括号内的时间是相互同步文件的耗时。

yuer-modul-01、yuer-modul-02、yuer-modul-03、yuer-modul-04、yuer-modul-05等5个比较常改动的组件使用本地源码进行编译。

操作步骤:

先执行 gradle clean 后,执行 gradle install。

使用的设备:

MacBook Pro:处理器:2 GHz 四核Intel Core i5 8 代,内存:16GB

Remote Host(Windows):处理器:桌面版 11 代 i5 11400 6核12线程 2.60 GHz,内存:40GB

12345平均耗时
MacBook4'13''4'09''4'29''4'08''4'33''4'18''
Remote Host(Win)2'23''(7''、2'')2'16(8''、4'')2'16(8''、4'')2'16''(6''、3'')2'17''(8''、5'')2'18''(7.4''、3.2'')

5 后续可优化

  • (1)将脚本封装成插件
  • (2)使用 inotify 配合 rsync 实现即时同步代码,或者直接使用 lsyncd 实现即时同步代码,以减少代码同步的耗时
  • (3)将主机系统从 Windows 换成 Linux
  • (4)加入一键配置运行环境的功能
  • (5)使用更多核心、性能更好的 CPU,增加内存条

6 总结

实现了最初的目标。本地 MacBook 不再受项目编译的影响。MacBook 基本不再卡顿。

不仅是 Android 项目,其它类型的项目基本上按照以上步骤也可以实现在远程主机上进行编译。

image.png