ApkTool項目解析resources.arsc詳解

前言

上回說道ApkTool項目的概覽,關於ApkTool如何編譯,如何運行,還有各個參數的介紹。 今天想主要說明一下關於ApkTool如何分析resources.arsc文件的,以及resources.arsc文件的格式java

整體流程

咱們首先執行命令apktool d xxx.apk,而後看輸出以下android

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整體作了以下幾個步驟bash

  • 加載resource table
  • 解碼AndroidManifest.xml
  • 解碼一些資源文件
  • 解碼dex文件
  • copy剩餘文件

今天想和你們討論的只有第一步,關於ApkTool是如何解析resources.arsc的。網絡

如何初始ApkDecoder的成員變量mResTable的,剩下的咱們會下次繼續探討。app

ps:想看大概結果的,直接跳到最後看圖。ide

resources.arsc的格式

resources.arsc是一個二進制文件,想要解析他就必須先弄懂這個文件格式究竟是什麼樣子的。 先上一張來源於網絡的圖片。(圖片來源與網絡,侵,刪)網站

其實總體的就是這個意思了,首先所有的話就是一個resource table,而後依次讀取String Pool,Package Header等。ui

這些格式,具體的都在Android源碼裏面,具體的文件是ResourceTypes.h, 好比:this

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

複製代碼

由於篇幅緣由,因此把註釋部分刪除掉了,具體的你們能夠查閱源碼,也有一個不錯的源碼閱讀網站分享給你們,想看的話能夠不用下載啦,直接在線看就行了。spa

源碼網站地址,

解析流程

咱們首先看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

相關文章
相關標籤/搜索