前言
上回說道ApkTool專案的概覽,關於ApkTool如何編譯,如何執行,還有各個引數的介紹。 今天想主要說明一下關於ApkTool如何分析resources.arsc檔案的,以及resources.arsc檔案的格式
總體流程
我們首先執行命令apktool d xxx.apk
,然後看輸出如下
I: Using Apktool 2.3.1 on douyin.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: C:\Users\hch\AppData\Local\apktool\framework\1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Baksmaling classes3.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
複製程式碼
其實這個時候apktool總體做了如下幾個步驟
- 載入resource table
- 解碼AndroidManifest.xml
- 解碼一些資原始檔
- 解碼dex檔案
- copy剩餘檔案
今天想和大家討論的只有第一步,關於ApkTool是如何解析resources.arsc的。
如何初始ApkDecoder的成員變數mResTable的,剩下的我們會下次繼續探討。
ps:想看大概結果的,直接跳到最後看圖。
resources.arsc的格式
resources.arsc是一個二進位制檔案,想要解析他就必須先弄懂這個檔案格式到底是什麼樣子的。 先上一張來源於網路的圖片。(圖片來源與網路,侵,刪)
其實整體的就是這個意思了,首先全部的話就是一個resource table,然後依次讀取String Pool,Package Header等。
這些格式,具體的都在Android原始碼裡面,具體的檔案是ResourceTypes.h, 比如:
struct ResChunk_header
{
uint16_t type;
uint16_t headerSize;
uint32_t size;
};
enum {
RES_NULL_TYPE = 0x0000,
RES_STRING_POOL_TYPE = 0x0001,
RES_TABLE_TYPE = 0x0002,
RES_XML_TYPE = 0x0003,
// Chunk types in RES_XML_TYPE
RES_XML_FIRST_CHUNK_TYPE = 0x0100,
RES_XML_START_NAMESPACE_TYPE= 0x0100,
RES_XML_END_NAMESPACE_TYPE = 0x0101,
RES_XML_START_ELEMENT_TYPE = 0x0102,
RES_XML_END_ELEMENT_TYPE = 0x0103,
RES_XML_CDATA_TYPE = 0x0104,
RES_XML_LAST_CHUNK_TYPE = 0x017f,
// This contains a uint32_t array mapping strings in the string
// pool back to resource identifiers. It is optional.
RES_XML_RESOURCE_MAP_TYPE = 0x0180,
// Chunk types in RES_TABLE_TYPE
RES_TABLE_PACKAGE_TYPE = 0x0200,
RES_TABLE_TYPE_TYPE = 0x0201,
RES_TABLE_TYPE_SPEC_TYPE = 0x0202
};
struct ResStringPool_header
{
struct ResChunk_header header;
uint32_t stringCount;
uint32_t styleCount;
enum {
SORTED_FLAG = 1<<0,
UTF8_FLAG = 1<<8
};
uint32_t flags;
uint32_t stringsStart;
uint32_t stylesStart;
};
複製程式碼
因為篇幅原因,所以把註釋部分刪除掉了,具體的大家可以查閱原始碼,也有一個不錯的原始碼閱讀網站分享給大家,想看的話可以不用下載啦,直接線上看就好了。
解析流程
我們首先看Main.java
public static void main(String[] args) throws IOException, InterruptedException, BrutException {
//......略......
boolean cmdFound = false;
for (String opt : commandLine.getArgs()) {
if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
//主要是這裡,執行了cmdDecode方法來解碼
cmdDecode(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
cmdBuild(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
cmdInstallFramework(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("empty-framework-dir")) {
cmdEmptyFrameworkDirectory(commandLine);
cmdFound = true;
} else if (opt.equalsIgnoreCase("publicize-resources")) {
cmdPublicizeResources(commandLine);
cmdFound = true;
}
}
//......略......
}
複製程式碼
主要是呼叫了cmdDecode方法來解碼,我們跟進去看看
private static void cmdDecode(CommandLine cli) throws AndrolibException {
//先new了一個APkDecoder類,主要是利用這個類進行解碼
ApkDecoder decoder = new ApkDecoder();
int paraCount = cli.getArgList().size();
String apkName = cli.getArgList().get(paraCount - 1);
File outDir;
//這裡主要是根據我們設定的一些引數,然後來對應的設定decoder類的成員變數,、
//最主要的主要是設定好輸出目錄,一些模式,以及版本等
if(//......略......) {
//......略......
} else {
// make out folder manually using name of apk
String outName = apkName;
outName = outName.endsWith(".apk") ? outName.substring(0,
outName.length() - 4).trim() : outName + ".out";
//設定輸出目錄
outName = new File(outName).getName();
outDir = new File(outName);
decoder.setOutDir(outDir);
}
//......略......
decoder.setApkFile(new File(apkName));
try {
//開始解碼
decoder.decode();
} catch (OutDirExistsException ex) {
//......略......
} finally {
//......略......
}
}
複製程式碼
我們跟進decoder.decode()方法來看看
public void decode() throws AndrolibException, IOException, DirectoryException {
try {
//獲取輸出目錄
File outDir = getOutDir();
//這裡其實是和我們輸入的一個keep-broken-res引數有關
AndrolibResources.sKeepBroken = mKeepBrokenResources;
//判斷是否需要覆蓋
if (!mForceDelete && outDir.exists()) {
throw new OutDirExistsException();
}
//判斷apk檔案是否合法
if (!mApkFile.isFile() || !mApkFile.canRead()) {
throw new InFileNotFoundException();
}
//清理乾淨需要輸出的目錄,準備寫入
try {
OS.rmdir(outDir);
} catch (BrutException ex) {
throw new AndrolibException(ex);
}
outDir.mkdirs();
//列印log資訊,這個時候就對應我們執行apktool d xxx.apk時候的第一句了
LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
//判斷apk內是否有resources.arsc檔案,
if (hasResources()) {
//判斷解碼Resources
switch (mDecodeResources) {
case DECODE_RESOURCES_NONE:
mAndrolib.decodeResourcesRaw(mApkFile, outDir);
if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
setTargetSdkVersion();
setAnalysisMode(mAnalysisMode, true);
// done after raw decoding of resources because copyToDir overwrites dest files
if (hasManifest()) {
mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
}
}
break;
case DECODE_RESOURCES_FULL:
setTargetSdkVersion();
setAnalysisMode(mAnalysisMode, true);
if (hasManifest()) {
mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
}
mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
break;
}
} else {
// if there's no resources.arsc, decode the manifest without looking
// up attribute references
if (hasManifest()) {
if (mDecodeResources == DECODE_RESOURCES_FULL
|| mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable());
}
else {
mAndrolib.decodeManifestRaw(mApkFile, outDir);
}
}
}
//......略......
}
複製程式碼
一般來說的話,我們會執行到DECODE_RESOURCES_FULL分支裡面的,這裡面的第一步是setTargetSdkVersion。
我們主要再看看setTargetSdkVersion方法的內部實現
public void setTargetSdkVersion() throws AndrolibException, IOException {
if (mResTable == null) {
mResTable = mAndrolib.getResTable(mApkFile);
}
Map<String, String> sdkInfo = mResTable.getSdkInfo();
if (sdkInfo.get("targetSdkVersion") != null) {
mApi = Integer.parseInt(sdkInfo.get("targetSdkVersion"));
}
}
複製程式碼
其實ApkDecoder內部是維護了一個mResTable的,我們的任何的資訊都是根據mResTable來取的,那可能會問了,那ApkDecoder內部的ResTable到底是個什麼東西呢,其實他就是我們上面的部分說的那張經典的圖。
當ApkDecoder發現mResTable變數是空的的時候,會對此進行初始化,接下來我們就主要看看Androlib的getResTable方法,這個方法就是主要從apkFile裡面讀出mResTable,分析他的格式,
//Androidlib.java檔案內容
public ResTable getResTable(ExtFile apkFile)
throws AndrolibException {
return mAndRes.getResTable(apkFile, true);
}
//AndrolibResources.java的getResTable方法
public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
throws AndrolibException {
ResTable resTable = new ResTable(this);
if (loadMainPkg) {
loadMainPkg(resTable, apkFile);
}
return resTable;
}
複製程式碼
上面的程式碼掉有了mAndRes的getResTable方法,然後內部再呼叫loadMainPkg方法,我們繼續跟進內部實現
public ResPackage loadMainPkg(ResTable resTable, ExtFile apkFile)
throws AndrolibException {
//列印log資訊,這個時候就對應到了我們上面說的第二句了
LOGGER.info("Loading resource table...");
ResPackage[] pkgs = getResPackagesFromApk(apkFile, resTable, sKeepBroken);
ResPackage pkg = null;
switch (pkgs.length) {
case 1:
pkg = pkgs[0];
break;
case 2:
if (pkgs[0].getName().equals("android")) {
LOGGER.warning("Skipping \"android\" package group");
pkg = pkgs[1];
break;
} else if (pkgs[0].getName().equals("com.htc")) {
LOGGER.warning("Skipping \"htc\" package group");
pkg = pkgs[1];
break;
}
default:
pkg = selectPkgWithMostResSpecs(pkgs);
break;
}
if (pkg == null) {
throw new AndrolibException("arsc files with zero packages or no arsc file found.");
}
resTable.addPackage(pkg, true);
return pkg;
}
複製程式碼
這個時候首先是執行getResPackagesFromApk方法,獲取ResPackage資訊,
private ResPackage[] getResPackagesFromApk(ExtFile apkFile,ResTable resTable, boolean keepBroken)
throws AndrolibException {
try {
Directory dir = apkFile.getDirectory();
BufferedInputStream bfi = new BufferedInputStream(dir.getFileInput("resources.arsc"));
try {
//主要是這個方法來對resources.arsc檔案進行解析
return ARSCDecoder.decode(bfi, false, keepBroken, resTable).getPackages();
} finally {
try {
bfi.close();
} catch (IOException ignored) {}
}
} catch (DirectoryException ex) {
throw new AndrolibException("Could not load resources.arsc from file: " + apkFile, ex);
}
}
複製程式碼
我們跟進ARSCDecoder的decode方法
public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken,
ResTable resTable)
throws AndrolibException {
try {
//首先根據輸入流,resTable等引數new一個ARSCDecoder
ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken);
//
ResPackage[] pkgs = decoder.readTableHeader();
return new ARSCData(pkgs, decoder.mFlagsOffsets == null
? null
: decoder.mFlagsOffsets.toArray(new FlagsOffset[0]), resTable);
} catch (IOException ex) {
throw new AndrolibException("Could not decode arsc file", ex);
}
}
複製程式碼
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
nextChunkCheckType(Header.TYPE_TABLE);
int packageCount = mIn.readInt();
mTableStrings = StringBlock.read(mIn);
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
mTypeIdOffset = 0;
packages[i] = readTablePackage();
}
return packages;
}
複製程式碼
那麼這裡的時候,關鍵的點總算來了,首先是讀取了ChunkCheckType,Header.TYPE_TABLE的值是0x0002, 這裡的type正好對應上了我們在ResourceTypes.h裡面對應的RES_TABLE_TYPE = 0x0002
,其實就是圖中最外層的那個ResourceTable
我們跟進nextChunkCheckType方法,
//ARSCDecoder類內
private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException {
nextChunk();
//這時候這裡的引數expectedType的值是2,也就是RES_TABLE_TYPE的,
checkChunkType(expectedType);
}
//ARSCDecoder類內
private Header nextChunk() throws IOException {
return mHeader = Header.read(mIn, mCountIn);
}
//Header類內
public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException {
short type;
int start = countIn.getCount();
try {
//首先讀出type,
type = in.readShort();
} catch (EOFException ex) {
return new Header(TYPE_NONE, 0, 0, countIn.getCount());
}
//這裡分別解釋下4個引數,
//第一個引數type 對應的型別 2個位元組
//第二個引數 頭大小 2個位元組
//第三個引數 檔案大小 4個位元組
//第四個引數 暫時我們start位置為0
//然後返回new出來的Header
return new Header(type, in.readShort(), in.readInt(), start);
}
private void checkChunkType(int expectedType) throws AndrolibException {
//這裡主要校驗的就是我們剛剛header的type和我們傳入的是否相同,不同就拋異常了
if (mHeader.type != expectedType) {
throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x",
expectedType, mHeader.type));
}
}
複製程式碼
讀取一個Chunk,如上方所示呼叫關係,關鍵的地方已經加上了註釋。
nextChunkCheckType(Header.TYPE_TABLE)
主要是讀取了下面紅圈的部分。
我們繼續分析readTableHeader方法。
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
//主要是讀取紅圈部分的值
nextChunkCheckType(Header.TYPE_TABLE);
//讀取上圖紅圈後面的packageCount變數,4位元組
int packageCount = mIn.readInt();
//接下來就是主要分析這裡了,讀取Global String Pool
mTableStrings = StringBlock.read(mIn);
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
mTypeIdOffset = 0;
packages[i] = readTablePackage();
}
return packages;
}
複製程式碼
接下來主要分析StringBlock的read方法
public static StringBlock read(ExtDataInput reader) throws IOException {
//這裡主要是跳過了RES_STRING_POOL_TYPE,和頭大小兩個,並且還校驗了一下,
//校驗的方法就是和CHUNK_STRINGPOOL_TYPE比對一下,CHUNK_STRINGPOOL_TYPE的值是0x001C0001
//這是因為RES_STRING_POOL_TYPE的值是0x0001,頭大小是0x001C,所以這個CHUNK_STRINGPOOL_TYPE就是0x001C0001了
reader.skipCheckInt(CHUNK_STRINGPOOL_TYPE);
//讀取塊大小,Global String Pool內
int chunkSize = reader.readInt();
// ResStringPool_header
//字串數
int stringCount = reader.readInt();
//style數
int styleCount = reader.readInt();
//flags標記,1是SORTED_FLAG,256是UTF8_FLAG
int flags = reader.readInt();
//字串起始位置
int stringsOffset = reader.readInt();
//style起始位置
int stylesOffset = reader.readInt();
//new一個StringBlock
StringBlock block = new StringBlock();
//根據讀取出的flags資訊,來設定block
block.m_isUTF8 = (flags & UTF8_FLAG) != 0;
//初始化block變數
block.m_stringOffsets = reader.readIntArray(stringCount);
block.m_stringOwns = new int[stringCount];
Arrays.fill(block.m_stringOwns, -1);
//初始化block內部style
if (styleCount != 0) {
block.m_styleOffsets = reader.readIntArray(styleCount);
}
int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset;
block.m_strings = new byte[size];
reader.readFully(block.m_strings);
if (stylesOffset != 0) {
size = (chunkSize - stylesOffset);
block.m_styles = reader.readIntArray(size / 4);
// read remaining bytes
int remaining = size % 4;
if (remaining >= 1) {
while (remaining-- > 0) {
reader.readByte();
}
}
}
//返回最終的結果
return block;
}
複製程式碼
reader.skipCheckInt(CHUNK_STRINGPOOL_TYPE)
跳過的部分如下:
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
//主要是讀取紅圈部分的值
nextChunkCheckType(Header.TYPE_TABLE);
//讀取上圖紅圈後面的packageCount變數,4位元組
int packageCount = mIn.readInt();
//接下來就是主要分析這裡了,讀取Global String Pool
mTableStrings = StringBlock.read(mIn);
//此時此刻執行到了這裡,要開始分析ResPackage了
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
mTypeIdOffset = 0;
//使用readTablePackage方法來分析
packages[i] = readTablePackage();
}
return packages;
}
複製程式碼
重複性任務
emmmmm。。。。 博主分析到了這裡,如果你能讀到這裡我自己也感受到很高興啊,希望能給你帶來了幫助。其實後序的分析readTablePackage方法和之前的一樣啦,博主詳細如果你讀懂了前面的分析,那麼這個肯定也不在話下。
所以呢,我就不一一的帶大家理解,主要的還是看懂那張圖,然後看懂ApkTool是如何來分析就可以啦。
這樣做的好處就是,如果有apk在這個resource.arsc檔案內做文章,我們可以debug反查,看看到底是怎麼回事,可以有一些自己對付的思路。
readTablePackage之後
讀取完了之後,程式就會一步一步的返回回去,這個時候我們的mResTable變數就初始化好了,就可以繼續進行setTargetSdkVersion
方法的執行了,
我們這篇部落格主要就是進行ApkDeocder成員變數mResTable的初始化分析, 我畫了個圖,希望能幫助大家慮說清楚上面的一系列呼叫
participant Main
participant ApkDecoder
participant Androidlib
participant AndrolibResources
participant ARSCDecoder
Main->Main: cmdDecode
Main->ApkDecoder: decode
ApkDecoder->ApkDecoder: setTargetSdkVersion
ApkDecoder->Androidlib: getResTable
Androidlib->AndrolibResources: getResTable
AndrolibResources->AndrolibResources:loadMainPkg
AndrolibResources->AndrolibResources:getResPackagesFromApk
AndrolibResources->ARSCDecoder: decode
ARSCDecoder->ARSCDecoder: readTableHeader
ARSCDecoder->ARSCDecoder: nextChunkCheckType
ARSCDecoder->ARSCDecoder: nextChunk
ARSCDecoder->ARSCDecoder: readTablePackage
ARSCDecoder-->AndrolibResources:
AndrolibResources-->Androidlib:
Androidlib-->ApkDecoder:
ApkDecoder-->Main:
複製程式碼
防止在某些平臺上,不支援Markdown的UML圖,下面特意放一張圖片
寫在最後
分析原始碼並不難,希望大家都能耐下心來一點一點看,一點一點除錯分析。 文章一層一層的呼叫很深,所以可能會給讀者困惑,有困惑的,可以聯絡我,我也喜歡和讀者一起探討啦,有寫的不對的地方多多指教。
正因為呼叫比較深,所以最後畫出了UML圖,希望能讓大家看得更簡單明瞭
關於我
個人部落格:MartinHan的小站
部落格網站:hanhan12312的專欄
知乎:MartinHan01