为什么AssetManager.list()如此缓慢?

7
我正在尝试将SD卡和APK中存储的文件混合填充到ListView中。使用TraceView,我可以看到与SD卡使用文件名过滤器相比,AssetManager.list()的性能较差。以下是一个简单的方法,它从SD卡上的文件夹中返回所有的png文件:
// The folder on SDcard may contain files other than png, so filter them out
private File[] getMatchingFiles(File path) {
File[] flFiles = path.listFiles(new FilenameFilter() {
    public boolean accept(File dir, String name) {
    name = name.toLowerCase();
    return name.endsWith(".png");
    }
});  
return flFiles;
}

我在这里调用了该方法,大约需要12毫秒来检索16个文件:

final String state = Environment.getExternalStorageState();           
if (Environment.MEDIA_MOUNTED.equals(state)||Environment.MEDIA_SHARED.equals(state)) {
    File path = Environment.getExternalStoragePublicDirectory(getResources().getString(R.string.path_dir));
if (path.exists()){
    File[] files = getMatchingFiles(path); 
        ... 

相比之下,am.list 方法花费了 49 毫秒的时间才能获取大约 6 个文件的名称!

// Get all filenames from specific Asset Folder and store them in String array
AssetManager am = getAssets();
String path = getResources().getString(R.string.path_dir);
String[] fileNames = am.list(path);  
...

有人能解释一下为什么性能会这么糟吗?性能是否与存储在 APK 中的资产数量成比例?我知道资产被压缩了,但我只是获取资产的名称,我认为这些名称应该存储在某个表中。


你应该非常清楚你的资产文件夹里有什么。 - njzk2
1
AssetManager很糟糕,性能很差。我在子文件夹中有大约7.5K个资产文件。它们在资产中是因为它们来自外部来源,将它们放入资源的维护开销(重命名文件等)和性能损失是不可接受的。为了在这个结构中找到文件,我必须递归搜索它,而性能非常糟糕,正如你所说,主要是围绕.list()。看起来设计师从未考虑过使用大量静态数据的应用程序。我会密切关注这个问题。 - Simon
4个回答

4
Coverdriven的评论"存储在某个表中"启发了我解决自己一直搁置已久的问题。这并不能回答OP的问题,但提供了不同的方法,并处理子文件夹,而CommonsWare的解决方案则无法处理子文件夹,除非你递归(当然,这是另一种可能的解决方案)。它专门针对在子文件夹中有大量资产的应用程序。我添加了一个ANT预构建目标来运行此命令(我在Windows上)。
dir assets /b /s /A-d > res\raw\assetfiles

这将创建一个递归(/s)的、基础的(/b)文件列表,其中排除了我的资产文件夹中的目录项(/A-d)。
然后我创建了这个类来静态地加载assetfiles的内容到一个哈希映射中,其中键是文件名,值是完整路径。
public class AssetFiles {

// create a hashmap of all files referenced in res/raw/assetfiles

/*map of all the contents of assets located in the subfolder with the name specified in FILES_ROOT
the key is the filename without path, the value is the full path relative to FILES_ROOT
includes the root, e.g. harmonics_data/subfolder/file.extension - this can be passed
directly to AssetManager.open()*/
public static HashMap<String, String> assetFiles = new HashMap<String, String>();
public static final String FILES_ROOT = "harmonics_data";

static {

    String line;
    String filename;
    String path;

    try {

        BufferedReader reader = new BufferedReader(new InputStreamReader(TidesPlannerApplication.getContext().getResources().openRawResource(R.raw.assetfiles)));

        while ((line = reader.readLine()) != null) {
            // NB backlash (note the escape) is specific to Windows
            filename = line.substring(line.lastIndexOf("\\")+1);
            path = line.substring(line.lastIndexOf(FILES_ROOT)).replaceAll("\\\\","/");;
            assetFiles.put(filename, path);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }

}

public static boolean exists(String filename){
    return assetFiles.containsKey(filename);
}

public static String getFilename(String filename){
    if (exists(filename)){
        return assetFiles.get(filename);
    } else {
        return "";
    }

}

要使用它,只需调用AssetFiles.getFilename(filename),它会返回完整的路径,然后可以将其传递给AssetManager.open()。速度快得多!

NB. 我还没有完成这个类,它还没有经过加固,所以您需要添加适当的异常捕获和操作。它也非常特定于我的应用程序,因为我的所有资产都在子文件夹中,这些子文件夹又位于资产文件夹的子文件夹中(请参见FILES_ROOT),但很容易适应您的情况。

还要注意替换反斜杠(因为Windows生成assetfiles列表)为正斜杠。您可以在OSX和*nix平台上消除这种情况。


2
有人能解释一下为什么性能会这么差吗?
读取ZIP存档(包含资产的APK)的内容比读取文件系统上目录的内容要慢,这在抽象层面上并不特别令人惊讶,因为我认为这对所有主要操作系统来说都是正确的。
list()数据读入一次,然后保存到其他地方以供快速访问(例如数据库),特别是以未来查找为优化的形式(例如,一个简单的数据库查询可以给你想要的结果,而不必再次加载和“递归搜索”)。

谢谢,这是一个周到的回答,但如果缓存列表如此有用,为什么系统不已经这样做了呢?资源在打包成apk时已经被枚举,为什么不能同时存储资产列表?当然,我可以像CommonsWare建议的那样,在启动时读取它一次,但将其缓存在AssetManager中是否更有意义? - coverdriven
2
@coverdriven: "为什么系统不自动处理呢?" -- 可能是因为具有您使用情况的开发人员数量相对较少。据我所知,谷歌并没有无限的工程时间,而安卓设备也没有无限的存储空间和内存。因此,并非所有开发人员可以自行解决的问题都会在操作系统层面得到解决。 - CommonsWare

1
你可以将APK包视为ZIP文件,并使用Java的内置ZipFile读取所有条目。它将提供所有文件名及其完整路径。也许很容易找到您拥有哪些目录。到目前为止,这是我测试过的最快方法。功劳归于@obastemur在jxcore-android-basics示例项目上的提交。

免责声明:自我推广。我创建了一个Gradle插件,专门用于添加编译时安全性,这还具有自动生成每个文件夹列表的额外好处。请访问github.com/oriley-me/crate查看。 - Kane O'Riley

1
如果您在资产中有深层的目录树,您可以首先检测项目是文件还是目录,然后在其上调用.list()(真正加速了遍历目录树)。这是我发现的解决方案:
try {
    AssetFileDescriptor desc = getAssets().openFd(path);  // Always throws exception: for directories and for files
    desc.close();  // Never executes
} catch (Exception e) {
    exception_message = e.toString();
}

if (exception_message.endsWith(path)) {  // Exception for directory and for file has different message
    // Directory
} else {
    // File
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接