布景

作为 Androider,咱们平常在 Assets 资源目录下都放点啥呢,字体、预置数据、图片、配置文件…等等,那大家有没有想过,万一哪天我在 Assets 目录下新增了一个子目录放了点自己的资源文件,打包之后再解包发现 Apk 包里没有找到这部分文件,怎么办呢?

原理剖析

咱们都知道典型的 Android 使用构建流程第一步便是 Android 编译器将使用的源代码转换成 DEX 文件(即 Dalvik 可执行文件),而且把其他所有内容转换成为编译后的资源,然后打包器将 DEX 文件和编译后的资源文件组合成 APK。

这里的资源文件就包括 Assets 目录下的文件在内,还有 res 目录下的所有文件和 AndroidManifest.xml 文件,咱们刚刚提到 APK 的资源编译是编译过程中的一项主要工作,AGP3.0.0 之后默许通过 AAPT2 来编译资源。

受到 Android 体系 AAPT 配置的影响,假如走体系默许配置打包,咱们的 Assets 目录兼并过程中会走一些判别逻辑,假如依据体系规矩判定该文件夹是需求被疏忽的,那么也就意味着打不进 Apk 里了。

规矩是这样的:

# Assets 目录兼并疏忽模式
!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~

规矩判别的源码是这样的:

// 疏忽模式核心源码
static bool isHidden(const char *root, const char *path)
{
    if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) {
        return true;
    }
    const char *delim = ":";
    const char *p = gUserIgnoreAssets;
    if (!p || !p[0]) {
        p = getenv("ANDROID_AAPT_IGNORE");
    }
    if (!p || !p[0]) {
        p = gDefaultIgnoreAssets;
    }
    char *patterns = strdup(p);
    bool ignore = false;
    bool chatty = true;
    char *matchedPattern = NULL;
    String8 fullPath(root);
    fullPath.appendPath(path);
    FileType type = getFileType(fullPath);
    int plen = strlen(path);
    // Note: we don't have strtok_r under mingw.
    for(char *token = strtok(patterns, delim);
        !ignore && token != NULL;
        token = strtok(NULL, delim)) {
            chatty = token[0] != '!';
            if (!chatty) token++; // skip !
            if (strncasecmp(token, "<dir>" , 5) == 0) {
                if (type != kFileTypeDirectory) continue;
                token += 5;
            }
            if (strncasecmp(token, "<file>", 6) == 0) {
                if (type != kFileTypeRegular) continue;
                token += 6;
            }
            matchedPattern = token;
            int n = strlen(token);
            if (token[0] == '*') {
                // Match *suffix
                token++;
                n--;
                if (n <= plen) {
                    ignore = strncasecmp(token, path + plen - n, n) == 0;
                }
            } else if (n > 1 && token[n - 1] == '*') {
                // Match prefix*
                ignore = strncasecmp(token, path, n - 1) == 0;
            } else {
                ignore = strcasecmp(token, path) == 0;
            }
        }
    if (ignore && chatty) {
        fprintf(stderr, "    (skipping %s '%s' due to ANDROID_AAPT_IGNORE pattern '%s')\n",
            type == kFileTypeDirectory ? "dir" : "file",
            path,
            matchedPattern ? matchedPattern : "");
    }
    free(patterns);
    return ignore;
}

源码挺长,咱们直接看疏忽模式解读图:

在APK打包过程中,Assets资源漏编译漏打包的本质

举个比如

原理剖析比较长,咱们看一个详细的比如,更好地理解这个判别规矩

随便找一个子 Module,在对应 Assets 目录下增加一个新的子目录,命名为 “__testxxx”,在这个目录下随便放一个测验文件,”test.txt”。

咱们测验打 SNAPSHOP 包然后更新依赖而且打 APK 包,解包今后好像并没有找到这个目录,更没有这个测验文件。

为什么呢?

咱们回过头看下规矩,其中 “:” 是分隔符,<dir>_* 便是今天的重点了,啥意思呢,当判别到 “__testxxx” 是一个目录时,依照规矩会接着判别后一位,刚好后一位是 “_” 下划线,和咱们的文件夹称号开头一致,也就被认为需求被疏忽兼并。

简略来说,便是下划线开头的文件夹目录在打包时将会被认做需求疏忽兼并的文件夹,不予兼并。

解决计划

两种方法,第一种很简略,便是换名字,换个和规矩不匹配的称号即可

当然,有些景象下是没办法换文件夹称号的,不急,还有第二种计划,自定义 AAPT 配置,像这样:

android {
    aaptOptions {
        noCompress ' test1', 'test2'
        ignoreAssetsPattern "!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~"
    }
}

接下来,就看你自己的诉求了,假如是需求打到子 Module 的 SNAPSHOP 中,就把 ignoreAssetsPattern 加到对应 Module 的 android 配置中,假如是需求打到安装包 APK 中,就需求到壳工程的 build.gradle 中去同步一份配置了。

影响面评估

这份配置修正的是打包过程中 Assets 资源兼并的规矩,那么相对地,就会有一些绕开默许规矩且契合自定义规矩的资源被兼并进 APK 包中了。简略地说:

  • 关于加了配置的 Module,会有一些 <dir>_* 规矩以外的文件会被兼并进去
  • 关于没有加配置的 Module,也就没有影响

大家能够针对 app/build/intermediates/incremental/mergeDebugAssets/merger.xml XML 文件做加配置前后的 DIFF 对比,在兼并之前检查是否有不应兼并的文件被兼并进去。

影响完全可控,而且也达到了咱们的初心,让合理资源被正常打包,不合理的资源被疏忽,彻底解决漏编译漏打。