动态修改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文件,需要对二进制文件结构有一定的了解,实际上就是要有反编译这些文件,或者部分内容的能力。
我们还需要了解整个打包流程,每个阶段都做了哪些事情,才能知道要在什么时机来做这些事情。



猜你喜欢

转载自blog.csdn.net/chzphoenix/article/details/80581838
今日推荐