preface
Last time I gave you an overview of the ApkTool project, how ApkTool is compiled, how to run, and the introduction of various parameters. How does ApkTool analyze resources. Arsc files
The overall process
We first execute the command apktool d xxx.apk and see the output as follows
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...Copy the code
In this case, apktool does the following steps
- Load the resource table
- Decoding AndroidManifest. XML
- Decode some resource files
- Decode dex files
- Copy Remaining files
Today I want to discuss only the first step, about how ApkTool resolves resources.arsc.
How to initialize ApkDecoder member variable mResTable, the rest will be discussed next time.
Ps: If you want to see the general result, skip to the last image.
Resources. The format of arsc
Resources. Arsc is a binary file, and to parse it he must first understand what the file format looks like. Let’s start with a picture from the Internet. (Image source and network, abuse, deletion)
Resource table, String Pool,Package Header, etc.
These formats are in the Android source code, and the specific file is resourcetypes.h, for example:
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;
};
Copy the code
Because of the length of the reason, so the annotation part was deleted, we can refer to the specific source code, there is also a good source code reading site to share with you, if you want to see it can not download, see it directly online.
Source site address,
Analytical process
Let’s start with main.java
public static void main(String[] args) throws IOException, InterruptedException, BrutException {
/ /... A little...
boolean cmdFound = false;
for (String opt : commandLine.getArgs()) {
if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
// Mainly here, the cmdDecode method is implemented to decode
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; }}/ /... A little...
}
Copy the code
CmdDecode is mainly called to decode method, let’s follow in to see
private static void cmdDecode(CommandLine cli) throws AndrolibException {
// First new APkDecoder class, mainly use this class to decode
ApkDecoder decoder = new ApkDecoder();
int paraCount = cli.getArgList().size();
String apkName = cli.getArgList().get(paraCount - 1);
File outDir;
// Here is mainly according to some parameters we set, and then set the corresponding decoder class member variables,
// The main thing is to set up the output directory, some mode, and version, etc
if(/ /... A little... {
/ /... A little...
} 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";
// Set the output directory
outName = new File(outName).getName();
outDir = new File(outName);
decoder.setOutDir(outDir);
}
/ /... A little...
decoder.setApkFile(new File(apkName));
try {
// Start decoding
decoder.decode();
} catch (OutDirExistsException ex) {
/ /... A little...
} finally {
/ /... A little...}}Copy the code
We follow the decoder.decode() method to have a look
public void decode(a) throws AndrolibException, IOException, DirectoryException {
try {
// Get the output directory
File outDir = getOutDir();
// This is related to the keep-broken-res parameter we entered
AndrolibResources.sKeepBroken = mKeepBrokenResources;
// Determine whether overwriting is required
if(! mForceDelete && outDir.exists()) {throw new OutDirExistsException();
}
// Check whether the APK file is valid
if(! mApkFile.isFile() || ! mApkFile.canRead()) {throw new InFileNotFoundException();
}
// Clean up the directory to be output, ready to write
try {
OS.rmdir(outDir);
} catch (BrutException ex) {
throw new AndrolibException(ex);
}
outDir.mkdirs();
Apktool d xxx.apk
LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
// Check if there are resources. Arsc files in apK.
if (hasResources()) {
// Determine decode 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); }}}/ /... A little...
}
Copy the code
Typically, we execute to the DECODE_RESOURCES_FULL branch, where the first step is setTargetSdkVersion.
Let’s focus on the internal implementation of the setTargetSdkVersion method
public void setTargetSdkVersion(a) 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")); }}Copy the code
In fact, ApkDecoder internal is to maintain a mResTable, any of our information is based on mResTable to take, that may ask, the ApkDecoder internal ResTable is what it is, in fact, he is our part of the above said that the classic figure.
When ApkDecoder finds that the mResTable variable is empty, it initializes it. Next we’ll look at Androlib’s getResTable method, which reads the mResTable from the apkFile and analyzes its format.
// androidlib.java file contents
public ResTable getResTable(ExtFile apkFile)
throws AndrolibException {
return mAndRes.getResTable(apkFile, true);
}
// the getResTable method of androlibResources.java
public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
throws AndrolibException {
ResTable resTable = new ResTable(this);
if (loadMainPkg) {
loadMainPkg(resTable, apkFile);
}
return resTable;
}
Copy the code
The above code takes mAndRes’ getResTable method and then internally calls the loadMainPkg method. We follow up with the internal implementation
public ResPackage loadMainPkg(ResTable resTable, ExtFile apkFile)
throws AndrolibException {
// Print the log information
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;
}
Copy the code
The getResPackagesFromApk method is executed to retrieve the ResPackage information.
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 {
// This is the main method to parse the resources.arsc file
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); }}Copy the code
We follow the DECODE method of ARSCDecoder
public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken,
ResTable resTable)
throws AndrolibException {
try {
// Start with an ARSCDecoder based on the input stream, resTable and other parameters
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); }}Copy the code
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;
}
Copy the code
The key point is that the ChunkCheckType is read, The value of header. TYPE_TABLE is 0x0002, where type corresponds to RES_TABLE_TYPE = 0x0002 (ResourceTable)
We follow up the nextChunkCheckType method,
/ / ARSCDecoder within the class
private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException {
nextChunk();
// Where the parameter expectedType is 2, RES_TABLE_TYPE,
checkChunkType(expectedType);
}
/ / ARSCDecoder within the class
private Header nextChunk(a) throws IOException {
return mHeader = Header.read(mIn, mCountIn);
}
/ / the Header in the class
public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException {
short type;
int start = countIn.getCount();
try {
// First read type,
type = in.readShort();
} catch (EOFException ex) {
return new Header(TYPE_NONE, 0.0, countIn.getCount());
}
// Define the four parameters.
// The type corresponding to the first parameter type is 2 bytes
// The second argument header is 2 bytes in size
// The third parameter file size is 4 bytes
// For the fourth argument we temporarily set the start position to 0
// Then return the Header from new
return new Header(type, in.readShort(), in.readInt(), start);
}
private void checkChunkType(int expectedType) throws AndrolibException {
// Check if the type of the header is the same as that passed in
if(mHeader.type ! = expectedType) {throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x", expectedType, mHeader.type)); }}Copy the code
Reading a Chunk, as shown above, calls the relationship, with the key points annotated.
NextChunkCheckType (header.type_table) mainly reads the part circled in red below.
Let’s move on to the readTableHeader method.
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
// Read the value of the red circle
nextChunkCheckType(Header.TYPE_TABLE);
// Read the packageCount variable after the red circle in the figure above, 4 bytes
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;
}
Copy the code
Next, we’ll focus on the read method of a StringBlock
public static StringBlock read(ExtDataInput reader) throws IOException {
// RES_STRING_POOL_TYPE and the header size are skipped.
// The verification method is to compare with CHUNK_STRINGPOOL_TYPE, whose value is 0x001C0001
// This is because RES_STRING_POOL_TYPE has a value of 0x0001 and a header size of 0x001C, so CHUNK_STRINGPOOL_TYPE is 0x001C0001
reader.skipCheckInt(CHUNK_STRINGPOOL_TYPE);
// Read the block size in the Global String Pool
int chunkSize = reader.readInt();
// ResStringPool_header
// Number of strings
int stringCount = reader.readInt();
/ / style number
int styleCount = reader.readInt();
//flags flags, 1 is SORTED_FLAG, 256 is UTF8_FLAG
int flags = reader.readInt();
// Start position of string
int stringsOffset = reader.readInt();
//style start position
int stylesOffset = reader.readInt();
/ / new a StringBlock
StringBlock block = new StringBlock();
// Set blocks based on flags readblock.m_isUTF8 = (flags & UTF8_FLAG) ! =0;
// Initialize the block variable
block.m_stringOffsets = reader.readIntArray(stringCount);
block.m_stringOwns = new int[stringCount];
Arrays.fill(block.m_stringOwns, -1);
// Initialize the block internal 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 the final result
return block;
}
Copy the code
Reader.skipcheckint (CHUNK_STRINGPOOL_TYPE) skips the following:
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
// Read the value of the red circle
nextChunkCheckType(Header.TYPE_TABLE);
// Read the packageCount variable after the red circle in the figure above, 4 bytes
int packageCount = mIn.readInt();
//接下来就是主要分析这里了,读取Global String Pool
mTableStrings = StringBlock.read(mIn);
// At this point in time, it is time to analyze ResPackage
ResPackage[] packages = new ResPackage[packageCount];
nextChunk();
for (int i = 0; i < packageCount; i++) {
mTypeIdOffset = 0;
// Use the readTablePackage method to analyze
packages[i] = readTablePackage();
}
return packages;
}
Copy the code
Repetitive task
Emmmmm… Blogger analysis here, if you can read this I feel very happy ah, hope to help you. ReadTablePackage readTablePackage readTablePackage readTablePackage readTablePackage readTablePackage
So, I will not take you to understand, the main thing is to understand that picture, and then understand how ApkTool is to analyze it.
The advantage of this is that if an APK is doing something with the resource. Arsc file, we can debug it to see what is going on and have some ideas for dealing with it ourselves.
After readTablePackage
After reading, the program will step back, at which point our mResTable variable is initialized and we can continue to execute the setTargetSdkVersion method.
The main focus of this blog post is the initialization analysis of the ApkDeocder member variable mResTable. I have drawn a diagram to help you think through the above series of calls
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:
Copy the code
In case Markdown’s UML diagrams are not supported on some platforms, a special image is included below
Write in the last
It is not difficult to analyze the source code, and I hope everyone can afford to look at it bit by bit and debug it bit by bit. The article is very deep, so it may give readers confused, confused, please contact me, I also like to discuss with readers, there are wrong places to write more advice.
Because of the depth of the call, I drew a UML diagram in the end, hoping to make it easier for you to see
About me
Personal blog: MartinHan’s station
Blog site: Hanhan12312 column
Zhihu: MartinHan01