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这个过程中产生的。
太早则相关文件还未生成出来,太晚则可能影响到后面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,所以出现上面的错误。
问题出现在这里,我们上面只修改了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
中间的两个字节表示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,里面确实还存在一些完整的资源索引。
检查了一下修改后的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,代码如下:
大于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 0x7d090062W/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文件,需要对二进制文件结构有一定的了解,实际上就是要有反编译这些文件,或者部分内容的能力。
我们还需要了解整个打包流程,每个阶段都做了哪些事情,才能知道要在什么时机来做这些事情。
Android进阶之路系列:http://blog.csdn.net/column/details/16488.html