android: Native 層訪問assets目錄


有時候需要把一些文件放置到app里面並且打包出去(比如OpenCV里的模型文件、一些試聽的歌曲等),android下面一般都是放在 assets 目錄或者是 raw 目錄,后者還可以通過R文件訪問,前者則不能。從java層訪問這兩個目錄很簡單,從native層訪問卻很少用到,剛好項目中有用到,特記錄之:

一. 在CMake中添加相關的依賴

這里主要是添加native層用到的一些庫函數,包含在libandroid.so中,因此我們直接在 CMakeList.txt 中添加對其依賴即可。

target_link_libraries( # Specifies the target library.
                       native-lib
                       #lib to link
                       android
                       # other libs
                       )

二. 獲得 AssetManager

在Java中,我們可以通過 Context.getAssets()  輕松獲得 AssetManager 。在NDK中,提供了 AAssetManager_fromJava 函數來獲得Native中對應的 AAssetManager 。顧名思義,fromJava,肯定是要從Java層獲取了,也即意味着要通過JNI來獲得。代碼如下:

/***code in Java, such as MainActivity.java***/

//decale the jni func
public native void setNativeAssetManager(AssetManager assetManager);

//call it, such as during Activity.onCreate()
setNativeAssetManager(getAssets());

/***end of java***/


/***code in native c++***/
extern "C"
JNIEXPORT void JNICALL
Java_willhua_androidstudioopencl_MainActivity_setNativeAssetManager(
    JNIEnv *env, 
    jobject instance,
    jobject assetManager) {
            AAssetManager *nativeasset = AAssetManager_fromJava(env, assetManager);
            
            //the use of nativeasset
}

在Native層的C++文件中使用  AAssetManager_fromJava 這個函數需要注意,先包含下面兩個頭文件:

#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

不然會提示找不到這個函數。

三. 訪問assets下的文件

我們知道,assets文件夾下面是可以有子文件夾的,因為,下面我以讀取圖片為例,介紹各種情況的訪問方法。
測試用assets文件夾目錄:

已知完整路徑的訪問

如果我們已經知道assets下某個文件的完整路徑,比如"sz.jpg","dir/cs.jpg",那么我們可以直接把這個路徑傳給 AAssetManager_open 來獲得AAsset.:

//open file
AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER);
//this will also be ok 
//AAsset *assetFile = AAssetManager_open(nativeasset, "dir/cs.jpg", AASSET_MODE_BUFFER);
//get file length
size_t fileLength = AAsset_getLength(assetFile);
char *dataBuffer2 = (char *) malloc(fileLength);
//read file data
AAsset_read(assetFile, dataBuffer2, fileLength);
//the data has been copied to dataBuffer2, so , close it
AAsset_close(assetFile);

//free malloc
free(dataBuffer2);

獲取文件下的名字並訪問之:

如果我們只知道文件夾的名字,但並不知道文件夾下面有哪些具體文件,比如我們只知道有個dir文件夾,但不知道下面的具體情況。那么我們可以使用 AAssetDir_getNextFileName 來獲取文件夾的文件名。但是有個問題,這個方法只能獲得文件夾下的文件名,而無法獲得子文件夾,有哪位知道的請告知。


注意:AAssetDir_getNextFileName只返回文件名,而不是該文件的完整路徑,比如只會返回cs.jpg,而不是dir/cs.jpg,所以如果直接把AAssetDir_getNextFileName的返回結果傳給AAssetManager_open會讀取不到正確的文件,返回NULL。

AAssetDir *assetDir = AAssetManager_openDir(nativeasset, "dir");
const char *filename = AAssetDir_getNextFileName(assetDir);
while (filename != NULL){
    char fullname[1024];
    sprintf(fullname, "dir/%s", filename); //get the full path
    AAsset *file = AAssetManager_open(nativeasset, fullname, AASSET_MODE_BUFFER);
    if(file == NULL){
        LOGD("FILE NULL  %s", filename);
        break;
    }
    size_t fileLength = AAsset_getLength(file);
    LOGD("filename next:%s,  size:%d", filename, fileLength);
    char *buffer = (char*)malloc(fileLength);
    AAsset_read(file, buffer, fileLength);
    AAsset_close(file);

    //do something with the buffer


    free(buffer);

    filename = AAssetDir_getNextFileName(assetDir);
}
AAssetDir_close(assetDir);  //remember to close it

使用AAsset_getBuffer讀整個文件內容

在上面的case中,我們拿到AAsset之后都是malloc內存,然后把文件信息讀出來的形式,其實這種方式適合不一次性讀取整個文件內容的情況,按照官網的說法就是:

Attempt to read 'count' bytes of data from the current offset.

也就是 AAsset_read 應該配合 AAsset_seek 使用更美味。
對於一次性讀取整個文件的內容,更好的方式是使用 AAsset_getBuffer ,讀取小文件時推薦使用此方法。

AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(assetFile);
const char *dataBuffer2 =(const char *) AAsset_getBuffer(assetFile);

std::vector<char> vec2(dataBuffer2, dataBuffer2 + fileLength);
cv::Mat mat2 = cv::imdecode(vec2, CV_LOAD_IMAGE_COLOR);
LOGD("asset file lenght:%d   mat: %d x %d  %d", fileLength,  mat2.cols, mat2.rows, mat2.channels());

AAsset_close(assetFile);

以FileDescriptor的方式來讀取

我們可以使用 AAsset_openFileDescriptor 來獲取 FileDescriptor ,然后再進行其他操作。需要注意的是,AAsset_openFileDescriptor返回當前fd的起始seek位置start以及文件長度length。在讀取內容之前記得要先seek到start,否則會發現文件內容不對。見代碼中的 lseek .

AAsset *assetFile = AAssetManager_open(nativeasset, "sz.jpg", AASSET_MODE_BUFFER);
size_t fileLength = AAsset_getLength(assetFile);
LOGD("before fd fileLength:%d",fileLength);

off_t start = 0, length = 0;
int fd = AAsset_openFileDescriptor(assetFile, &start, &length);
LOGD("fd:%d  start:%d  length:%d", fd, start, length);
lseek(fd, start, SEEK_CUR); //NOTICE 

char *dataBuffer = (char*)malloc(fileLength);
memset(dataBuffer, 0, fileLength);
read(fd, dataBuffer, fileLength);
close(fd);  //close fd
LOGD("read_  %d %d %d",  dataBuffer[0], dataBuffer[1], dataBuffer[2]);

AAsset_close(assetFile);

獲得fd之后,也可以通過他來獲得一個FILE:FILE * file = fdopen(fd, "rb");但是一定要記得fclose(file)。總的來說不如read方便。

open mode

 AAssetManager_open 需要傳入一個mode參數,各參數的含義如下,按需使用。

AASSET_MODE_UNKNOWN: Not known how the data is to be accessed
AASSET_MODE_RANDOM: Read chunks, and seek forward and backward
AASSET_MODE_STREAMING: Read sequentially, with an occasional
forward seek
AASSET_MODE_BUFFER: Attempt to load contents into memory, for fast
small reads

細節提示

AAsset是只讀的,比如上面獲得FILE之后,不能用來寫。

AAsset provides access to a read-only asset.

另外需要記得在用完后:

AAsset_close
AAssetDir_close

關於壓縮文件

Android APK中有些文件是會進行壓縮的,而有些文件則因為本身就是已經壓縮過的,不再進行壓縮,具體有:

/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
    ".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"
};

那么對於在APK中會被壓縮的文件,比如txt文件,就不能使用AAsset_openFileDescriptor來讀了,否則,會返回-1這樣的無效fd。對於會被壓縮的文件,那么就只能使用AAsset_read或者AAsset_getBuffer來訪問了。

參考鏈接:

1. Android: 在native中訪問assets全解析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM