Android XML资源合并工具

在做聚合SDK开发时,Android方向会遇到从Java编译到APK打包的一系列问题,其中有一项就是需要合并多个工程中AndroidManifest.xml以及strings.xml、styles.xml中的信息,这样才能使用aapt工具生成正确的R.java文件。

但由于AndroidManifest.xml中,字段名中包含 "android:" 带冒号的特殊字符串,所以还无法使用序列化解析XML的方式处理。

目前我通过解码的方式读取出XML中的所有字段信息,保存在结构体的数组对象中,这样既方便记录字段信息,又方便做去重处理。直到全部XML处理完毕,再将结构体生成成一个新的XML文件导出。

1. AndroidManifest.xml合并代码

参数:
-m : 主要XML文件的路径,相同属性以主XML为准。
-l : 合并XML文件的路径。
-o : 导出新XML文件的路径。

package main

import (
    "bytes"
    "encoding/xml"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

// TService 服务
type TService struct {
    MainPath              string
    LinkPaths             string
    OutputPath            string
    ManifestAttr          string
    ApplicationAttr       string
    SupportsScreensAttr   string
    UsesSdkAttr           string
    UsesConfigurationAttr string
    InApplication         map[string]*TokenInfo
    OutApplication        map[string]*TokenInfo
}

// TokenInfo 标签信息
type TokenInfo struct {
    Tag  string // 标签
    Name string // 名称
    Attr string // 属性
    Data string // 内容
}

func main() {
    mainPath := flag.String("m", "", "This is the main XML file Path.")
    linkPaths := flag.String("l", "", "This is the link XML files Path. Multiple files must be separated by commas.")
    outputPath := flag.String("o", "", "This is the output XML file Path.")
    flag.Parse()

    fmt.Println("MergeManifestXML Params -m:", *mainPath)
    fmt.Println("MergeManifestXML Params -l:", *linkPaths)
    fmt.Println("MergeManifestXML Params -o:", *outputPath)
    if *mainPath == "" || *linkPaths == "" || *outputPath == "" {
        fmt.Println("Please enter the correct parameters!")
        return
    }

    service := TService{
        MainPath:       *mainPath,
        LinkPaths:      *linkPaths,
        OutputPath:     *outputPath,
        InApplication:  make(map[string]*TokenInfo),
        OutApplication: make(map[string]*TokenInfo),
    }

    service.readMainXMLInfo()
    service.readMergeXMLInfo(service.MainPath)
    files := strings.Split(service.LinkPaths, ",")
    for index := 0; index < len(files); index++ {
        service.readMergeXMLInfo(files[index])
    }
    service.makeManifestXML()
}

func (service *TService) readMainXMLInfo() {
    content, err := ioutil.ReadFile(service.MainPath)
    if err != nil {
        return
    }

    decoder := xml.NewDecoder(bytes.NewBuffer(content))
    for t, err := decoder.Token(); err == nil; t, err = decoder.Token() {
        switch token := t.(type) {
        case xml.StartElement: // 处理元素开始(标签)
            tokenNameLocal := token.Name.Local
            for _, attr := range token.Attr {
                attrSpace := attr.Name.Space
                attrName := attr.Name.Local
                attrValue := attr.Value

                attr := "android:" + attrName + "=" + "\"" + attrValue + "\" "
                if tokenNameLocal == "manifest" {
                    space := attrSpace + ":"
                    if attrSpace == "" || attrSpace == "http://schemas.android.com/apk/res/android" {
                        space = ""
                    }
                    service.ManifestAttr += space + attrName + "=" + "\"" + attrValue + "\" "
                } else if tokenNameLocal == "application" {
                    service.ApplicationAttr += attr
                } else if tokenNameLocal == "supports-screens" {
                    service.SupportsScreensAttr += attr
                } else if tokenNameLocal == "uses-sdk" {
                    service.UsesSdkAttr += attr
                } else if tokenNameLocal == "uses-configuration" {
                    service.UsesConfigurationAttr += attr
                }
            }
        }
    }
}

func (service *TService) readMergeXMLInfo(path string) {
    content, err := ioutil.ReadFile(path)
    if err != nil {
        return
    }

    tokenNum := 0
    isInApplication := false
    isStart, isEnd := false, false
    tempTokenLocalName := ""
    tempTokenInfo := &TokenInfo{}
    decoder := xml.NewDecoder(bytes.NewBuffer(content))
    for t, err := decoder.Token(); err == nil; t, err = decoder.Token() {
        switch token := t.(type) {
        case xml.StartElement: // 处理元素开始(标签)
            tokenNameLocal := token.Name.Local
            if tokenNameLocal == "manifest" || tokenNameLocal == "application" || tokenNameLocal == "supports-screens" || tokenNameLocal == "uses-sdk" || tokenNameLocal == "uses-configuration" {
                if tokenNameLocal == "application" {
                    isInApplication = true
                }
            } else {
                if tokenNum == 0 {
                    tempTokenInfo.Tag = tokenNameLocal
                    for _, attr := range token.Attr {
                        tempTokenInfo.Attr += "android:" + attr.Name.Local + "=" + "\"" + attr.Value + "\" "
                        if attr.Name.Local == "name" {
                            tempTokenInfo.Name = attr.Value
                        }
                    }
                } else {
                    tempTokenLocalName = tokenNameLocal
                    tempTokenInfo.Data += "<" + tokenNameLocal + " "
                    for _, attr := range token.Attr {
                        tempTokenInfo.Data += "android:" + attr.Name.Local + "=" + "\"" + attr.Value + "\" "
                    }
                }
                isStart = true
                tokenNum++
            }
        case xml.EndElement: // 处理元素结束(标签)
            tokenNameLocal := token.Name.Local
            tempTokenLocalName = tokenNameLocal
            if tokenNameLocal == "manifest" || tokenNameLocal == "application" || tokenNameLocal == "supports-screens" || tokenNameLocal == "uses-sdk" || tokenNameLocal == "uses-configuration" {
                if tokenNameLocal == "application" {
                    isInApplication = false
                }
            } else {
                isEnd = true
                tokenNum--
            }
        case xml.CharData: // 处理字符数据(这里就是元素的文本)
            if tempTokenInfo.Name != "" {
                if tokenNum == 0 {
                    tokenInfo := &TokenInfo{}
                    tokenInfo.Tag = tempTokenInfo.Tag
                    tokenInfo.Name = tempTokenInfo.Name
                    tokenInfo.Attr = tempTokenInfo.Attr
                    tokenInfo.Data = tempTokenInfo.Data
                    key := tokenInfo.Tag + "_" + tokenInfo.Name
                    if isInApplication == true {
                        if service.InApplication[key] == nil {
                            service.InApplication[key] = tokenInfo
                        }
                    } else {
                        if service.OutApplication[key] == nil {
                            service.OutApplication[key] = tokenInfo
                        }
                    }
                    tempTokenInfo.Tag = ""
                    tempTokenInfo.Name = ""
                    tempTokenInfo.Attr = ""
                    tempTokenInfo.Data = ""
                } else {
                    if tempTokenInfo.Data != "" {
                        if isStart && isEnd {
                            tempTokenInfo.Data += "/>\n"
                        } else if isStart && !isEnd {
                            tempTokenInfo.Data += ">\n"
                        } else if !isStart && isEnd {
                            tempTokenInfo.Data += "</" + tempTokenLocalName + ">\n"
                        }
                    }
                }
            }
            isStart, isEnd = false, false
        }
    }
}

func (service *TService) makeManifestXML() {
    content := ""
    // 增加 manifest 标签
    content += "<manifest " + service.ManifestAttr + ">\n"

    // 增加 uses-sdk 标签
    if service.UsesSdkAttr != "" {
        content += "<uses-sdk " + service.UsesSdkAttr + "/>\n"
    }

    // 增加 supports-screens 标签
    if service.SupportsScreensAttr != "" {
        content += "<supports-screens " + service.SupportsScreensAttr + "/>\n"
    }

    // 增加 uses-configuration 标签
    if service.UsesConfigurationAttr != "" {
        content += "<uses-configuration " + service.UsesConfigurationAttr + "/>\n"
    }

    // 增加 application外层 标签
    for _, token := range service.OutApplication {
        content += "<" + token.Tag + " " + token.Attr
        if token.Data == "" {
            content += "/>\n"
        } else {
            content += ">\n"
            content += token.Data
            content += "</" + token.Tag + ">\n"
        }
    }

    // 增加 application 标签
    content += "<application " + service.ApplicationAttr + " >\n"

    // 增加 application内层 标签
    for _, token := range service.InApplication {
        content += "<" + token.Tag + " " + token.Attr
        if token.Data == "" {
            content += "/>\n"
        } else {
            content += ">\n"
            content += token.Data
            content += "</" + token.Tag + ">\n"
        }
    }

    content += "</application>\n"
    content += "</manifest>\n"
    service.saveNewXML(content)
}

func (service *TService) saveNewXML(content string) {
    fileName := service.OutputPath
    os.Remove(fileName)
    f, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return
    }
    defer f.Close()
    f.Write([]byte(xml.Header))
    f.Write([]byte(content))
    fmt.Println("Merge ManifestXML Success!")
}

2. strings.xml和styles.xml合并代码

参数:
-m : 主要XML文件的路径,相同属性以主XML为准。
-l : 合并XML文件的路径。
-o : 导出新XML文件的路径。

package main

import (
    "bytes"
    "encoding/xml"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "strings"
)

// TXMLData XML属性
type TXMLData struct {
    Data  string
    Names map[string]bool
}

// TService 服务
type TService struct {
    MainPath   string    // 主XML文件路径
    LinkPaths  string    // 依赖XML文件路径
    OutputPath string    // 导出XML文件路径
    XMLData    *TXMLData // XML Style属性
}

func main() {
    mainPath := flag.String("m", "", "This is the main XML files Path.")
    linkPaths := flag.String("l", "", "This is the link XML files Path. Multiple files must be separated by commas.")
    outputPath := flag.String("o", "", "This is the output XML file Path.")
    flag.Parse()

    fmt.Println("MergeResXML Params -m:", *mainPath)
    fmt.Println("MergeResXML Params -l:", *linkPaths)
    fmt.Println("MergeResXML Params -o:", *outputPath)
    if *linkPaths == "" || *outputPath == "" {
        fmt.Println("Please enter the correct parameters!")
        return
    }

    service := &TService{
        MainPath:   *mainPath,
        LinkPaths:  *linkPaths,
        OutputPath: *outputPath,
        XMLData:    &TXMLData{Data: "", Names: make(map[string]bool)},
    }
    service.loadXMLFile()
}

// 读取XML文件
func (service *TService) loadXMLFile() {
    service.saveXMLFileData(service.MainPath)
    files := strings.Split(service.LinkPaths, ",")
    for index := 0; index < len(files); index++ {
        service.saveXMLFileData(files[index])
    }
    service.makeXML()
}

// 保存XML文件数据
func (service *TService) saveXMLFileData(path string) {
    content, err := ioutil.ReadFile(path)
    if err != nil {
        return
    }

    isSaveData := false
    isHasContent := false
    decoder := xml.NewDecoder(bytes.NewBuffer(content))
    for t, err := decoder.Token(); err == nil; t, err = decoder.Token() {
        switch token := t.(type) {
        case xml.StartElement:
            tokenNameLocal := token.Name.Local
            if tokenNameLocal == "style" || tokenNameLocal == "string" {
                for _, attr := range token.Attr {
                    value := attr.Value
                    if attr.Name.Local == "name" && !service.XMLData.Names[value] {
                        service.XMLData.Names[value] = true
                        isSaveData = true
                    }
                }
            }
            if isSaveData {
                service.XMLData.Data += "\n<" + tokenNameLocal
                for _, attr := range token.Attr {
                    service.XMLData.Data += " " + attr.Name.Local + "=\"" + attr.Value + "\""
                }
                service.XMLData.Data += ">"
            }
        case xml.EndElement:
            tokenNameLocal := token.Name.Local
            if isSaveData {
                if tokenNameLocal == "style" || tokenNameLocal == "string" {
                    if isHasContent && tokenNameLocal != "string" {
                        service.XMLData.Data += "\n"
                        isHasContent = false
                    }
                    isSaveData = false
                }
                service.XMLData.Data += "</" + tokenNameLocal + ">"
            }
        case xml.CharData:
            if isSaveData {
                content := string([]byte(token))
                if strings.TrimSpace(content) != "" {
                    service.XMLData.Data += content
                    isHasContent = true
                }
            }
        }
    }
}

func (service *TService) makeXML() {
    content := "<resources>"
    content += service.XMLData.Data
    content += "\n</resources>"
    service.saveNewXML(content)
}

func (service *TService) saveNewXML(content string) {
    fileName := service.OutputPath
    os.Remove(fileName)
    f, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return
    }
    defer f.Close()
    f.Write([]byte(xml.Header))
    f.Write([]byte(content))
    fmt.Println("Merge ManifestXML Success!")
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容