用Gradle腳本管理Manifest文件

編譯時區分不同的manifest

很多Android項目都會區分debug和release的manifest文件,以便調試,一些組件化的項目甚至有多個manifest文件來調試不同的組件。舉個簡單的例子,在app的build.gradle文件中:

android {
    defaultConfig {
        applicationId "com.xxx.xxx"
    }
    sourceSets {
        main {
            if(是否爲debug打包) {
                manifest.srcFile "${projectDir}/src/main/debug/AndroidManifest.xml"
            } else {
                manifest.srcFile "${projectDir}/src/main/release/AndroidManifest.xml"
            }
        }
    }
}

這個地方的if條件,一般可以是一個變量,或者一個方法:

// 這樣
def isDebug = true
// 或者
def isDebug() {
    // 一些邏輯
    return true
}
// 以上均遵循Groovy語法規則,Android項目中的Gradle腳本都是用Groovy語言(一門JVM動態語言,很容易上手)編寫的。
android { ... }

然後此處的 ${projectDir} 變量對應的是當前模塊所在的路徑,這裏就是app的路徑,不是整個工程的路徑。這樣一來,我們就能編譯不同的manifest文件了。

自動判斷編譯類型

但是,這樣每次編譯都需要手動改那個debug變量,挺麻煩的,尤其是一些技術團隊可能是用的公司服務器在線編譯,每次都要提交代碼到倉庫。其實我們可以用gradle插件的特性,這樣來判斷:

// 這裏app是當前模塊名,assembleDebug是gradle編譯debug包時的默認task
if(gradle.startParameter.taskNames.contains(":app:assembleDebug")) {
    manifest.srcFile "${projectDir}/src/main/debug/AndroidManifest.xml"
} else {
	manifest.srcFile "${projectDir}/src/main/release/AndroidManifest.xml"
}

不用改代碼,打release包和debug包就自動區分了,當然你也可以增加新的條件,去對應不同的編譯task。

解析並自動生成manifest文件

這部分是本文重點啦!從上面的步驟來看,我們顯然是準備了2份manifest文件,平時維護時也需要一起修改。實際業務中很可能debug和release的manifest內容差不多,可能只是某些組件節點的屬性不同,手動改也挺麻煩的。

能不能通過gradle腳本動態地來修改並生成manifest文件呢?當然可以,本質上就是處理XML文件。AndroidManifest是標準的XML文件。正好,Groovy處理XML又非常的簡單。這裏我們還是以一個實際例子來講。

比如我們的需求是,在debug測試時,應用正常顯示桌面圖標,在release發佈時,應用需要隱藏圖標。那麼,兩個manifest文件就是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application ...>
        <!--App入口頁面-->
        <activity
            ...
            android:name=".TestActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
...

release版本的manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <application ...>
        <!--App入口頁面-->
        <activity
            ...
            android:name=".TestActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <!--在入口Activity添加如下data節點即可隱藏桌面圖標-->
                <data 
                    android:host="localhost" 
                    android:scheme="${applicationId}" />
            </intent-filter>
        </activity>
...

然後平時我們開發過程中,如果新增了一些四大組件,2個文件都要同時增加,而區別卻只有入口Activity。我們想要的是release版本的manifest是自動生成的(在生成時插入那個data節點),平時只需要開發改動debug版本的文件即可。

主要思路比較簡單:

  1. 通過Groovy的XML解析庫讀取debug的manifest文件,遍歷節點找到入口Activity;
  2. 將data節點插入到入口Activity下面;
  3. 把新的內容寫入release版本的文件當中。

先直接看build.gradle腳本源碼:

import groovy.xml.XmlUtil

def getManifestPath(buildType) {
    return "${projectDir}/src/main/$buildType/AndroidManifest.xml"
}

def handleManifestXml(manifest) {
    if (gradle.startParameter.taskNames.contains(":app:assembleDebug")) {
        // debug build
        def debugPath = getManifestPath('debug')
        manifest.srcFile debugPath
        println("Manifest path: $debugPath")
    } else {
        // release build
        def releasePath = getManifestPath('release')
        println("Manifest path: $releasePath")
        manifest.srcFile releasePath

        def debugPath = getManifestPath('debug')
        def debugFile = new File(debugPath)
        def releaseFile = new File(releasePath)
        def debugXml = new XmlParser(false, false).parse(debugFile)
        // 修改debug的manifest,自動生成release版本
        debugXml.application[0].each { comp ->
            if (comp.name() == 'activity') {
                comp.each { filter ->
                    // 搜索入口Activity
                    if (filter.toString().contains('android.intent.category.LAUNCHER')) {
                        // 添加data節點,隱藏桌面icon
                        filter.appendNode('data', ['android:host': 'localhost', 'android:scheme': '${applicationId}'])
                        return true // break each閉包
                    }
                }
            }
        }
        releaseFile.write(XmlUtil.serialize(debugXml))
    }
}

android {
    defaultConfig {
        applicationId "com.xxx.xxx"
    }
    sourceSets {
        main {
            // 此處會自動處理manifest文件
            // 注意:平時開發時只需修改debug下的manifest即可,請勿手動修改release的
            handleManifestXml(manifest)
        }
    }
...

關鍵邏輯從 def debugXml = new XmlParser(false, false).parse(debugFile) 開始,這裏的XmlParser構造方法可以不傳任何參數,我這裏傳false主要是爲了讓manifest根節點自動添加 xmlns ,這樣最後生成文件內容會簡潔一點。

然後就是 debugXml.application[0].each { comp -> ... } 循環塊,Groovy的語法很神奇,這裏可以直接通過節點名稱來獲取數組,比如application,取0當然就是第一個,一般我們的manifest裏就一個application節點,所以也不會出現空指針異常。each是數組的函數,遍歷數組的每一個節點,comp參數是我自定義的命名。能拿到application的子節點,後面就都是一些邏輯處理了,不贅述。

一些小問題

如果是Windows開發者,最後寫文件時,需要注意換行符和編碼的問題。

1、最後的write方法,加上參數:

releaseFile.write(xmlStr, "UTF-8")

或者直接改全局配置,修改整個工程下的gradle.properties文件,在gradle的JVM參數後面追加如下:

org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

2、換行符問題,沒有JVM參數可供修改,只能手動處理,還是最後write時:

releaseFile.write(XmlUtil.serialize(debugXml).replaceAll('\r\n', '\n'))

這裏XmlUtil工具類的serialize方法返回類型實際上就是String,所以可以這樣直接replace。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章