动态修改android中的资源索引resId

article/2025/8/15 16:32:08

目录

一、引言

1、为什么要动态修改资源索引

2、怎么修改资源索引

3、什么时候修改

二、处理Task及R文件

1、处理Task

2、修改R文件

三、处理编译后的二进制文件

1、编译后的文件在哪?

2、解压、压缩AP_文件

3、修改resources.arsc文件的pkgId

4、修改Xml文件

5、修改ConfigList

6、添加资源包id映射

四、总结


一、引言

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 = falseandroid.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.0File[] 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.pathdef 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 lendef 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.separatorunZip(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.bytesfor(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] = newPkgIdbreak}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.bytesfor(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.bytesfor(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] = newPkgIdi += headSizecontinue}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 * 4int 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 " + dataEndif(offsetStart < dataStart && dataStart < dataEnd && dataEnd < buf.length){//println "chuck start " + ireplaceResIdInArscConfigList(buf, offsetStart, offsetSize, dataStart, dataEnd, newPkgId)i = dataEnd + 1continue}}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 " + dataEndif(offsetSize == 1){replaceResIdInArscEntry(buf, dataStart, dataEnd, newPkgId)}else{int lastoffset = dataStartfor(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 " + entryEndif(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 = 2int headSizeLength = 2int totalSizeLength = 4int countLength = 4int pkgIdLength = 4def pkgbyte = pkgName.bytesint pkgLength = pkgbyte.length * 2if(pkgLength % 4 != 0){pkgLength += 2}if(pkgLength < 256){pkgLength = 256}def pkgBuf = new byte[typeLength + headSizeLength + totalSizeLength + countLength + pkgIdLength + pkgLength]pkgBuf[0]=0x03pkgBuf[1]=0x02pkgBuf[typeLength]=0x0cpkgBuf[typeLength + 1]=0x00pkgBuf[typeLength + headSizeLength] = pkgBuf.length & 0x000000ffpkgBuf[typeLength + headSizeLength + 1] = (pkgBuf.length & 0x0000ff00) >> 8pkgBuf[typeLength + headSizeLength + 2] = (pkgBuf.length & 0x00ff0000) >> 16pkgBuf[typeLength + headSizeLength + 3] = (pkgBuf.length & 0xff000000) >> 24pkgBuf[typeLength + headSizeLength + totalSizeLength]=0x01pkgBuf[typeLength + headSizeLength + totalSizeLength + countLength] = newPkgIdfor(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.bytesdef dynamicRefBytes = getDynamicRef(pkgName, newPkgId)int size = buf.length + dynamicRefBytes.lengthbuf[4] = size & 0x000000ffbuf[5] = (size & 0x0000ff00) >> 8buf[6] = (size & 0x00ff0000) >> 16buf[7] = (size & 0xff000000) >> 24for(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:" + iint 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.lengthbuf[i+4] = pkgSize & 0x000000ffbuf[i+5] = (pkgSize & 0x0000ff00) >> 8buf[i+6] = (pkgSize & 0x00ff0000) >> 16buf[i+7] = (pkgSize & 0xff000000) >> 24buf[i+8] = newPkgIdi += headSizecontinue}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 * 4int 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 " + dataEndif(offsetStart < dataStart && dataStart < dataEnd && dataEnd < buf.length){//println "chuck start " + ireplaceResIdInArscConfigList(buf, offsetStart, offsetSize, dataStart, dataEnd, newPkgId)i = dataEnd + 1continue}}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文件,需要对二进制文件结构有一定的了解,实际上就是要有反编译这些文件,或者部分内容的能力。
我们还需要了解整个打包流程,每个阶段都做了哪些事情,才能知道要在什么时机来做这些事情。

关注公众号:BennuCTech,发送“CustomResID”获取源码。


http://chatgpt.dhexx.cn/article/x5FKSppP.shtml

相关文章

Redis安装(Windows环境)

文章目录 一、Resid简介&#xff1a;二、下载Redis三、启动Redis服务四、设置Windows服务五、常用的Redis服务命令六、cmd启动服务&#xff1a;七、操作测试Redis 一、Resid简介&#xff1a; Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分…

Resid总结

Redis是什么&#xff1f; Redis&#xff1a;REmote DIctionary Server(远程字典服务器&#xff09; 是完全开源免费的&#xff0c;用C语言编写的&#xff0c;遵守BSD协议&#xff0c;是一个高性能的(key/value)分布式内存数据库&#xff0c;基于内存运行并支持持久化的NoSQL数…

Redis系列(一):各大厂都在用的Resid到底是什么?

一、redis是什么&#xff1f; 1.Redis的定义 先上一段官方定义&#xff1a;Redis 是开源免费&#xff08;遵守BSD协议&#xff09;、高性能的key-value数据库。 是不是觉得上面的一句话既熟悉又陌生&#xff0c;作者怎么想的为啥开源免费&#xff1f;BSD协议又是什么&#x…

opencv 实现图像高斯金字塔

函数&#xff1a; dst cv.pyrDown( src[, dst[, dstsize[, borderType]]] ) # 高斯金字塔下采样 参数&#xff1a; src 源图像. dst 输出图像&#xff1b;它有着指定的大小&#xff0c;和源图像有着相同的类型. dstsize 输出图像的大小. borderType 像素外延方法. 默认情况下&…

图像增强中的高斯金字塔/拉普拉斯金字塔 融合

1.高斯金字塔 注意区分&#xff1a;高斯金字塔层级越高&#xff0c;分辨率越低。但下层&#xff08;第i层&#xff09;得到上层&#xff08;第i1层&#xff09;的过程叫做下采样&#xff0c;有些地方用reduce描述这个过程。 图像的金字塔化过程实际上是先平滑、再下采样的过程…

python --opencv图像处理金字塔(高斯金字塔、拉普拉斯金字塔)

引言 前面的文章中&#xff0c;我们有用过图像方法或者缩小的函数 resize() &#xff0c;这个函数既可以放大图像&#xff0c;也可以缩小图像&#xff0c;其中&#xff1a; 缩小图像&#xff1a;一版使用 CV_INETR_AREA &#xff08;区域插值&#xff09;来插值。放大图像&am…

高斯金字塔及拉普拉斯金字塔的Matlab实现

1、高斯金字塔计算步骤 &#xff08;1&#xff09;对第 i i i 层图像进行高斯内核卷积&#xff1b; &#xff08;2&#xff09;将所有偶数行和列去除&#xff08;下采样&#xff09;&#xff0c;得到第 i 1 i1 i1 层图像&#xff1b; &#xff08;3&#xff09;对原始图像不…

【C++】高斯金字塔和拉普拉斯金字塔原理和实现

【C】高斯金字塔和拉普拉斯金字塔原理和实现 图像中各个像素与其相邻像素之间的有很强的相关性&#xff0c;包含的信息也十分丰富&#xff0c;目标的尺寸有大有小&#xff0c;对比度有强有弱&#xff0c;此时就需要一个“显微镜”或者“望远镜”-----多尺度图像技术。它可以在…

python 高斯金字塔_12、高斯金字塔、拉普拉斯金字塔与图片尺寸缩放(示例代码)...

一、引言 我们经常会将某种尺寸的图像转换为其他尺寸的图像&#xff0c;如果放大或者缩小图片的尺寸&#xff0c;笼统来说的话&#xff0c;可以使用OpenCV为我们提供的如下两种方式&#xff1a; (1)resize函数。这是最直接的方式&#xff0c; (2)pyrUp( )、pyrDown( )函数。即图…

高斯金字塔的构建步骤

转自&#xff1a;https://www.cnblogs.com/starfire86/p/5735061.html SIFT(Scale-Invariant Feature Transform&#xff0c;尺度不变特征转换)在目标识别、图像配准领域具有广泛的应用&#xff0c;下面按照SIFT特征的算法流程对其进行简要介绍对SIFT特征做简要介绍。 高斯金字…

图像处理(九)高斯金字塔及拉普拉斯金字塔

高斯滤波的核就是一个求平均值的卷积核 高斯金字塔也是一个高斯核&#xff0c;只是步长为2&#xff0c;卷积后图片缩小二倍。高斯金字塔的目的是在图像不同大小时可以获得不同的信息&#xff0c;比如说图像较小时可以获得轮廓&#xff0c;图像较大时可以获得细节 使用函数cv2.p…

OpenCV SIFT源码讲解——构建高斯金字塔

目录 一、构建方法 二、函数重要点注释 一、构建方法 高斯金字塔每层图像的尺度为&#xff1a;。理论上金字塔每层图像可以从原图做的高斯滤波得到。但是实际操作中&#xff0c;每组的第一张影像&#xff08;除第一组&#xff09;是上一组倒数第三张影像降采样得到&#xff0c…

OpenCV中的图像金字塔(高斯金字塔、拉普拉斯金字塔)

最近在看关于数字图像的知识点&#xff0c;目前在图像金字塔部分&#xff0c;实在是懒得用手作笔记了&#xff0c;就以其中比较出名的“高斯金字塔”和“拉普拉斯金字塔”为例&#xff0c;基于OpenCV的源代码作解析存个档&#xff1b;毕竟属于基础部分&#xff0c;以后有需要就…

python 高斯金字塔_Python OpenCV 之图像金字塔,高斯金字塔与拉普拉斯金字塔

Python OpenCV 365 天学习计划&#xff0c;与橡皮擦一起进入图像领域吧。 基础知识铺垫 学习图像金字塔&#xff0c;发现网上的资料比较多&#xff0c;检索起来比较轻松。 图像金字塔是一张图像多尺度的表达&#xff0c;或者可以理解成一张图像不同分辨率展示。 金字塔越底层的…

图像金字塔和高斯金字塔

一&#xff1a;图像金字塔 图像金字塔是图像中多尺度表达的一种&#xff0c;最主要用于图像的分割&#xff0c;是一种以多分辨率来解释图像的有效但概念简单的结构。图像金字塔实际上是一张图片在不同尺度下的集合&#xff0c;即原图的上采样和下采样集合。金字塔的底部是高分辨…

opencv学习-高斯金字塔和拉普拉斯金字塔

图像金字塔 一个图像金字塔是由一系列的图像组成&#xff0c;最底下一张是图像尺寸最大&#xff0c;最上方的图像尺寸最小&#xff0c;从空间上从上向下看就像一个古代的金字塔。金字塔的底部是待处理图像的高分辨率表示&#xff0c;而顶部是低分辨率的近似。我们将一层一层的…

高斯金字塔与拉普拉斯金字塔的原理与python构建

转载自:https://zhuanlan.zhihu.com/p/94014493 高斯金字塔和拉普拉斯金字塔【1】在图像相关领域应用广泛&#xff0c;尤其是图像融合和图像分割方面。本文从理论和opencv实现两个方面对两种金字塔进行了介绍&#xff0c;并给出了二者的视觉效果。 1、高斯金字塔 在计算机视觉…

图像金字塔、高斯金字塔、拉普拉斯金字塔是怎么回事?附利用拉普拉斯金字塔和高斯金字塔重构原图的Python-OpenCV代码

图像金字塔是对图像进行多分辨率表示的一种有效且简单的结构。 一个图像金字塔是一系列以金字塔形状排列的分辨率逐步降低的图像。图像金字塔的底部是待处理图像的高分辨率表示&#xff0c;而顶部是低分辨率表示。 图像金字塔有什么作用&#xff1f; 图像金字塔常用于图像缩放…

高斯金字塔

1、为什么要构建高斯金字塔 高斯金字塔模仿的是图像的不同的尺度&#xff0c;尺度应该怎样理解&#xff1f;对于一副图像&#xff0c;你近距离观察图像&#xff0c;与你在一米之外观察&#xff0c;看到的图像效果是不同的&#xff0c;前者比较清晰&#xff0c;后者比较模糊&am…

系统集成项目管理工程师知识点

信息化知识 在这个学时里&#xff0c;将学习有关信息化的许多知识点&#xff0c;这些知识点的试题大多出现在上午试 题中。这引起知识点主要是&#xff1a; &#xff08;1&#xff09;信息与信息化的定义。 &#xff08;2&#xff09;国家信息化发展战略&#xff08;2006-2020&…