背景
作为 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;
}
源码挺长,我们直接看忽略模式解读图:
举个例子
原理分析比较长,我们看一个详细的例子,更好地理解这个判断规则
随便找一个子 Module,在对应 Assets 目录下增加一个新的子目录,命名为 “__testxxx”,在这个目录下随便放一个测试文件,“test.txt”。
我们尝试打 SNAPSHOP 包然后更新依赖并且打 APK 包,解包以后好像并没有找到这个目录,更没有这个测试文件。
为什么呢?
我们回过头看下规则,其中 “:” 是分隔符,
* 就是今天的重点了,啥意思呢,当判断到 “__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,会有一些 _* 规则以外的文件会被合并进去
- 对于没有加配置的 Module,也就没有影响
大家可以针对 app/build/intermediates/incremental/mergeDebugAssets/merger.xml
XML 文件做加配置前后的 DIFF 对比,在合并之前检查是否有不该合并的文件被合并进去。
影响完全可控,并且也达到了我们的初心,让合理资源被正常打包,不合理的资源被忽略,彻底解决漏编译漏打。
作者:闫宇威