Android Gradle原理分析系列(一):解析Gradle Wrapper

前言

新建一個AS工程,編譯器會自動幫咱們建立了Gradle Wrapper相關的文件,目錄結構以下:java

├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat
複製代碼

這裏有幾個問題:git

  1. 這個gradlew和gradle有什麼區別?github

  2. 咱們在gradle-wrapper.properties中配置的信息是如何被解析的?能配置的項又有哪些?windows

  3. 若是網絡很差,如何本身手動下載gradle壓縮包配置版本庫?數組

下面會分析Gradle Wrapper的實現原理,解答這幾個問題。網絡

從gradlew談起

通常咱們編譯Android項目,不會直接執行gradle命令,而是執行gradlew xxxx,而這個gradlew,其實就是執行當前目錄下的gradlew.bat(windows平臺下),因此先看一下這個批處理文件究竟執行了什麼命令:app

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
複製代碼

這裏會去運行/gradle/wrapper/目錄下的gradle-wrapper.jar,而後執行其org.gradle.wrapper.GradleWrapperMain類的main方法:gradle

public static void main(String[] args) throws Exception {
    File wrapperJar = wrapperJar();
    File propertiesFile = wrapperProperties(wrapperJar);
    File rootDir = rootDir(wrapperJar);

    CommandLineParser parser = new CommandLineParser();
    parser.allowUnknownOptions();
    parser.option(GRADLE_USER_HOME_OPTION, GRADLE_USER_HOME_DETAILED_OPTION).hasArgument();
    parser.option(GRADLE_QUIET_OPTION, GRADLE_QUIET_DETAILED_OPTION);

    SystemPropertiesCommandLineConverter converter = new SystemPropertiesCommandLineConverter();
    converter.configure(parser);

    ParsedCommandLine options = parser.parse(args);

    Properties systemProperties = System.getProperties();
    systemProperties.putAll(converter.convert(options, new HashMap<String, String>()));

    File gradleUserHome = gradleUserHome(options);

    addSystemProperties(gradleUserHome, rootDir);

    Logger logger = logger(options);

    WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(propertiesFile);
    wrapperExecutor.execute(
            args,
            new Install(logger, new Download(logger, "gradlew", UNKNOWN_VERSION), new PathAssembler(gradleUserHome)),
            new BootstrapMainStarter());
}
複製代碼

調用wrapperProperties方法找到當前目錄下的gradle-wrapper.properties文件,該文件是用來指定一些配置信息的,後面會分析,先無論,接着會解析命令行傳遞過來的參數,最後一步是關鍵,會調用WrapperExecutor對象的execute方法,傳入了一個Install對象,這個是用來處理gradle的下載,若是指定版本的gradle在指定的gradle倉庫中沒有,就先下載解壓,execute方法傳入的另外一個對象是BootstrapMainStarter對象,這個是用來啓動執行gradle的幫助類,最終會執行。接下來詳細分析這兩步的內部實現原理:優化

  1. Install對象下載安裝gradle庫url

    public File createDist(final WrapperConfiguration configuration) throws Exception {
         final URI distributionUrl = configuration.getDistribution();
         final String distributionSha256Sum = configuration.getDistributionSha256Sum();
    
         final PathAssembler.LocalDistribution localDistribution = pathAssembler.getDistribution(configuration);
         final File distDir = localDistribution.getDistributionDir();
         final File localZipFile = localDistribution.getZipFile();
    
         return exclusiveFileAccessManager.access(localZipFile, new Callable<File>() {
             public File call() throws Exception {
                 final File markerFile = new File(localZipFile.getParentFile(), localZipFile.getName() + ".ok");
                 if (distDir.isDirectory() && markerFile.isFile()) {
                     InstallCheck installCheck = verifyDistributionRoot(distDir, distDir.getAbsolutePath());
                     if (installCheck.isVerified()) {
                         return installCheck.gradleHome;
                     }
                     // Distribution is invalid. Try to reinstall.
                     System.err.println(installCheck.failureMessage);
                     markerFile.delete();
                 }
    
                 boolean needsDownload = !localZipFile.isFile();
                 URI safeDistributionUrl = Download.safeUri(distributionUrl);
    
                 if (needsDownload) {
                     File tmpZipFile = new File(localZipFile.getParentFile(), localZipFile.getName() + ".part");
                     tmpZipFile.delete();
                     logger.log("Downloading " + safeDistributionUrl);
                     download.download(distributionUrl, tmpZipFile);
                     tmpZipFile.renameTo(localZipFile);
                 }
    
                 List<File> topLevelDirs = listDirs(distDir);
                 for (File dir : topLevelDirs) {
                     logger.log("Deleting directory " + dir.getAbsolutePath());
                     deleteDir(dir);
                 }
    
                 verifyDownloadChecksum(configuration.getDistribution().toString(), localZipFile, distributionSha256Sum);
    
                 try {
                     unzip(localZipFile, distDir);
                 } catch (IOException e) {
                     logger.log("Could not unzip " + localZipFile.getAbsolutePath() + " to " + distDir.getAbsolutePath() + ".");
                     logger.log("Reason: " + e.getMessage());
                     throw e;
                 }
    
                 InstallCheck installCheck = verifyDistributionRoot(distDir, safeDistributionUrl.toString());
                 if (installCheck.isVerified()) {
                     setExecutablePermissions(installCheck.gradleHome);
                     markerFile.createNewFile();
                     return installCheck.gradleHome;
                 }
                 // Distribution couldn't be installed.
                 throw new RuntimeException(installCheck.failureMessage);
             }
         });
     }
    複製代碼

    代碼比較長,但思路很清晰,主要是注意一些細節問題,有利於平時開發,主要步驟以下:

    (1)首先是拿到distributionUrl,這個就是gradle的下載地址,也就是咱們在gradle-wrapper.properties文件中配置的那個連接。

    (2)而後調用ExclusiveFileAccessManager的access方法,這個方法主要是去lock一個lck文件,這個文件的做用就是實如今某個時刻只能有一個使用者在使用該gradle庫,因此下載前須要lock住這個文件。

    (3)lock後,繼續判斷文件是否存在,若是已經存在了,就返回,不然就開始下載,下載存放目錄是經過gradle-wrapper.properties文件的distributionPath屬性配置的,下載時會先下載到一個part文件,等下載完成了,就重命名爲最終的文件名,這裏有個細節,每次下載都會刪除掉以前的part文件,也就是說這裏並不支持斷點下載,若是文件比較大而後又由於網絡緣由下載失敗了,下次仍是得從新下載,可能又會繼續失敗,而後一直反反覆覆就會很煩,這裏咱們能夠直接修改代碼優化。

    (4)下載後,就要校驗sha256,不過只有在gradle-wrapper.properties文件中配置了sha256碼的狀況下才會進行校驗,沒配置的就跳過。

    (5)接着是解壓,由於下載下來是一個zip文件,解壓後文件的存放目錄是經過gradle-wrapper.properties文件的distributionPath屬性配置的。

    (6)最後是簡單校驗解壓後的文件是否正確,好比是否有gradle-launcher-xxx.jar之類。若是校驗經過,還會建立一個ok文件,用於表示該gradle庫下載校驗經過,這個gradle庫能夠被正常使用。

    (7)ExclusiveFileAccessManager的access方法unlock前面那個lck文件,至此流程結束,該gradle能夠被使用了。

    下載流程說完了,這裏還有個問題,就是咱們去看了一下下載後的目錄,以下:

    C:\Users\mai\.gradle\wrapper\dists\gradle-5.4.1-all\3221gyojl5jsh0helicew7rwx
    複製代碼

    咱們發現這個路徑跟distributionPath指定的仍是有點不一樣,gradle-5.4.1-all目錄還能夠解釋得通,不一樣版本確定要放到不一樣目錄下,因此乾脆就用版本名命名目錄就行了,但咱們還看到下一級目錄名爲3221gyojl5jsh0helicew7rwx,這個就不太容易看得出來了,這時咱們就去代碼裏查找:

    public LocalDistribution getDistribution(WrapperConfiguration configuration) {
         String baseName = getDistName(configuration.getDistribution());
         String distName = removeExtension(baseName);
         String rootDirName = rootDirName(distName, configuration);
         File distDir = new File(getBaseDir(configuration.getDistributionBase()), configuration.getDistributionPath() + "/" + rootDirName);
         File distZip = new File(getBaseDir(configuration.getZipBase()), configuration.getZipPath() + "/" + rootDirName + "/" + baseName);
         return new LocalDistribution(distDir, distZip);
     }
    
     private String rootDirName(String distName, WrapperConfiguration configuration) {
         String urlHash = getHash(Download.safeUri(configuration.getDistribution()).toString());
         return distName + "/" + urlHash;
     }
    
     private String getHash(String string) {
             try {
                 MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                 byte[] bytes = string.getBytes();
                 messageDigest.update(bytes);
                 return new BigInteger(1, messageDigest.digest()).toString(36);
             } catch (Exception e) {
                 throw new RuntimeException("Could not hash input string.", e);
             }
     }
    複製代碼

    getHash方法的返回值就是這個3221gyojl5jsh0helicew7rwx目錄名,能夠看到它是計算傳入的string的MD5值字節數組,而後轉成BigInteger,最後用36進製表示返回字符串,而這個string的值,就是gradle的下載地址https://services.gradle.org/distributions/gradle-5.4.1-all.zip,咱們能夠本身調用getHash方法,傳入這個地址,發現返回值就是3221gyojl5jsh0helicew7rwx,說明咱們的分析是正確的。

    總結下,下載流程涉及到幾個文件,lck文件用於實現使用互斥,part文件是下載過程當中的臨時文件,ok文件表示該gradle庫能夠正常使用,若是沒有ok文件,執行gradlew命令後會致使從新解壓(不存在zip文件就先下載)。

  2. BootstrapMainStarter對象啓動執行gradle庫

    這個就比較簡單了,BootstrapMainStarter的start方法:

    public void start(String[] args, File gradleHome) throws Exception {
         File gradleJar = findLauncherJar(gradleHome);
         if (gradleJar == null) {
             throw new RuntimeException(String.format("Could not locate the Gradle launcher JAR in Gradle distribution '%s'.", gradleHome));
         }
         URLClassLoader contextClassLoader = new URLClassLoader(new URL[]{gradleJar.toURI().toURL()}, ClassLoader.getSystemClassLoader().getParent());
         Thread.currentThread().setContextClassLoader(contextClassLoader);
         Class<?> mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain");
         Method mainMethod = mainClass.getMethod("main", String[].class);
         mainMethod.invoke(null, new Object[]{args});
         if (contextClassLoader instanceof Closeable) {
             ((Closeable) contextClassLoader).close();
         }
     }
    複製代碼

    能夠看到,這裏反射調用gradle-launcher-xxx.jar中的org.gradle.launcher.GradleMain的main方法,這個GradleMain類,也就是gradle的入口類,至於接下來的流程,後面的文章會繼續分析。

總結,gradlew的做用就是在執行gradle前,確保gradle倉庫中有項目所須要的gradle版本,若是沒有就會先下載解壓準備好。

gradle-wrapper.properties文件

這個文件用來指定gradle-wrapper.jar執行所須要的配置信息,主要有五種信息:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
複製代碼
  • distributionBase

    指定gradle庫解壓後存放目錄的根目錄,默認設置爲GRADLE_USER_HOME,這個其實就是環境變量名,若是沒有設定,就會採用默認的gradle倉庫根目錄,windows下爲C:\Users\用戶名.gradle,若是要本身指定到某個目錄,就去新建個叫GRADLE_USER_HOME的環境變量,而後路徑填本身的路徑就行,這段邏輯在GradleUserHomeLookup.java類裏,有興趣能夠本身查看。

    distributionBase還有另一個取值就是PROJECT,表示當前工程的根目錄。

  • distributionPath

    指定gradle庫解壓後存放目錄的子目錄。

  • zipStoreBase

    指定下載的gradle壓縮包的存放目錄的根目錄。

  • zipStorePath

    指定下載的gradle壓縮包的存放目錄的子目錄。

  • distributionUrl

    指定gradle庫的下載地址。

手動下載gradle

有時由於各類緣由,執行gradlew後下載gradle比較慢,甚至失敗,根據上面對gradlew下載流程的分析,咱們能夠本身去網上下載,而後按照規則存放文件,就能夠直接使用了,好比說咱們要下載gradle-5.6.4-all,具體的步驟以下:

  1. 在C:\Users\mai.gradle\wrapper\dists下新建目錄gradle-5.6.4-all,進入目錄。

  2. 容許上面所說的getHash方法,傳入https://services.gradle.org/distributions/gradle-5.6.4-all.zip,獲得ankdp27end7byghfw1q2sw75f,以該字符串新建目錄。

  3. 去網上下載gradle-5.6.4-all.zip,放到ankdp27end7byghfw1q2sw75f目錄下,解壓。

  4. 新建gradle-5.6.4-all.zip.lck和gradle-5.6.4-all.zip.ok兩個文件。

這樣就配置好了,咱們把gradle-wrapper.properties的distributionUrl改成https://services.gradle.org/distributions/gradle-5.6.4-all.zip,而後執行gradlew命令,發現已經檢測到咱們配置的gradle庫了,不會再去下載。

參考資料

github.com/gradle/grad…

相關文章
相關標籤/搜索