動態修改android中的資源索引resId

Android進階之路系列:http://blog.csdn.net/column/details/16488.html


一、引言


1、爲什麼要動態修改資源索引

一般情況下我們不需要干預資源索引,因爲gradle會自動整合項目及所有依賴的資源,再進行相關編譯工作,這樣資源索引不會衝突。
但是如果我們在app中從另外一個apk包中獲取代碼或資源來使用,就有可能產生衝突。這時候就需要進行動態修改。

2、怎麼修改資源索引

目前網上最流行的方式是修改aapt源碼,重新編譯aapt並替換原有的文件。
這樣做好處是從根源解決問題,代碼改動很小,風險很小。
但是這樣做缺點是需要每個開發人員都替換文件,或者有一臺pc專門用於打這種包。
所以我們換一個角度來思考這個問題,是否我們可以在資源編譯完成後,對生成的R.java和二進制文件進行修改?
這樣做的好處是我們可以通過groovy做一個腳本或插件出來,在項目裏直接使用即可。

3、什麼時候修改

我們需要在資源編譯完成,生成了R.java等文件後,再去修改纔可以。那麼最好的時機是什麼時候呢?

gradle編譯過程中有類似如下幾個task

:app:generateXXXXResValues UP-TO-DATE
:app:generateXXXXResources
:app:mergeXXXXResources UP-TO-DATE
:app:processXXXXManifest UP-TO-DATE
:app:processXXXXResources
:app:generateXXXXSources

進過測試和對編譯過程的研究,發現資源索引resId是在processXXXXResources這個過程中產生的。
所以我們要在這個task之後執行修改,比如將0x7f都修改成0x7d(注意如果改成大於0x7f會有問題)
太早則相關文件還未生成出來,太晚則可能影響到後面class文件的編譯。所以最好是在processXXXXResources這個task之後立刻執行。

實際上processXXXXResources這個過程是執行了一個aapt命令,aapt即 Android Asset Packaging Tool,該工具在SDK/tools目錄下,用於打包資源文件。生成R.java、resources.arsc和res文件(二進制 & 非二進制如res/raw和pic保持原樣)。

有關processXXXXResources的詳解請閱讀《gradle編譯打包過程 之 ProcessAndroidResources的源碼分析


二、處理Task及R文件


1、處理Task

首先,我們需要找到對應的task,然後通過doLast函數讓我們的代碼在這個task之後執行。
考慮到buildType和productFlavors(環境和渠道等)的問題,一次gradle過程中這種task可能有多個,所以我們代碼如下:
project.afterEvaluate {
    def processResSet = project.tasks.findAll{
        boolean isProcessResourcesTask = false
        android.applicationVariants.all { variant ->
            if(it.name == 'process' + variant.getName() + 'Resources'){
                isProcessResourcesTask = true
            }
        }
        return isProcessResourcesTask
    }
    for(def processRes in processResSet){
        processRes.doLast{
            int newPkgId = 0x6D

            //gradle 3.0.0
            File[] fileList = getResPackageOutputFolder().listFiles()
            for(def i = 0; i < fileList.length; i++){
                if(fileList[i].isFile() && fileList[i].path.endsWith(".ap_")){
                    dealApFile(fileList[i], newPkgId, android.defaultConfig.applicationId)
                }
            }
            String newPkgIdStr = "0x" + Integer.toHexString(newPkgId)
            replaceResIdInJavaDir(getSourceOutputDir(), newPkgIdStr)
            replaceResIdInRText(getTextSymbolOutputFile(), newPkgIdStr)

//            //gradle 2.2.3
//            dealApFile(packageOutputFile, newPkgId, android.defaultConfig.applicationId)
//            replaceResIdInJava(textSymbolOutputDir, sourceOutputDir, android.defaultConfig.applicationId, newPkgId)
//            String newPkgIdStr = "0x" + Integer.toHexString(newPkgId)
//            replaceResIdInJavaDir(sourceOutputDir, newPkgIdStr)
//            replaceResIdInRText(textSymbolOutputDir + File.separator + "R.txt", newPkgIdStr)
        }
    }
}
先根據variant找到processXXXXResources這類task,然後遍歷執行doLast,這樣doLast中的語句塊就會在資源編譯完成後立刻執行。至於語句塊中的代碼我們後面一點點分析。

2、修改R文件

觀察臨時生成的文件發現與R文件有關的文件有兩種,分別是
build/intermediates/symbols/[productFlavors]/[buildType]/R.txt  (這個貌似與kotlin有關)
build/generated/source/r/[productFlavors]/[buildType]/[packageName]/R.java

這兩種文件都是直接可讀的,所以直接替換即可。關鍵點在於如何得到這兩個文件的路徑。
我們在上一步中的processRes是一個Task對象,它實際上是Task的一個子類:ProcessAndroidResources_Decorated。
在gradle源碼中沒有找到這個類,但是找到了ProcessAndroidResources類,根據類名可以猜測ProcessAndroidResources_Decorated實際上是對ProcessAndroidResources進行了包裝,而且很有可能是編譯時生成的類。

在ProcessAndroidResources類中我們可以找到與文件相關的變量
經過簡單測試既可以找到我們需要的,其中:
textSymbolOutputDir是build/intermediates/symbols/[productFlavors]/[buildType]/

sourceOutputDir是build/generated/source/r/[productFlavors]/[buildType]/

(注意,上面是基於gradle2.3.3版本,gradle3.0.0版本ProcessAndroidResources代碼變動很大,需要使用一個函數來獲取,而且獲取的路徑也有所不同,所以doLast代碼塊中處理有不同)


OK,我們寫兩個函數來處理R文件,代碼如下:
def replaceResIdInRText(File textSymbolOutputFile, String newPkgIdStr){
    println textSymbolOutputFile.path
    def list1 = []
    textSymbolOutputFile.withReader('UTF-8') { reader ->
        reader.eachLine {
            if (it.contains('0x7f')) {
                it = it.replace('0x7f', newPkgIdStr)
            }
            list1.add(it + "\n")
        }
    }
    textSymbolOutputFile.withWriter('UTF-8') { writer ->
        list1.each {
            writer.write(it)
        }
    }
}

def replaceResIdInJavaDir(File srcFile, String newPkgIdStr){
    if(srcFile.isFile()){
        if(srcFile.name.equals("R.java")){
            def list = []
            file(srcFile).withReader('UTF-8') { reader ->
                reader.eachLine {
                    if (it.contains('0x7f')) {
                        it = it.replace('0x7f', newPkgIdStr)
                    }
                    list.add(it + "\n")
                }
            }
            file(srcFile).withWriter('UTF-8') { writer ->
                list.each {
                    writer.write(it)
                }
            }
        }
    }
    else{
        def fileList = srcFile.listFiles()
        for(def i = 0; i < fileList.length; i++){
            replaceResIdInJavaDir(fileList[i], newPkgIdStr)
        }
    }
}

代碼比較簡單,就是將文件裏的0x7f都替換成新的pkgId。然後在doLast中執行這兩個函數,見前面代碼(注意不同gradle版本代碼有點不同)。

不過這裏注意,R.java文件在不同的包名下都會存在一個,我們需要都進行更改,否則會出錯。所以代碼中我們遍歷整個路徑下所有文件處理。


這樣我們把R文件修改成功了,這時候如果編譯運行app會報錯
Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x8f04001b

因爲build的過程中有關resource的過程如下:
1、除了assets和res/raw資源被原裝不動地打包進APK之外,其它的資源都會被編譯或者處理.xml文件會被編譯爲二進制的xml。
2、除了assets資源之外,其它的資源都會被賦予一個資源ID。
3、打包工具負責編譯和打包資源,編譯完成之後,會生成一個resources.arsc文件和一個R.java,前者保存的是一個資源索引表,後者定義了各個資源ID常量,供在代碼中索引資源。

當應用程序在運行時,則通過AssetManager來訪問資源,或通過資源ID來訪問,或通過文件名來訪問。通過ID訪問時會用ID去resources.arsc中查找對應的資源。
也就是說實際上索引是通過resources.arsc來進行的,而R.java文件的作用只是將資源ID通過常量的方式在代碼中使用。

問題出現在這裏,我們上面只修改了R.java,對於resources.arsc文件沒有動,這樣resources.arsc中還是舊的id,所以出現上面的錯誤。



三、處理編譯後的二進制文件


1、編譯後的文件在哪?

上面我們說到需要修改resources.arsc文件,那麼這個文件在哪?
它其實是與R.java一起由aapt命令生成的,但是我們在build目錄下未找到任何這個文件的影子。
但是我在[project]/app/build/intermediates/res/目錄下找到了一個resources-debug.ap_文件,經測試這個文件是與R.java一樣都是在processDebugResources這個task中生成的。

那麼這個resources-debug.ap_就是resources.arsc文件麼?
經過與打包後apk中的resources.arsc文件對比發現,這兩個文件肯定不是一個文件。resources-debug.ap_要大很多。

機緣巧合下我發現了一點端倪。
因爲我一直僅僅進行編譯,而未執行打包。
當我們使用rebuild等命令打包apk後,在[project]/app/build/intermediates/incremental/packageDebug/目錄下會生成一個file-input-save-data.txt
其中有如下部分信息:
341.file=/Users/bennu/TestApp/app/build/intermediates/res/resources-debug.ap_/resources.arsc
54.base=/Users/bennu/TestApp/app/build/intermediates/res/resources-debug.ap_
76.set=ANDROID_RESOURCE
327.set=ANDROID_RESOURCE
357.base=/Users/bennu/TestApp/app/build/intermediates/res/resources-debug.ap_
374.file=/Users/bennu/TestApp/app/build/intermediates/res/resources-debug.ap_/res/drawable-xhdpi-v4/abc_ic_star_half_black_16dp.png

這樣一看,這個resources-debug.ap_文件實際上包含了resources.arsc文件,那麼它到底是個什麼?
首先它肯定不是目錄,在終端中無法直接進入。
它既然包含其他文件,那麼它可能是一個壓縮文件,查看它的二進制內容發現是以“504B0304”開頭的,那麼就可以確定它是一個zip文件了。

改擴展名並解壓縮後,我們就得到了一個目錄,進入後發現:
這個包裏不僅僅有resources.arsc,還包括AndroidManifest.xml和res目錄(除asset外所有資源)。

2、解壓、壓縮AP_文件


上一步中我們發現ap_文件實際上是一個壓縮包,裏面包含resources.arsc、AndroidManifest.xml和其他資源文件。這些文件實際上就是經過aapt編譯後的資源二進制文件。
我們想修改這些文件,那麼就需要解壓ap_文件,同時修改後再壓縮回去。因爲這個ap_文件在後面打包的流程中會用到。
同樣,我們編寫壓縮和解壓縮的函數待用,代碼如下:
def unZip(File src, String savepath)throws IOException
{
    def count = -1;
    def index = -1;
    def flag = false;
    def file1 = null;
    def is = null;
    def fos = null;
    def bos = null;

    ZipFile zipFile = new ZipFile(src);
    Enumeration<?> entries = zipFile.entries();

    while(entries.hasMoreElements())
    {
        def buf = new byte[2048];
        ZipEntry entry = (ZipEntry)entries.nextElement();
        def filename = entry.getName();

        filename = savepath + filename;
        File file2=file(filename.substring(0, filename.lastIndexOf('/')));

        if(!file2.exists()){
            file2.mkdirs()
        }

        if(!filename.endsWith("/")){

            file1 = file(filename);
            file1.createNewFile();
            is = zipFile.getInputStream(entry);
            fos = new FileOutputStream(file1);
            bos = new BufferedOutputStream(fos, 2048);

            while((count = is.read(buf)) > -1)
            {
                bos.write(buf, 0, count );
            }

            bos.flush();

            fos.close();
            is.close();

        }
    }

    zipFile.close();

}

def zipFolder(String srcPath, String savePath)throws IOException
{
    def saveFile = file(savePath)
    saveFile.delete()
    saveFile.createNewFile()
    def outStream = new ZipOutputStream(new FileOutputStream(saveFile))
    def srcFile = file(srcPath)
    zipFile(srcFile.getAbsolutePath() + File.separator, "", outStream)
    outStream.finish()
    outStream.close()
}

def zipFile(String folderPath, String fileString, ZipOutputStream out)throws IOException
{
    File srcFile = file(folderPath + fileString)
    if(srcFile.isFile()){
        def zipEntry = new ZipEntry(fileString)
        def inputStream = new FileInputStream(srcFile)
        out.putNextEntry(zipEntry)
        def len
        def buf = new byte[2048]
        while((len = inputStream.read(buf)) != -1){
            out.write(buf, 0, len)
        }
        out.closeEntry()
    }
    else{
        def fileList = srcFile.list()
        if(fileList.length <= 0){
            def zipEntry = new ZipEntry(fileString + File.separator)
            out.putNextEntry(zipEntry)
            out.closeEntry()
        }

        for(def i = 0; i < fileList.length; i++){
            zipFile(folderPath, fileString.equals("") ?  fileList[i] : fileString + File.separator + fileList[i], out)
        }
    }
}

這部分不是重點,不細說了,注意壓縮的時候不能帶着根目錄。


接下來還有一個問題,就是如何得到這個ap_文件路徑?
前面說過ProcessAndroidResources有幾個變量,其中packageOutputFile就是這個ap_文件的路徑。
(基於gradle2.3.3版本,在gradle3.0.0版本則需要使用getResPackageOutputFolder()來獲取,而且獲取的只是目錄,所以代碼上會有些許不同)
這樣我們再寫一個函數來處理這個文件,如下:
def dealApFile(File packageOutputFile, int newPkgId, String pkgName){
    int prefixIndex = packageOutputFile.path.lastIndexOf(".")
    String unzipPath = packageOutputFile.path.substring(0, prefixIndex) + File.separator
    unZip(packageOutputFile, unzipPath)

    //TODO 這裏處理二進制文件,下面會講
    replaceResIdInResDir(unzipPath, newPkgId)
    replaceResIdInArsc(file(unzipPath + 'resources.arsc'), newPkgId, pkgName)

    zipFolder(unzipPath, packageOutputFile.path)
    //file(unzipPath).deleteDir() //如果需要可以在處理後刪除解壓後的文件
}

解壓後的目錄保持與ap_文件同名,防止出現混亂。

最後在doLast中執行這個函數就可以了,注意不同gradle版本的不同處理。

3、修改resources.arsc文件的pkgId

這樣我們就有了resources.arsc文件,下一步就是修改裏面的resId。
由於resources.arsc文件是二進制的,所以需要參考一些解析的文章(比如《resource.arsc二進制內容解析 之 RES_TABLE_TYPE_TYPE》)。這裏我們只聊有關資源索引的。
經過研究發現,每一個資源ID其實由三部分組成:
packId + resTypeId + 遞增id
最高兩個字節是packId,系統資源id是:0x01,普通應用資源id是:0x7F
中間的兩個字節表示resTypeId,類型id即資源的類型(string、color等),這個值從0開始。(注意每個類型的id不是固定的)
最低四個字節表示這個資源的順序id,從1開始,逐漸累加1

而且資源ID的三個部分在resources.arsc文件中是分別存儲的,因爲我們只想修改lib包中最高兩個字節,防止出現資源重複的現象,所以只需要修改package id。

那麼package id在哪?我們來看resources.arsc文件部分結構:

可以看到在Package Header這個結構裏就有一個package id,經過分析這個正是我們需要修改的部分。
下面的問題就是如果找到它的位置?
注意到Package Header是以RES_TABLE_PACKAGE_TYPE開頭的,它是一個常量0x200。並且它後面緊跟着的頭大小和塊大小佔用的位數是固定的。
一個resources.arsc文件的這部分內容如下:
因爲有字序問題,所以RES_TABLE_PACKAGE_TYPE是0002,2001是頭大小,98FB0200是塊大小,而package id是7F000000。
所以我們需要在文件中找到0002xxxx xxxxxxxx 7F000000這樣的數據就可以了

我們的思路是每次讀取4byte(因爲每個結構塊都是4byte的整倍數),當發現前兩個byte是0002,則讀取它往後的9b到11b,如果是7F000000,說明我們就得到了package id的位置。將第9b改爲新pkgId即可。(另外package id後面一定跟着包名,也可以判斷包名提高準確率,不過應該沒必要)
我們再寫一個函數來處理,代碼如下:
def replaceResIdInArsc(File resFile, int newPkgId, String pkgName) throws Exception
{
    def buf = resFile.bytes

    for(def i = 0; i + 15 < buf.length; ){
        if(buf[i] == 0x00 && buf[i+1] == 0x02 && buf[i+8] == 0x7F && buf[i+9] == 0x00 && buf[i+10] == 0x00 && buf[i+11] == 0x00){
            buf[i+8] = newPkgId
            break
        }
        i=i+4
    }

    def outStream = new FileOutputStream(resFile)
    outStream.write(buf, 0, buf.length)
    outStream.flush()
    outStream.close()
}

代碼很簡單,就不細說了。

(注意這裏沒有處理完整,所以這個函數後續會補充)
然後在之前的dealApFile函數中執行即可。

我們再次編譯運行App,在java代碼中使用資源id就能正常找到了。但是還有一個問題,運行時發現在xml文件中使用id還是7F開頭的,所以解析xml會失敗。
這是因爲在processDebugResources過程中,我們使用aapt打包資源文件時,將xml文件都轉爲了二進制。而這些二進制文件中則不再是資源名稱了,而是資源id,也就是說xml文件中不通過資源名去查找資源,直接通過ID查找。而這些xml文件中的資源ID還是7F開頭的,所以我們還需要將所有的二進制xml文件中的資源ID都替換一遍。


4、修改Xml文件

因爲xml文件(包括AndroidManifest)都是二進制,所以我們需要閱讀《Android逆向:二進制xml文件解析 之 Start Tag Chunk》。
這裏我們只關注資源索引的部分。所以我們關注TypeValue這部分結構。
因爲我們需要改的是resId,所以類型應該是TYPE_REFERENCE,即0x01。但是後來發現我們還需要處理TYPE_ATTRIBUTE,即0x02。(xml中使用 ?attr/xxxx 這種情況)
(注意這裏的TYPE_STRING等類型指的是直接使用的字符串,而非@string/xxx這樣的)

這樣我們要找的Res_value就是類似下面的
08000001 XXXX7F 或 08000002 XXXX7F
(注意resId有字節序的問題)
然後修改即可。

因爲我們要修改所有xml文件,包括AndroidManifest.xml,所以通過遞歸來處理,代碼如下:
def replaceResIdInResDir(String resPath, int newPkgId) throws Exception
{
    File resFile = file(resPath)
    if(resFile.isFile()){
        if(resPath.endsWith(".xml")){
            replaceResIdInXml(resFile, newPkgId)
        }
    }
    else{
        def fileList = resFile.list()
        if(fileList == null || fileList.length <= 0){
            return
        }
        for(def i = 0; i < fileList.length; i++){
            replaceResIdInResDir(resPath + File.separator + fileList[i], newPkgId)
        }
    }
}

def replaceResIdInXml(File resFile, int newPkgId) throws Exception
{
    def buf = resFile.bytes

    for(def i = 0; i + 7 < buf.length; i=i+4){
        if(buf[i] == 0x08 && buf[i+1] == 0x00 && buf[i+2] == 0x00 && (buf[i+3] == 0x01 || buf[i+3] == 0x02)){
            if(buf[i+7] == 0x7f){
                buf[i+7] = newPkgId
                //println resFile.name + "," + (i+7)
            }
        }
    }

    def outStream = new FileOutputStream(resFile)
    outStream.write(buf, 0, buf.length)
    outStream.flush()
    outStream.close()
}

然後在之前的dealApFile函數中執行即可。

這樣修改後,我們的App終於正常運行起來了,但是還是有一點小問題,樣式不對了,即在AndroidManifest.xml爲Application設置的theme失效了。

觀察日誌發現這樣一條信息
W/ResourceType: Invalid package identifier when getting bag for resource number 0x7f090062
我們設置的Theme是Theme.AppCompat.Light,而這個0x7f090062則是Base.Theme.AppCompat.Light的資源索引。
檢查了一下修改後的resources.arsc,裏面確實還存在一些完整的資源索引。



5、修改ConfigList

接着上面的問題,爲什麼會有完整的資源索引?如何處理它們?
這涉及到resources.arsc結構中最核心的部分——ConfigList。這部分比較複雜,所以請先仔細閱讀resource.arsc二進制內容解析 之 RES_TABLE_TYPE_TYPE
通過文章我們知道,當一個資源的value是另外一個資源索引,那麼這個索引就必須完整存在ConfigList中;同時,bag類型的數據結構中還有parent也可能會是完整的資源索引。這些都是我們需要處理的。

這樣我們需要補充之前的replaceResIdInArsc函數,增加對configList的處理,代碼如下:
def replaceResIdInArsc(File resFile, int newPkgId, String pkgName) throws Exception
{
    def buf = resFile.bytes

    for(def i = 0; i + 15 < buf.length; ){
        if(buf[i] == 0x00 && buf[i+1] == 0x02 && buf[i+8] == 0x7F && buf[i+9] == 0x00 && buf[i+10] == 0x00 && buf[i+11] == 0x00){
            buf[i+8] = newPkgId
            i += headSize
            continue
        }
        if(buf[i] == 0x01 && buf[i+1] == 0x02 && buf[i+9] == 0x00 && buf[i+10] == 0x00 && buf[i+11] == 0x00){
            int offsetStart = i + ((buf[i+3]&0xFF) << 8) + (buf[i+2]&0xFF)
            int offsetSize = ((buf[i+15]&0xFF) << 24) + ((buf[i+14]&0xFF) << 16) + ((buf[i+13]&0xFF) << 8) + (buf[i+12]&0xFF)
            int dataStart = offsetStart + offsetSize * 4
            int dataEnd = i + ((buf[i+7]&0xFF) << 24) + ((buf[i+6]&0xFF) << 16) + ((buf[i+5]&0xFF) << 8) + (buf[i+4]&0xFF) - 1
            //println "chuck start " + i + " offsetStart " + offsetStart + " offsetSize " + offsetSize + " dataStart " + dataStart + " dataEnd " + dataEnd
            if(offsetStart < dataStart && dataStart < dataEnd && dataEnd < buf.length){
                //println "chuck start " + i
                replaceResIdInArscConfigList(buf, offsetStart, offsetSize, dataStart, dataEnd, newPkgId)
                i = dataEnd + 1
                continue
            }
        }
        i=i+4
    }

    def outStream = new FileOutputStream(resFile)
    outStream.write(buf, 0, buf.length)
    outStream.flush()
    outStream.close()
}

(注意,這個函數依然需要補充,後面會講)

首先找到ConfigList的header,以RES_TABLE_TYPE_TYPE開頭,考慮字序即0102,然後2byte是頭大小,再4byte是塊大小,然後就是resType,resType後三個byte是固定的0,所以我們找這樣的數據:
0102xxxx xxxxxxxx xx000000
找到header後,我們可以根據結構解析出一些數據:
offsetStart:解析出header大小,再加上header的index就得到偏移數組的實際位置(因爲偏移數組是緊跟着header的)
offsetSize:解析出偏移數組的數量,即entry的總數
dataStart:entry數組的起始位置,offsetSize*4加上offsetStart即可(每個偏移固定佔4byte,偏移數組後緊接着就是數組)
dataEnd:解析出塊大小,再加上header的index就得到entry數組的末尾位置,也是這個ConfigList的末尾。

然後調用replaceResIdInArscConfigList來處理,這個函數代碼如下:
def replaceResIdInArscConfigList(byte[] buf, int offsetStart, int offsetSize, int dataStart, int dataEnd, int newPkgId) throws Exception
{
    //println "offsetStart " + offsetStart + " offsetSize " + offsetSize + " dataStart " + dataStart + " dataEnd " + dataEnd
    if(offsetSize == 1){
        replaceResIdInArscEntry(buf, dataStart, dataEnd, newPkgId)
    }
    else{
        int lastoffset = dataStart
        for(def i = offsetStart + 4; i + 3 < dataStart; i=i+4){
            if(buf[i] == -1 && buf[i+1] == -1 && buf[i+2] == -1 && buf[i+3] == -1){
                continue
            }
            int offset = dataStart + ((buf[i+3]&0xFF) << 24) + ((buf[i+2]&0xFF) << 16) + ((buf[i+1]&0xFF) << 8) + (buf[i]&0xFF)
            replaceResIdInArscEntry(buf, lastoffset, offset, newPkgId)
            lastoffset = offset
        }
        replaceResIdInArscEntry(buf, lastoffset, dataEnd, newPkgId)
    }
}
如果offsetSize爲1,說明只有一個entry,dataStart和dataEnd就是entry的開始和結束,執行replaceResIdInArscEntry函數。
大於1的時候,我們取下一個entry的偏移量來計算當前entry的結尾,並單獨處理最後一個entry。

下面就是重點函數replaceResIdInArscEntry,代碼如下:
def replaceResIdInArscEntry(byte[] buf, int entryStart, int entryEnd, int newPkgId){
    //println "entryStart " + entryStart + " entryEnd " + entryEnd
    if(buf[entryStart] == 0x08 && buf[entryStart+1] == 0x00 && buf[entryStart+2] == 0x00 && buf[entryStart+3] == 0x00){
        if(entryStart+15 > entryEnd){
            return
        }
        if(buf[entryStart+8] == 0x08 && buf[entryStart+9] == 0x00 && buf[entryStart+10] == 0x00 && buf[entryStart+11] == 0x01 && buf[entryStart+15] == 0x7F){
            buf[entryStart+15] = newPkgId
            //println entryStart+15
        }
    }
    if(buf[entryStart] == 0x10 && buf[entryStart+1] == 0x00 && buf[entryStart+2] == 0x01 && buf[entryStart+3] == 0x00){
        if(entryStart+15 > entryEnd){
            return
        }
        if(buf[entryStart+11] == 0x7F){
            buf[entryStart+11] = newPkgId
            //println entryStart+11
        }
        int size = ((buf[entryStart+15]&0xFF) << 24) + ((buf[entryStart+14]&0xFF) << 16) + ((buf[entryStart+13]&0xFF) << 8) + (buf[entryStart+12]&0xFF)
        for(def i = 0; i < size; i++){
            if(buf[entryStart+19+i*12] == 0x7F){
                buf[entryStart+19+i*12] = newPkgId
                //println entryStart+19+i*12
            }
            if(buf[entryStart+20+i*12] == 0x08 && buf[entryStart+21+i*12] == 0x00 && buf[entryStart+22+i*12] == 0x00 && (buf[entryStart+23+i*12] == 0x01 || buf[entryStart+23+i*12] == 0x02) && buf[entryStart+27+i*12] == 0x7F){
                buf[entryStart+27+i*12] = newPkgId
                //println entryStart+27+i*12
            }
        }
    }
}
如果以08000000開始則是非bag,以10000000開始則是bag,分別處理。
非bag的處理與之前xml的處理類似。
bag則需要先處理parent,然後再遍歷處理ResTable_map。ResTable_map中先處理資源項id;在處理Res_value,這個與非bag一樣。

經過處理後再檢查resources.arsc,已經沒有資源索引了,說明這次我們改的很徹底。
編譯運行,樣式還不行!
日誌顯示:
W/ResourceType: Failed resolving bag parent id 0x7d090062
W/ResourceType: Attempt to retrieve bag 0x7d090114 which is invalid or in a cycle.


6、添加資源包id映射

日誌與上次的有了不同,說明是另外一個問題了。
經過了兩天的折磨,總算有點頭緒了,是缺少資源包id映射的問題,關於這個問題請詳細閱讀《resource.arsc二進制內容解析 之 Dynamic package reference》。
通過文章我們瞭解,由於我們放棄了默認的0x7F,在5.0以上的系統尋找bag的parent就會有問題。
這樣就需要我們手動添加這個結構了,在resources.arsc修改數據還可以,但是添加數據就一定要注意,很容易影響所有數據。

在這裏我們暫時考慮只有一個package的情況,這樣通過文章知道,在末尾添加這部分數據只會影響package大小和文件大小。
首先,我們先創建出數據塊,代碼如下:
def getDynamicRef(String pkgName ,int newPkgId){
    int typeLength = 2
    int headSizeLength = 2
    int totalSizeLength = 4
    int countLength = 4
    int pkgIdLength = 4

    def pkgbyte = pkgName.bytes
    int pkgLength = pkgbyte.length * 2
    if(pkgLength % 4 != 0){
        pkgLength += 2
    }
    if(pkgLength < 256){
        pkgLength = 256
    }

    def pkgBuf = new byte[typeLength + headSizeLength + totalSizeLength + countLength + pkgIdLength + pkgLength]

    pkgBuf[0]=0x03
    pkgBuf[1]=0x02

    pkgBuf[typeLength]=0x0c
    pkgBuf[typeLength + 1]=0x00

    pkgBuf[typeLength + headSizeLength] = pkgBuf.length & 0x000000ff
    pkgBuf[typeLength + headSizeLength + 1] = (pkgBuf.length & 0x0000ff00) >> 8
    pkgBuf[typeLength + headSizeLength + 2] = (pkgBuf.length & 0x00ff0000) >> 16
    pkgBuf[typeLength + headSizeLength + 3] = (pkgBuf.length & 0xff000000) >> 24

    pkgBuf[typeLength + headSizeLength + totalSizeLength]=0x01

    pkgBuf[typeLength + headSizeLength + totalSizeLength + countLength] = newPkgId

    for(int i = 0; i < pkgbyte.length; i++){
        pkgBuf[typeLength + headSizeLength + totalSizeLength + countLength + pkgIdLength + i * 2] = pkgbyte[i]
    }

    return pkgBuf
}
根據dynamicRefTable結構,這裏我們只加入一組packageId和packageName即可。然後需要修改之前的replaceResIdInArsc函數,補充相關代碼,最終這個函數代碼如下:
def replaceResIdInArsc(File resFile, int newPkgId, String pkgName) throws Exception
{
    def buf = resFile.bytes
    def dynamicRefBytes = getDynamicRef(pkgName, newPkgId)
    int size = buf.length + dynamicRefBytes.length
    buf[4] = size & 0x000000ff
    buf[5] = (size & 0x0000ff00) >> 8
    buf[6] = (size & 0x00ff0000) >> 16
    buf[7] = (size & 0xff000000) >> 24

    for(def i = 0; i + 15 < buf.length; ){
        if(buf[i] == 0x00 && buf[i+1] == 0x02 && buf[i+8] == 0x7F && buf[i+9] == 0x00 && buf[i+10] == 0x00 && buf[i+11] == 0x00){
            //println "packagePosition:" + i
            int headSize = ((buf[i+3]&0xFF) << 8) + (buf[i+2]&0xFF)
            int pkgSize = ((buf[i+7]&0xFF) << 24) + ((buf[i+6]&0xFF) << 16) + ((buf[i+5]&0xFF) << 8) + (buf[i+4]&0xFF) + dynamicRefBytes.length
            buf[i+4] = pkgSize & 0x000000ff
            buf[i+5] = (pkgSize & 0x0000ff00) >> 8
            buf[i+6] = (pkgSize & 0x00ff0000) >> 16
            buf[i+7] = (pkgSize & 0xff000000) >> 24

            buf[i+8] = newPkgId
            i += headSize
            continue
        }
        if(buf[i] == 0x01 && buf[i+1] == 0x02 && buf[i+9] == 0x00 && buf[i+10] == 0x00 && buf[i+11] == 0x00){
            int offsetStart = i + ((buf[i+3]&0xFF) << 8) + (buf[i+2]&0xFF)
            int offsetSize = ((buf[i+15]&0xFF) << 24) + ((buf[i+14]&0xFF) << 16) + ((buf[i+13]&0xFF) << 8) + (buf[i+12]&0xFF)
            int dataStart = offsetStart + offsetSize * 4
            int dataEnd = i + ((buf[i+7]&0xFF) << 24) + ((buf[i+6]&0xFF) << 16) + ((buf[i+5]&0xFF) << 8) + (buf[i+4]&0xFF) - 1
            //println "chuck start " + i + " offsetStart " + offsetStart + " offsetSize " + offsetSize + " dataStart " + dataStart + " dataEnd " + dataEnd
            if(offsetStart < dataStart && dataStart < dataEnd && dataEnd < buf.length){
                //println "chuck start " + i
                replaceResIdInArscConfigList(buf, offsetStart, offsetSize, dataStart, dataEnd, newPkgId)
                i = dataEnd + 1
                continue
            }
        }
        i=i+4
    }

    def outStream = new FileOutputStream(resFile)
    outStream.write(buf, 0, buf.length)
    outStream.write(dynamicRefBytes)
    outStream.flush()
    outStream.close()
}
先創建出dynamicRefTable結構的數據,然後將文件大小增加並重新寫回;
再解析package header的時候,獲取package塊大小,同樣增加該大小並重新寫回;
最後在重新寫入文件時,先寫入原文件數據(修改過的),在寫入dynamicRefTable就可以了。

編譯運行,樣式終於正確顯示了!說明我們成功了!



四、總結

經過上面的處理,我們已經可能動態修改資源索引了。但是要注意沒有考慮一些較複雜的情況,例如多package的情況,如果考慮這些情況需要對代碼做一些補充。
在整個過程中,需要修改到R文件、resources.arsc和二進制的xml文件,需要對二進制文件結構有一定的瞭解,實際上就是要有反編譯這些文件,或者部分內容的能力。
我們還需要了解整個打包流程,每個階段都做了哪些事情,才能知道要在什麼時機來做這些事情。



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