通过ApkTool分析resources.arsc文件以及resources.arsc文件的格式

resources.arsc是Android打包后,资源映射文件,所有res目录下的资源都会便已到此文件里面。理解文件对我们认识安卓和做安卓开发会有很大帮助。

今天我们通过一个反编译工具来了解和认识resources.arsc。

反编译

通过apktool我们可以反编译出apk的资源。 apktool源码地址

命令:apktool d xxx.apk(或java -jar apktool.jar d xxx.apk)

# 使用apktool反编译apk, java -jar apktool.jar d xxx.apk
192:android_tools yuh$ java -jar ./apktool_2.2.2.jar d app-debug.apk 
# 输出
I: Using Apktool 2.2.2 on app-debug.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/yuh/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: null reference: m1=0x01010540(reference), m2=0xffffffff(bool)
I: Baksmaling classes.dex...
I: Baksmaling classes2.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

Apktool工作流程:

  • 加载resource table
  • 解码AndroidManifest.xml
  • 解码一些资源文件
  • 解码dex文件
  • copy剩余文件

后面的步骤与今天的主题无关,暂不研究,主要是第一步加载和解析resources.arsc

如何得到resources.arsc

修改apk的后缀为zip,然后解压就行了

resources.arsc结构

我们通过一个很经典的图片来认识一下resources.arsc。 图片来自网络。 一张图道破天机呀。

对应的Android里面的源码/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h。 网络查看全部源码

// 部分源码,全部源码查看上面的链接
// 对应的枚举
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,
    RES_TABLE_LIBRARY_TYPE      = 0x0203
};

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;
};

Apktool解析流程

public static void main(String[] args) throws IOException, InterruptedException, BrutException {
        // 略 解析参数类
        boolean cmdFound = false;
        for (String opt : commandLine.getArgs()) {
        // 此处即我们输入的命令参数 d
            if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("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;
            }
        }
 // 略
    }

接着查看cmdDecode()函数

private static void cmdDecode(CommandLine cli) throws AndrolibException {
        ApkDecoder decoder = new ApkDecoder();
        // 略  解析 设置一些参数
         decoder.setApkFile(new File(apkName));
        decoder.decode(); // 开始解析
        // 略
    }

继续查看decode()方法。

public void decode() throws AndrolibException, IOException, DirectoryException {
        try {
           // 一些必要的检查 略
            // 判断 输入的文件是否包含resources.arsc 反编译apk必须有 true
            if (hasResources()) {
            // 使用那种解析方式, 如果默认就是DECODE_RESOURCES_FULL
                switch (mDecodeResources) {
                    case DECODE_RESOURCES_NONE:
                       // 略
                        break;
                        // 全部解析出来
                    case DECODE_RESOURCES_FULL:
                    // 读取版本 resources.arsc头信息
                        setTargetSdkVersion(); // 见后面setTargetSdkVersion 流程分析
                        setAnalysisMode(mAnalysisMode, true);
                    // 判断是否有manifest 并解析。流程分析略
                        if (hasManifest()) {
                            mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
                        }
                        // 解析其他资源
                        mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
                        break;
                }
            // 略 
        } catch (Exception ex) {
            throw ex;
        } finally {
            try {
                mApkFile.close();
            } catch (IOException ignored) {}
        }
    }

setTargetSdkVersion();

public void setTargetSdkVersion() throws AndrolibException, IOException {
// mResTable 表示整个resource.asrc
if (mResTable == null) {
// 解析
            mResTable = mAndrolib.getResTable(mApkFile);
        }

        Map<String, String> sdkInfo = mResTable.getSdkInfo();
        if (sdkInfo.get("targetSdkVersion") != null) {
            mApi = Integer.parseInt(sdkInfo.get("targetSdkVersion"));
        }
    }

mAndroidlib是AndroidLib的实例,里面实际使用到了AndrolibResources。 调用mAndrolib.getResTable(mApkFile)实际是AndrolibResources.getResTable()。所以我们直接来看看,AndrolibResources的实现。

public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
            throws AndrolibException {
    // 实例化一个ResTable
        ResTable resTable = new ResTable(this);
        if (loadMainPkg) {
        // 加载apk
            loadMainPkg(resTable, apkFile);
        }
        return resTable;
    }

接着查看loadMainPkg()

public ResPackage loadMainPkg(ResTable resTable, ExtFile apkFile)
            throws AndrolibException {
    // 解析文件
        ResPackage[] pkgs = getResPackagesFromApk(apkFile, resTable, sKeepBroken);
        // 本例中pkgs.length = 1
       //略
       // 填充 restable部分信息
         resTable.addPackage(pkg, true); 
        return pkg;
    }

接下来就到解析的地方:

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 {
            // 读取文件
                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 {
            ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken);
            // 读取头
            // Res_TABLE_COUNT 2byte
            // 头大小 2byte
            // 文件大小 4byte
            // packeage数量 4byte
            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);
        }
    }

接着查看decoder.readTableHeader()

private ResPackage[] readTableHeader() throws IOException, AndrolibException {
// 读取前3个head
nextChunkCheckType(Header.TYPE_TABLE);
// 读取package大小
        int packageCount = mIn.readInt();
// 读取string pool 见后面StringBlock.read(mIn) 分析
        mTableStrings = StringBlock.read(mIn);
        ResPackage[] packages = new ResPackage[packageCount];

// 下一个块 
        nextChunk();
        // Package 可能多个
        for (int i = 0; i < packageCount; i++) {
            mTypeIdOffset = 0;
            // 解析每个package结构 见后面分析
            packages[i] = readTablePackage();
        }
        return packages;
    }

部分方法简单我们连这列出调用顺序nextChunkCheckType() -> nextChunk() -> Header.read(mIn, mCountIn)

public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException {
            short type;
            int start = countIn.getCount();
            try {
            // Res_TABLE_COUNT 2byte
                type = in.readShort();
            } catch (EOFException ex) {
                return new Header(TYPE_NONE, 0, 0, countIn.getCount());
            }
                    // 头大小 2byte
                    // 文件大小 4byte
            return new Header(type, in.readShort()/*头大小*/, in.readInt()/*文件大小*/, start);
        }
本列apk:
type=2 // 固定就是2
headerSize=12
chunkSize=233584
package count = 1 // 通常固定1

StringBlock.read(mIn) 分析

public static StringBlock read(ExtDataInput reader) throws IOException {
        reader.skipCheckChunkTypeInt(CHUNK_STRINGPOOL_TYPE, CHUNK_NULL_TYPE);
        int chunkSize = reader.readInt(); // 块大小大小

        // ResStringPool_header
        int stringCount = reader.readInt(); // 字符串数量
        int styleCount = reader.readInt(); // style数量
        int flags = reader.readInt(); // 标记
        int stringsOffset = reader.readInt(); // 字符串起始位置
        int stylesOffset = reader.readInt(); // style起始位置

// 字符串偏移数组
        StringBlock block = new StringBlock();
        block.m_isUTF8 = (flags & UTF8_FLAG) != 0;
        block.m_stringOffsets = reader.readIntArray(stringCount);
        block.m_stringOwns = new int[stringCount];
        Arrays.fill(block.m_stringOwns, -1);

// 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;
    }

readTablePackage() 分析

private ResPackage readTablePackage() throws IOException, AndrolibException {
        checkChunkType(Header.TYPE_PACKAGE); // package 头 即RES_TYPE_PACKAGE_TYPE
        int id = mIn.readInt(); // packageId

        if (id == 0) {
            id = 2;
            if (mResTable.getPackageOriginal() == null && mResTable.getPackageRenamed() == null) {
                mResTable.setSharedLibrary(true);
            }
        }

        String name = mIn.readNullEndedString(128, true);
        /* typeStrings */mIn.skipInt();
        /* lastPublicType */mIn.skipInt();
        /* keyStrings */mIn.skipInt();
        /* lastPublicKey */mIn.skipInt();

        // TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e
        // which is only in split/new applications.
        int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4
        if (mHeader.headerSize == splitHeaderSize) {
            mTypeIdOffset = mIn.readInt();
        }

        if (mTypeIdOffset > 0) {
            LOGGER.warning("Please report this application to Apktool for a fix: https://github.com/iBotPeaches/Apktool/issues/1728");
        }

        mTypeNames = StringBlock.read(mIn);
        mSpecNames = StringBlock.read(mIn);

        mResId = id << 24;
        mPkg = new ResPackage(mResTable, id, name);

        nextChunk();
        while (mHeader.type == Header.TYPE_LIBRARY) {
            readLibraryType();
        }

        while (mHeader.type == Header.TYPE_SPEC_TYPE) {
            readTableTypeSpec();
        }

        return mPkg;
    }

readTablePackage()完成后ResTable就全部有值了,解析就完成了。

梳理

整个流程

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:
声明:原创文章,版权所有,转载请注明出处,https://litets.com。