我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件
Posted 字节卷动
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件相关的知识,希望对你有一定的参考价值。
一、fopen简介
在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。
打开文件之后,程序可以得到文件的相关信息,例如大小、类型、权限、创建者、更新时间等。在后续读写文件的过程中,程序还可以记录当前读写到了哪个位置,下次可以在此基础上继续操作。
标准输入文件 stdin(表示键盘)、标准输出文件 stdout(表示显示器)、标准错误文件 stderr(表示显示器)是由系统打开的,可直接使用。
1.1 fopen函数
使用 <stdio.h> 头文件中的 fopen() 函数即可打开文件,它的用法为:
FILE *fopen(char *filename, char *mode);
filename
为文件名(包括文件路径),mode
为打开方式,它们都是字符串。
1.2 fopen() 函数的返回值
fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。
FILE
是 <stdio.h>
头文件中的一个结构体,它专门用来保存文件信息。我们不用关心 FILE 的具体结构,只需要知道它的用法就行。
如果希望接收 fopen()
的返回值,就需要定义一个 FILE
类型的指针。例如:
FILE *fp = fopen("demo.txt", "r");
表示以“只读
”方式打开当前目录下的 demo.txt
文件,并使 fp
指向该文件,这样就可以通过 fp
来操作 demo.txt
了。fp
通常被称为文件指针
。
再来看一个例子:
FILE *fp = fopen("D:\\\\\\\\demo.txt","rb+");
表示以二进制方式打开 D
盘下的 demo.txt
文件,允许读和写
。
1.2.1 判断文件是否打开成功
打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,请看下面的代码:
FILE *fp;
if( (fp=fopen("D:\\\\demo.txt","rb")) == NULL )
printf("Fail to open file!\\n");
exit(0); //退出程序(结束程序)
我们通过判断 fopen()
的返回值是否和 NULL
相等来判断是否打开失败:
如果 fopen()
的返回值为 NULL
,那么 fp
的值也为 NULL
,此时 if
的判断条件成立,表示文件打开失败。
以上代码是文件操作的规范写法,读者在打开文件时一定要判断文件是否打开成功,因为一旦打开失败,后续操作就都没法进行了,往往以“结束程序”告终。
1.3 fopen() 函数的打开方式
不同的操作需要不同的文件权限。例如,只想读取文件中的数据的话,“只读”权限就够了;既想读取又想写入数据的话,“读写”权限就是必须的了。
另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。
在调用 fopen() 函数时,这些信息都必须提供,称为“文件打开方式”。最基本的文件打开方式有以下几种:
- 控制读写权限的字符串(必须指明)
打开方式 | 说明 |
---|---|
“r” | 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。 |
“w” | 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
“a” | 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
“r+” | 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。 |
“w+” | 以“写入/更新”方式打开文件,相当于w和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。 |
“a+” | 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。 |
- 控制读写方式的字符串(可以不写)
打开方式 | 说明 |
---|---|
“t” | 文本文件。如果不写,默认为"t"。 |
“b” | 二进制文件。 |
调用 fopen() 函数时必须指明读写权限,但是可以不指明读写方式(此时默认为"t"
)。
读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。例如:
- 将读写方式放在读写权限的末尾:“rb”、“wt”、“ab”、“r+b”、“w+t”、“a+t”
- 将读写方式放在读写权限的中间:“rb+”、“wt+”、“ab+”
整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:
- r(read):读
- w(write):写
- a(append):追加
- t(text):文本文件
- b(binary):二进制文件
- +:读和写
1.4 关闭文件
文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。fclose() 的用法为:
int fclose(FILE *fp);
fclose(fp)
函数关闭fp
指定的文件,必要时刷新缓冲区。
对于较正式的程序,应该检查是否成功关闭。
如果成功关闭,fclose()
返回值为0
,否则返回EOF
。
if(fclose(fp) != 0)
printf("Error in closing file %s \\n", argv[1]);
如果磁盘已满、移动硬盘被移除或者出现I/O错误,都会导致fclose()函数失败。
1.5 实例演示
最后,我们通过一段完整的代码来演示 fopen 函数的用法,这个例子会一行一行地读取文本文件的所有内容:
#include <stdio.h>
#include <stdlib.h>
#define N 100
int main()
FILE *fp;
char str[N + 1];
//判断文件是否打开失败
if ( (fp = fopen("d:\\\\demo.txt", "rt")) == NULL )
puts("Fail to open file!");
exit(0);
//循环读取文件的每一行数据
while( fgets(str, N, fp) != NULL )
printf("%s", str);
//操作结束后关闭文件
fclose(fp);
return 0;
二、 fopen实例
2.1 读取常规文件
如果您开发一个带有一些原生 C/C++
层 (NDK
) 的 android
应用程序,并且
尝试通过执行
FILE *pFile = fopen ("myfile.txt" , "w" )
从其中打开文件,fopen
调用将失败。
原因是您的应用程序的当前工作目录是文件系统“ /”
的根目录,您的应用程序自然无权访问它。
为了解决这个问题,您必须将文件的完整路径传递给fopen
调用。
一种方法是将基本路径传递到您的应用程序具有读/写权限的应用程序数据文件夹。
例如,您可以使用上下文 API getFilesDir().getAbsolutePath()
http://developer.android.com/reference/android/content/Context.html#getFilesDir()
并将结果传递给本机函数调用。
这个特定的 API 当前解析为:“ /data/data/[app package]/文件/"
在您的本机代码中, fopen 调用应该成功
FILE *pFile = fopen (" /data/data/ [应用程序包]/文件/我的文件.txt" , "w" );
比如我写了下面这段代码来测试
我们读取一下/data/data/com.oyp.face2dsticker/files/HMS_MLKIT_FACE/version.txt
这个文件的内容
这个文件内容如下所示:
下面代码是处于cpp文件中的某个函数的代码,这个函数最终通过jni的方式被Java业务层调用。
FILE *pFile = nullptr;
// 这个路径可以在Java业务层通过 getFilesDir().getAbsolutePath() 来获取,然后传递到cpp层
pFile = fopen ("/data/data/com.oyp.face2dsticker/files/HMS_MLKIT_FACE/version.txt" ,"r");
if(nullptr == pFile)
LOGD("File open fail!\\n")
char tmp[100];
fread(tmp, 1, 100, pFile);
LOGD("File open success, content is :【%s】\\n", tmp)
fclose(pFile);
pFile = nullptr;
运行程序,成功读取到文件内容,打印出来的日志如下:
File open success, content is :【0.1.9.10q】
2.2 读取Asset目录文件
需要引入头文件
#include <android/asset_manager_jni.h>
typedef
一下AAsset
为esFile
typedef AAsset esFile;
static JNIEnv *sEnv = nullptr;
static jobject sAssetManager = nullptr;
void GLUtils::setEnvAndAssetManager(JNIEnv *env, jobject assetManager)
sEnv = env;
sAssetManager = assetManager;
static AAsset *loadAsset(const char *path)
AAssetManager *nativeManager = AAssetManager_fromJava(sEnv, sAssetManager);
if (nativeManager == nullptr)
return nullptr;
return AAssetManager_open(nativeManager, path, AASSET_MODE_UNKNOWN);
//
// File open
//
static esFile *esFileOpen(const char *fileName)
esFile *file;
FUN_BEGIN_TIME("GLUtils::esFileOpen")
AAssetManager *nativeManager = AAssetManager_fromJava(sEnv, sAssetManager);
if (nativeManager == nullptr)
return nullptr;
file = AAssetManager_open(nativeManager, fileName, AASSET_MODE_BUFFER);
FUN_END_TIME("GLUtils::esFileOpen")
return file;
//
// File close
//
static void esFileClose(esFile *pFile)
FUN_BEGIN_TIME("GLUtils::esFileClose")
if (pFile != nullptr)
AAsset_close(pFile);
FUN_END_TIME("GLUtils::esFileClose")
//
// File read
//
static int esFileRead(esFile *pFile, int bytesToRead, void *buffer)
int bytesRead = 0;
FUN_BEGIN_TIME("GLUtils::esFileRead")
if (pFile == nullptr)
return bytesRead;
bytesRead = AAsset_read(pFile, buffer, bytesToRead);
FUN_END_TIME("GLUtils::esFileRead")
return bytesRead;
char *GLUtils::openTextFile(const char *path)
char *buffer;
FUN_BEGIN_TIME("GLUtils::openTextFile")
LOGI("GLUtils::openTextFile path [%s]", path)
AAsset *asset = loadAsset(path);
if (asset == nullptr)
LOGE("Couldn't load %s", path)
return nullptr;
off_t length = AAsset_getLength(asset);
buffer = new char[length + 1];
int num = AAsset_read(asset, buffer, length);
AAsset_close(asset);
if (num != length)
LOGE("Couldn't read %s", path)
delete[] buffer;
return nullptr;
buffer[length] = '\\0';
FUN_END_TIME("GLUtils::openTextFile")
return buffer;
比如,我使用上面的openTextFile
方法读取asset目录下的vertex/vertex_sticker_normal.glsl
文件,代码如下:
char *VERTEX_SHADER = GLUtils::openTextFile("vertex/vertex_sticker_normal.glsl");
但是在调用上面代码之前,需要先调用一下setEnvAndAssetManager
初始化一下assetManager
// 初始化设置assetManager 一定要记得初始化,否则会报空指针异常
GLUtils::setEnvAndAssetManager(env, assetManager);
这个assetManager是java层传入进来的,下面讲解一下怎么传下来。
- 业务层通过这样调用,传assetManager过来
AssetManager assetManager = context.getAssets();
OypCustomEffectRender.onSurfaceCreated(assetManager);
- onSurfaceCreated方法最终会调用nativeOnSurfaceCreated方法
// 业务层方法
public static void onSurfaceCreated(AssetManager assetManager)
nativeOnSurfaceCreated(assetManager);
// JNI方法
private static native void nativeOnSurfaceCreated(AssetManager assetManager);
- 而nativeOnSurfaceCreated方法最终的JNI实现如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_opengl_OypCustomEffectRender_nativeOnSurfaceCreated(JNIEnv *env, jclass clazz, jobject asset_manager)
// 初始化设置assetManager 一定要记得初始化,否则会报空指针异常
GLUtils::setEnvAndAssetManager(env, assetManager);
// 干其他的业务
这样通过调用GLUtils::setEnvAndAssetManager(env, assetManager);
就将java层传进来的assetManager初始化了,那么在cpp层就可以使用去读取asset目录下的文件了。
运行结果如下:
2022-05-20 10:37:16.335 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][309]: [GLUtils::openTextFile] func start
2022-05-20 10:37:16.335 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][310]: GLUtils::openTextFile path [vertex/vertex_point.glsl]
2022-05-20 10:37:16.336 7627-7655/com.oyp.face2dsticker I/LOG_TAG_XtcCustomEffectRender: [GLUtils.cpp][openTextFile][326]: [GLUtils::openTextFile] func cost time 1ms
开发者涨薪指南
48位大咖的思考法则、工作方式、逻辑体系
以上是关于我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件的主要内容,如果未能解决你的问题,请参考以下文章
我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件
我的C/C++语言学习进阶之旅NDK开发之Native层使用fopen打开Android设备上的文件
我的C/C++语言学习进阶之旅NDK开发之解决错误:signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0xXXX
我的C/C++语言学习进阶之旅NDK开发之解决错误:signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0xXXX
我的C/C++语言学习进阶之旅NDK开发运行的时候出现错误signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x940a2e48
我的C/C++语言学习进阶之旅NDK开发运行的时候出现错误signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x940a2e48