Android: 在native中訪問assets全解析
本文總結在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全解析