1. 程式人生 > >Android: 在native中訪問assets全解析

Android: 在native中訪問assets全解析

lock mp4 cpp sets 這樣的 內容 jniexport opencl href

本文總結在Android Native C++開發中訪問APK中的assets資源的方法

在CMake中添加相關NDK LIB的 依賴

因為我們接下來用到的一些函數實現在NDK庫libandroid.so中,因此我們直接在CMakeList.txt中添加對其依賴即可:

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

如果沒有添加此依賴,顯然會提示undefined reference錯誤,比如:

error: undefined reference to ‘AAssetManager_fromJava‘
error: undefined reference to ‘AAssetManager_open‘
error: undefined reference to ‘AAsset_getLength‘
error: undefined reference to ‘AAsset_getBuffer‘
error: undefined reference to ‘AAsset_close‘
error: undefined reference to ‘AAssetManager_open‘
error: undefined reference to ‘AAsset_getLength‘
error: undefined reference to ‘AAsset_openFileDescriptor‘
error: undefined reference to ‘AAsset_close‘
error: undefined reference to ‘AAssetManager_openDir‘
error: undefined reference to ‘AAssetDir_getNextFileName‘
error: undefined reference to ‘AAssetManager_open‘

獲得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
}

下面所有的代碼都是在Java_willhua_androidstudioopencl_MainActivity_setNativeAssetManager內實現。

訪問assets下的文件

我們知道,assets文件夾下面是可以有子文件夾的,因為,下面我以讀取圖片為例,介紹各種情況的訪問方法。例子中用到OpenCV的相關方法,在此不介紹,自行了解。
測試用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);

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

//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]);

std::vector<char> vec2(dataBuffer, dataBuffer + fileLength);
cv::Mat mat2 = cv::imdecode(vec2, CV_LOAD_IMAGE_COLOR);
LOGD("use fd  mat:%d x %d  %d", mat2.cols, mat2.rows, mat2.channels());
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來訪問了。

參考

Developers NDK
不壓縮文件後綴
SOF

Android: 在native中訪問assets全解析