从源码看生成逻辑 — apktool.yml 每个节点的作用
本文最后更新于 1017 天前,其中的信息可能已经有所发展或是发生改变。

该篇文章基于 apktool 源码版本 2.6.1

1. apktool.yml 是什么

是 apktool 这个开源工具反编译 apk 生成的一个 配置文件,用来记录这个 apk 的基础信息

源码对应的bean文件是 brut.androlib.meta.MetaInfo.java;

源码里对应的 apktool.yml的生成逻辑是在

brut.androlib.ApkDecoder.java ,ApkDecoder 中的 writeMetaFile 逻辑我复制在了下面,文章会逐个对每个 key的生成原理 去分析

 private void writeMetaFile() throws AndrolibException {
        MetaInfo meta = new MetaInfo();
        meta.version = Androlib.getVersion();
        meta.apkFileName = mApkFile.getName();

        if (mResTable != null) {
            meta.isFrameworkApk = mAndrolib.isFrameworkApk(mResTable);
            putUsesFramework(meta);
            putSdkInfo(meta);
            putPackageInfo(meta);
            putVersionInfo(meta);
            putSharedLibraryInfo(meta);
            putSparseResourcesInfo(meta);
        } else {
            putMinSdkInfo(meta);
        }
        putUnknownInfo(meta);
        putFileCompressionInfo(meta);

        mAndrolib.writeMetaFile(mOutDir, meta);
    }

2. apktool.yml 节点分析

!!brut.androlib.meta.MetaInfo
apkFileName: filename.apk
compressionType: false
doNotCompress:
- arsc
- png
isFrameworkApk: false
packageInfo:
  forcedPackageId: '127'
  renameManifestPackage: null
sdkInfo:
  minSdkVersion: '19'
  targetSdkVersion: '26'
sharedLibrary: false
sparseResources: false
unknownFiles:
  sentry-build.properties: '0'
usesFramework:
  ids:
  - 1
  tag: null
version: 2.4.1-2264e6-SNAPSHOT
versionInfo:
  versionCode: '20220314'
  versionName: 1.14.2

1. apkFileName

反编译的文件名

生成逻辑对应 第一段落源码中的

meta.apkFileName = mApkFile.getName();

具体逻辑不用分析,比较简单,就是反编译 目标apk 的文件名

2. isFrameworkApk

判断是否是反编译 apktool 工具 的 framework apk

    public boolean isFrameworkApk(ResTable resTable) {
        for (ResPackage pkg : resTable.listMainPackages()) {
            if (pkg.getId() < 64) {
                return true;
            }
        }
        return false;
    }

入参 ResTable 是 通过 getResTable生成的

public ResTable getResTable() throws AndrolibException {
        if (mResTable == null) {
            boolean hasResources = hasResources();
            boolean hasManifest = hasManifest();
            if (! (hasManifest || hasResources)) {
                throw new AndrolibException(
                        "Apk doesn't contain either AndroidManifest.xml file or resources.arsc file");
            }
            mResTable = mAndrolib.getResTable(mApkFile, hasResources);
            mResTable.setAnalysisMode(mAnalysisMode);
        }
        return mResTable;
    }

一路往下走 在 brut.androlib.res.AndrolibResources 调用 getResTable , new 了一个 restable 然后返回回去, 这个 restable 就是 apk 读取 AndroidManifest.xml 和 res 内容后的 一个实体类


    public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
            throws AndrolibException {
        ResTable resTable = new ResTable(this);
        if (loadMainPkg) {
            loadMainPkg(resTable, apkFile);
        }
        return resTable;
    }


在 loadMainPkg 函数中传给 让 resTable 读取到对应的属性,在 brut.androlib.res.decoder.ARSCDecoder

给 resTable 赋予读取 apk 信息后的属性,在 readTablePackage 中给 mResId 这个属性赋值是 id 的

mResId = id << 24; 代码如下

 private ResPackage readTablePackage() throws IOException, AndrolibException {
        checkChunkType(Header.TYPE_PACKAGE);
        int id = mIn.readInt();

        if (id == 0) {
            // This means we are dealing with a Library Package, we should just temporarily
            // set the packageId to the next available id . This will be set at runtime regardless, but
            // for Apktool's use we need a non-zero packageId.
            // AOSP indicates 0x02 is next, as 0x01 is system and 0x7F is private.
            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();
        boolean flag = true;
        while (flag) {
            switch (mHeader.type) {
                case Header.TYPE_LIBRARY:
                    readLibraryType();
                    break;
                case Header.TYPE_SPEC_TYPE:
                    readTableTypeSpec();
                    break;
                default:
                    flag = false;
                    break;
            }
        }

        return mPkg;
    }

3. usesFramework

不太清楚这个标记是用来做什么,会把 包的 id 按顺序输出出来,然后读取 buildOptions 里面的 frameworkTag

    private void putUsesFramework(MetaInfo meta) {
        Set<ResPackage> pkgs = mResTable.listFramePackages();
        if (pkgs.isEmpty()) {
            return;
        }

        Integer[] ids = new Integer[pkgs.size()];
        int i = 0;
        for (ResPackage pkg : pkgs) {
            ids[i++] = pkg.getId();
        }
        Arrays.sort(ids);

        meta.usesFramework = new UsesFramework();
        meta.usesFramework.ids = Arrays.asList(ids);

        if (mAndrolib.buildOptions.frameworkTag != null) {
            meta.usesFramework.tag = mAndrolib.buildOptions.frameworkTag;
        }
    }

4. sdkInfo

应用的 sdk 版本信息

    private void putSdkInfo(MetaInfo meta) {
        Map<String, String> info = mResTable.getSdkInfo();
        if (info.size() > 0) {
            String refValue;
            if (info.get("minSdkVersion") != null) {
                refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("minSdkVersion"));
                if (refValue != null) {
                    info.put("minSdkVersion", refValue);
                }
            }
            if (info.get("targetSdkVersion") != null) {
                refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("targetSdkVersion"));
                if (refValue != null) {
                    info.put("targetSdkVersion", refValue);
                }
            }
            if (info.get("maxSdkVersion") != null) {
                refValue = ResXmlPatcher.pullValueFromIntegers(mOutDir, info.get("maxSdkVersion"));
                if (refValue != null) {
                    info.put("maxSdkVersion", refValue);
                }
            }
            meta.sdkInfo = info;
        }
    }

靠工具类 brut.androlib.res.xml.ResXmlPatcher 来完成 对 sdk 版本的读取,只不过读取路径是在

/res/values/integers.xml ,有点费解,因为实际看文件内容是没有 sdkverison 相关的内容,后续还需要单步调试看看获取原理

 /**
     * Finds key in integers.xml file and returns text value
     *
     * @param directory Root directory of apk
     * @param key Integer reference (ie @integer/foo)
     * @return String|null
     */
    public static String pullValueFromIntegers(File directory, String key) {
        if (key == null || ! key.contains("@")) {
            return null;
        }

        File file = new File(directory, "/res/values/integers.xml");
        key = key.replace("@integer/", "");

        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                XPath xPath = XPathFactory.newInstance().newXPath();
                XPathExpression expression = xPath.compile("/resources/integer[@name=" + '"' + key + "\"]/text()");

                Object result = expression.evaluate(doc, XPathConstants.STRING);

                if (result != null) {
                    return (String) result;
                }

            }  catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) {
            }
        }

        return null;
    }

5. unknownFiles

下面的代码是 apktool 对unknow 文件的识别逻辑,读取 zip 解压后的所有不在 APK_STANDARD_ALL_FILENAMES 文件列表里的文件 ,并且也不是dex 文件,那么就归属于 unknow 文件,但是 unknow文件 可不是 可有可无的,只是 apktool 对这些文件不知道如何处理,但是如果你删掉了,那么回编译代码肯定会有问题

    public void decodeUnknownFiles(ExtFile apkFile, File outDir)
            throws AndrolibException {
        LOGGER.info("Copying unknown files...");
        File unknownOut = new File(outDir, UNK_DIRNAME);
        try {
            Directory unk = apkFile.getDirectory();

            // loop all items in container recursively, ignoring any that are pre-defined by aapt
            Set<String> files = unk.getFiles(true);
            for (String file : files) {
                if (!isAPKFileNames(file) && !file.endsWith(".dex")) {

                    // copy file out of archive into special "unknown" folder
                    unk.copyToDir(unknownOut, file);
                    // lets record the name of the file, and its compression type
                    // so that we may re-include it the same way
                    mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
                }
            }
        } catch (DirectoryException ex) {
            throw new AndrolibException(ex);
        }
    }    public void decodeUnknownFiles(ExtFile apkFile, File outDir)
            throws AndrolibException {
        LOGGER.info("Copying unknown files...");
        File unknownOut = new File(outDir, UNK_DIRNAME);
        try {
            Directory unk = apkFile.getDirectory();

            // loop all items in container recursively, ignoring any that are pre-defined by aapt
            Set<String> files = unk.getFiles(true);
            for (String file : files) {
                if (!isAPKFileNames(file) && !file.endsWith(".dex")) {

                    // copy file out of archive into special "unknown" folder
                    unk.copyToDir(unknownOut, file);
                    // lets record the name of the file, and its compression type
                    // so that we may re-include it the same way
                    mResUnknownFiles.addUnknownFileInfo(file, String.valueOf(unk.getCompressionLevel(file)));
                }
            }
        } catch (DirectoryException ex) {
            throw new AndrolibException(ex);
        }
    }

private final static String[] APK_STANDARD_ALL_FILENAMES = new String[] {
        "classes.dex", "AndroidManifest.xml", "resources.arsc", "res", "r", "R",
        "lib", "libs", "assets", "META-INF", "kotlin" };

6. doNotCompress

apktool 源码中对这个节点的定义是列出 不需要压缩的文件

下面是 apktool 对 必须要压缩的文件的定义

    private final static Pattern NO_COMPRESS_PATTERN = Pattern.compile("(" +
            "jpg|jpeg|png|gif|wav|mp2|mp3|ogg|aac|mpg|mpeg|mid|midi|smf|jet|rtttl|imy|xmf|mp4|" +
            "m4a|m4v|3gp|3gpp|3g2|3gpp2|amr|awb|wma|wmv|webm|webp|mkv)$");

整体判断文件是否不需要压缩,就是读取文件后缀,不匹配必须压缩的文件后缀,那就加入不压缩的行列

  public void recordUncompressedFiles(ExtFile apkFile, Collection<String> uncompressedFilesOrExts) throws AndrolibException {
        try {
            Directory unk = apkFile.getDirectory();
            Set<String> files = unk.getFiles(true);

            for (String file : files) {
                if (isAPKFileNames(file) && unk.getCompressionLevel(file) == 0) {
                    String ext = "";
                    if (unk.getSize(file) != 0) {
                        ext = FilenameUtils.getExtension(file);
                    }

                    if (ext.isEmpty() || !NO_COMPRESS_PATTERN.matcher(ext).find()) {
                        ext = file;
                    }
                    if (!uncompressedFilesOrExts.contains(ext)) {
                        uncompressedFilesOrExts.add(ext);
                    }
                }
            }
        } catch (DirectoryException ex) {
            throw new AndrolibException(ex);
        }
    }

整个 apktool 反编译生成的 apktool.yml 的逻辑我已经讲了比较重要的几个 key ,还有 诸如 version

verisonInfo ,这些就可以从我开头写的源码入口自己去看看对应的生成逻辑,相信会受益匪浅

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇