Android C++系列:访问Assets 文件夹.md

1,688 阅读4分钟

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

Java 层Assets

assets目录是Android的一种特殊目录,用于放置APP所需的固定文件,且该文件被打包到APK中时,不会被编码到二进制文件。 Android还存在一种放置在res下的raw目录,该目录与assets目录不同。 区别点:

  1. assets目录不会被映射到R中,因此,资源无法通过R.id方式获取,必须要通过AssetManager进行操作与获取;res/raw目录下的资源会被映射到R中,可以通过getResource()方法获取资源。
  2. 多级目录:assets下可以有多级目录,res/raw下不可以有多级目录。
  3. 编码(都不会被编码):assets目录下资源不会被二进制编码;res/raw应该也不会被编码。

Java层我们通过Context获取AssetsManager,然后调用AssetsManager的open方法获取assets下文件的输入流:

AssetManager am = getAssets();  
InputStream is = am.open("filename");  

JNI 层Assets

很多时候我们需要在JNI中直接操作Assets下的文件,比如我们要使用Assets下的图片资源作为Opengl贴图,或者使用Assets下的tflite模型文件加载模型。

加载纹理图片

在 OpenGL 开发中,我们要渲染一张图片,通常先是得到一张图片对应的 Bitmap ,然后将该 Bitmap 作为纹理上传到 OpenGL 中。在 Android 中有封装好的 GLUtils 类的 texImage2D 方法供我们调用。

public static void texImage2D(int target, int level, int internalformat,
             Bitmap bitmap, int type, int border)

该方法的底层原理实际上也是解析了该 Bitmap ,得到了 Bitmap 所有的像素数据,类似于 Android NDK 关于 Bitmap 操作的 AndroidBitmap_lockPixels 方法,如果你不太了解该方法,可以参考《Android C++系列:JNI 操作Bitmap》。

得到了所有像素数据之后,实际最终还是调用了 OpenGL 的 glTexImage2D 来实现纹理上传。当然,如果可以直接得到所有数据,也不需要走解析 Bitmap 这一步了,这种场景最常见的就是把相机作为输入了。

接下来我们会通过 Android NDK 开发中去渲染一张图片,步骤还是如上,从图像解析到纹理上传,不同的是我们将会解析 Assets 文件夹中的图片,而不是一张已经保存在手机 SDCard 上的图片。

相比于前者,SDCard 上的图片已经有了绝对地址了,直接把地址传到 stb_image 库就可以完成解析了(参考之前的文章 ),而 Assets 文件夹的内容在手机上可没有绝对地址。

一开始陷入了误区,想着怎么去获得文件的绝对地址,看到了 AssetManager 的 AAsset_openFileDescriptor 方法以为拿到文件描述符就万事大吉了,结果打印的地址是这样的 /proc/9941/fd/79 ,这基本的不可用了。

换个思路,在 Java 中去加载 Assets 目录下的图片:

 InputStream is = getAssets().open(fileName); 

通过 AssertManager 的 open 方法直接拿到文件的输入流了。

而在 NDK 开发中同样的方式是行不通的,这里要采用另外一种方式,但其实意思都差不多的:

 // NDK 中是 AssetManager
     AAssetManager *mgr = AAssetManager_fromJava(env, assetManager);
     // 打开 Asset 文件夹下的文件
     AAsset *pathAsset = AAssetManager_open(mgr, assetPath, AASSET_MODE_UNKNOWN);
     // 得到文件的长度
     off_t assetLength = AAsset_getLength(pathAsset);
     // 得到文件对应的 Buffer
     unsigned char *fileData = (unsigned char *) AAsset_getBuffer(pathAsset);
     // stb_image 的方法,从内存中加载图片
     unsigned char *contnet = stbi_load_from_memory(fileData, assetLength, &w, &h, &n, 0);

NDK 中可拿不到像 Java 那样的输入流,但是可以通过 AssetManager 的 AAsset_getBuffer 或者是 AAsset_read 方法去获取文件内容。

看到上面那两个 API 基本就稳了,再配合 stb_image 介绍过的方法,stbi_load_from_memory 从内存中加载图片的像素数据,最后就是 glTexImage2D 方法实现纹理上传。

Assets方法类封装

#include <string>
#include <vector>
#include <android/asset_manager.h>
#include "util_asset.h"

//#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>



bool
asset_read_file (AAssetManager *assetMgr, char *fname, std::vector<uint8_t>&buf) 
{
    AAsset* assetDescriptor = AAssetManager_open(assetMgr, fname, AASSET_MODE_BUFFER);
    if (assetDescriptor == NULL)
    {
        return false;
    }

    size_t fileLength = AAsset_getLength(assetDescriptor);

    buf.resize(fileLength);
    int64_t readSize = AAsset_read(assetDescriptor, buf.data(), buf.size());

    AAsset_close(assetDescriptor);

    return (readSize == buf.size());
}

uint8_t *
asset_read_image (AAssetManager *assetMgr, char *fname, int32_t *img_w, int32_t *img_h)
{
    int32_t  width, height, channel_count;
    uint8_t* img_buf;
    bool     ret;

    /* read asset file */
    std::vector<uint8_t> read_buf;
    ret = asset_read_file (assetMgr, fname, read_buf);
    if (ret != true)
        return nullptr;

    /* decode image data to RGBA8888 */
    img_buf = stbi_load_from_memory (read_buf.data(), read_buf.size(),
            &width, &height, &channel_count, 4);

    *img_w = width;
    *img_h = height;

    return img_buf;
}

void
asset_free_image (uint8_t *image_buf)
{
    stbi_image_free (image_buf);
}

总结

今天我们介绍了Android Assets文件夹使用,包括Java层和JNI层,并详细介绍了JNI层AAssetManager接口的使用。