Android進階之路系列:http://blog.csdn.net/column/details/16488.html
一、引言
1、爲什麼要動態修改資源索引
2、怎麼修改資源索引
3、什麼時候修改
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這個過程中產生的。
有關processXXXXResources的詳解請閱讀《gradle編譯打包過程 之 ProcessAndroidResources的源碼分析》
二、處理Task及R文件
1、處理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文件
build/intermediates/symbols/[productFlavors]/[buildType]/R.txt (這個貌似與kotlin有關)
build/generated/source/r/[productFlavors]/[buildType]/[packageName]/R.java
sourceOutputDir是build/generated/source/r/[productFlavors]/[buildType]/
(注意,上面是基於gradle2.3.3版本,gradle3.0.0版本ProcessAndroidResources代碼變動很大,需要使用一個函數來獲取,而且獲取的路徑也有所不同,所以doLast代碼塊中處理有不同)
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文件修改成功了,這時候如果編譯運行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常量,供在代碼中索引資源。
問題出現在這裏,我們上面只修改了R.java,對於resources.arsc文件沒有動,這樣resources.arsc中還是舊的id,所以出現上面的錯誤。
三、處理編譯後的二進制文件
1、編譯後的文件在哪?
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
2、解壓、壓縮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)
}
}
}
這部分不是重點,不細說了,注意壓縮的時候不能帶着根目錄。
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_文件同名,防止出現混亂。
3、修改resources.arsc文件的pkgId
中間的兩個字節表示resTypeId,類型id即資源的類型(string、color等),這個值從0開始。(注意每個類型的id不是固定的)
最低四個字節表示這個資源的順序id,從1開始,逐漸累加1
那麼package id在哪?我們來看resources.arsc文件部分結構:
我們的思路是每次讀取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()
}
代碼很簡單,就不細說了。
4、修改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
檢查了一下修改後的resources.arsc,裏面確實還存在一些完整的資源索引。
5、修改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)
}
}
大於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一樣。
W/ResourceType: Failed resolving bag parent id 0x7d090062W/ResourceType: Attempt to retrieve bag 0x7d090114 which is invalid or in a cycle.
6、添加資源包id映射
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結構的數據,然後將文件大小增加並重新寫回;