android内部文件的读取

Android 文件管理方法
Android使用的是基于Linux的文件系统,对于文件的访问和管理是通过权限设置来限制的.
在Linux系统中,文件权限分别描述了创建者、同组用户和其他用户对文件的操作限制。
x表示可执行,r表示可读,w表示可写,d表示目录,-表示普通文件。
产生这样的文件权限与程序人员设定的
Android 存储文件的类型
(内部存储)程序开发人员可以建立和访问程序自身的私有文件;
(资源存储)可以访问保存在资源目录中的原始文件和XML文件;
(外部存储)可以在SD卡等外部存储设备中保存文件

Android系统允许应用程序创建仅能够自身访问的私有文件,文件保存在设备的内部存储器上,在Linux系统下的/data/data/<package name>/files目录中
Android系统不仅支持标准Java的IO类和方法,还提供了能够简化读写流式文件过程的函数
FileOutputStream openFileOutput(String filename int mode)
FileInputStream openFileInput(String filename)
参数文件不允许包含描述路径的斜杠(其存储位置固定)
访问模式:

MODE_PRIVATE 私有模式,缺陷模式,文件仅能够被文件创建程序访问,或具有相同UID的程序访问。
MODE_APPEND 追加模式,如果文件已经存在,则在文件的结尾处添加新数据。
MODE_WORLD_READABLE 全局读模式,允许任何程序读取私有文件。
MODE_WORLD_WRITEABLE 全局写模式,允许任何程序写入私有文件。

三个基本的读方法

abstract int read() :读取一个字节数据,并返回读到的数据,如果返回-1,表示读到了输入流的末尾。
   int read(byte[] b) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,表示读到了输入流的末尾。
   int read(byte[] b, int off, int len) :将数据读入一个字节数组,同时返回实际读取的字节数。如果返回-1,表示读到了输入流的末尾。off指定在数组b中存放数据的起始偏移位置;len指定读取的最大字节数。
其它方法
  long skip(long n) :在输入流中跳过n个字节,并返回实际跳过的字节数。
  int available() :返回在不发生阻塞的情况下,可读取的字节数。
  void close() :关闭输入流,释放和这个流相关的系统资源。
  void mark(int readlimit) :在输入流的当前位置放置一个标记,如果读取的字节数多于readlimit设置的值,则流忽略这个标记。
  void reset() :返回到上一个标记。
  boolean markSupported() :测试当前流是否支持mark和reset方法。如果支持,返回true,否则返回false。

三个基本的写方法

abstract void write(int b) :往输出流中写入一个字节。
   void write(byte[] b) :往输出流中写入数组b中的所有字节。
   void write(byte[] b, int off, int len) :往输出流中写入数组b中从偏移量off开始的len个字节的数据。
其它方法
  void flush() :刷新输出流,强制缓冲区中的输出字节被写出。
  void close() :关闭输出流,释放和这个流相关的系统资源。

对文件和流的操作容易引发异常,所以必须要用try-catch语句

主要核心代码

首先是新建一个文件
private final String FILE_NAME = "Myfile01.txt";
写文件
FileOutputStream fos = null;//声明一个全局变量
//注意下面的语句要进行抛异常处理FileNotFoundException e,IOException e
fos = openFileOutput(FILE_NAME,Context.MODE_PRIVATE);//写流式文件过程的

函数,这里的权限是私有的
String text = entryText.getText().toString();//把输入的内容转化为字符串
fos.write(text.getBytes());//把转化为字符串的内容转化为字节,然后写入
//下面语句写在finally里面
fos.flush();//把缓存里的内容写入到文件
fos.close();//关闭流

读取:方法一

FileInputStream fis = null;//定义一个全局变量  
fis = openFileInput(FILE_NAME);//打开要读取的文件  
if (fis.available() == 0){//判断文件是否为空,为空就直接返回  
 return;  
}  
byte[] readBytes = new byte[fis.available()];//把文件里的内容转化为字节  
while(fis.read(readBytes) != -1){//读文件,直到读到最后  
}  
String text = new String(readBytes);//把读到的字节转化为字符串  

读取:方法二

String path = "/data/data/cn.itcast.file/files/writeable.txt";//得到文件路径  
  File file = new File(path);//创建一个文件对象  
  FileInputStream inStream = new FileInputStream(file);//读文件  
  byte[] buffer = new byte[1024];//缓存  
  int len = 0;  
  ByteArrayOutputStream outStream = new ByteArrayOutputStream();  
  while( (len = inStream.read(buffer))!= -1){//直到读到文件结束  
   outStream.write(buffer, 0, len);  
  }  
  byte[] data = outStream.toByteArray();//得到文件的二进制数据  
  outStream.close();  
  inStream.close();  
  Log.i(TAG, new String(data)); 

  二.android存取的参考

可能遇到的问题

android系统自身自带有存储,另外也可以通过sd卡来扩充存储空间。前者好比pc中的硬盘,后者好移动硬盘。 前者空间较小,后者空间大,但后者不一定可用。 开发应用,处理本地数据存取时,可能会遇到这些问题:

1.需要判断sd卡是否可用: 占用过多机身内部存储,容易招致用户反感,优先将数据存放于sd卡

2.应用数据存放路径,同其他应用应该保持一致,应用卸载时,清除数据:

  • 标新立异在sd卡根目录建一个目录,招致用户反感
  • 用户卸载应用后,残留目录或者数据在用户机器上,招致用户反感

3.需要判断两者的可用空间: sd卡存在时,可用空间反而小于机身内部存储,这时应该选用机身存储;

4.数据安全性,本应用数据不愿意被其他应用读写;

5.图片缓存等,不应该被扫描加入到用户相册等媒体库中去。

基本操作

1.使用外部存储,需要的权限,在AndoridManifest.xml中:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2.

存储的用量情况

  • 根据系统用户不同,所能占用的存储空间大小也有不同 

 在API level 9及其以上时,File对象的getFreeSpace()方法获取系统root用户可用空间;
getUsableSpace()取非root用户可用空间

  •  当有多个存储可用时获取磁盘用量,根据当前系统情况选用合适的存储。
  • 根据系统存储用量,合理设定app所用的空间大小;运行时,也可做动态调整。 
  •  在API level 9及其以上的系统,可直接调用File对象的相关方法,以下需自行计算:
@TargetApi(VERSION_CODES.GINGERBREAD)
public static long getUsableSpace(File path) {
 if (path == null) {
     return -1;
 }
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
     return path.getUsableSpace();
 } else {
     if (!path.exists()) {
         return 0;
     } else {
         final StatFs stats = new StatFs(path.getPath());
         return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
     }
 }
}

 路径的规律

一般地,通过Context 和Environment`相关的方法获取文件存取的路径。

通过这两个类可获取各种路径,如图:

 ($rootDir)
+- /data                -> Environment.getDataDirectory()
|   |
|   |   ($appDataDir)
|   +- data/com.srain.cube.sample
|       |
|       |   ($filesDir)
|       +- files            -> Context.getFilesDir() / Context.getFileStreamPath("")
|       |       |
|       |       +- file1    -> Context.getFileStreamPath("file1")
|       |   ($cacheDir)
|       +- cache            -> Context.getCacheDir()
|       |
|       +- app_$name        ->(Context.getDir(String name, int mode)
|
|   ($rootDir)
+- /storage/sdcard0     -> Environment.getExternalStorageDirectory()
    |                       / Environment.getExternalStoragePublicDirectory("")
    |
    +- dir1             -> Environment.getExternalStoragePublicDirectory("dir1")
    |
    |   ($appDataDir)
    +- Andorid/data/com.srain.cube.sample
        |
        |   ($filesDir)
        +- files        -> Context.getExternalFilesDir("")
        |   |
        |   +- file1    -> Context.getExternalFilesDir("file1")
        |   +- Music    -> Context.getExternalFilesDir(Environment.Music);
        |   +- Picture  -> ... Environment.Picture
        |   +- ...
        |
        |   ($cacheDir)
        +- cache        -> Context.getExternalCacheDir()
        |
        +- ???

各个路径的特性

下面介绍这些路径的特性以及使用中需要注意的细节:

  1. 根目录($rootDir):

 内部存储路径: /data, 通过Environment.getDataDirectory() 获取
外部存储路径: /storage/sdcard0 (也有类似 /mnt/ 这样的),通过Environment.getExternalStorageDirectory()获取

示例:

Environment.getDataDirectory(): 
        /data

Environment.getExternalStorageDirectory(): 
        /storage/sdcard0

2.应用数据目录($appDataDir)

  • 内部储存: $appDataDir = $rootDir/data/$packageName,
  • 外部存储: $appDataDir = $rootDir/Andorid/data/$packageName
    在这些目录下的数据,在app卸载之后,会被系统删除,我们应将应用的数据放于这两个目录中。

3.外部存储中,公开的数据目录。 这些目录将不会随着应用的删除而被系统删除,请斟酌使用:

 Environment.getExternalStorageDirectory(): 
     /storage/sdcard0

 // 同 $rootDir
 Environment.getExternalStoragePublicDirectory(""): 
     /storage/sdcard0

 Environment.getExternalStoragePublicDirectory("folder1"): 
     /storage/sdcard0/folder1

4.应用数据目录下的目录 

一般的在$appDataDir下,会有两个目录:

1.数据缓存:$cacheDir = $appDataDir/cache:

  • 内部存储:Context.getCacheDir(), 机身内存不足时,文件会被删除
  •    外部存储:Context.getExternalCacheDir() 

外部存储没有实时监控,当空间不足时,文件不会实时被删除,可能返回空对象

示例:

Context.getCacheDir(): 
      /data/data/com.srain.cube.sample/cache

Context.getExternalCacheDir(): 
      /storage/sdcard0/Android/data/com.srain.cube.sample/cache

2.文件目录 $filesDir = $appDataDir/files:

  • 内部存储:通过Context.getFilesDir() 获取

Context.getFileStreamPath(String name)返回以name为文件名的文件对象,name为空,则返回 $filesDir 本身

示例:

Context.getFilesDir(): 
      /data/data/com.srain.cube.sample/files

Context.getFileStreamPath(""):
      /data/data/com.srain.cube.sample/files

Context.getFileStreamPath("file1"):
      /data/data/com.srain.cube.sample/files/file1
  •  外部存储:通过Context.getExternalFilesDir(String type)type为空字符串时获取.

type系统指定了几种类型:

Environment.DIRECTORY_MUSIC
Environment.DIRECTORY_PICTURES
...

 示例:

Context.getExternalFilesDir(""): 
      /storage/sdcard0/Android/data/com.srain.cube.sample/files

Context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
      /storage/sdcard0/Android/data/com.srain.cube.sample/files/Music

5.$cacheDir / $filesDir 安全性

在内部存储中,$cacheDir$filesDir是app安全的,其他应用无法读取本应用的数据,而外部存储则不是。

在外部存储中,这两个文件夹其他应用程序也可访问。

在外部存储中,$filesDir中的媒体文件,不会被当做媒体扫描出来,加到媒体库中。

  • $cacheDir / $filesDir 同级目录

在内部存储中:通过 Context.getDir(String name, int mode)可获取和 $filesDir / $cacheDir 同级的目录

目录的命名规则为 app_ + name, 通过mode可控制此目录为app私有还是其他app可读写。

示例:

Context.getDir("dir1", MODE_PRIVATE):
        Context.getDir: /data/data/com.srain.cube.sample/app_dir1
  • 特别注意, 对于外部存储,获取$cacheDir 或者 $filesDir及其下的路径

在API level 8 以下,或者空间不足,相关的方法获路径为空时,需要自己构造。

@TargetApi(VERSION_CODES.FROYO)
public static File getExternalCacheDir(Context context) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)) {
        File path = context.getExternalCacheDir();

        // In some case, even the sd card is mounted,
        // getExternalCacheDir will return null
        // may be it is nearly full.
        if (path != null) {
            return path;
        }
    }

    // Before Froyo or the path is null,
    // we need to construct the external cache folder ourselves
    final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
    return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}

                                        SharePreperfences存取

1.定义:

SharedPreferences也是一种轻型的数据存储方式,它的本质是基于XML文件存储key-value键值对数据,通常用来存储一些简单的配置信息(不如基本数据类型)。

2.存取路径:

其存储位置在/data/data/<包名>/shared_prefs目录下。

3.使用:

SharedPreferences对象本身只能获取数据而不支持存储和修改,存储修改是通过Editor对象实现。实现SharedPreferences存储的步骤如下:

  一、根据Context获取SharedPreferences对象

  二、利用edit()方法获取Editor对象。

  三、通过Editor对象存储key-value键值对数据。

  四、通过commit()方法提交数据。

1 publicclass MainActivity extends Activity {
 2     @Override
 3 publicvoid onCreate(Bundle savedInstanceState) {
 4 super.onCreate(savedInstanceState);
 5        setContentView(R.layout.main);
 6        
 7 //获取SharedPreferences对象
 8        Context ctx = MainActivity.this;       
 9        SharedPreferences sp = ctx.getSharedPreferences("SP", MODE_PRIVATE);
10 //存入数据
11        Editor editor = sp.edit();
12        editor.putString("STRING_KEY", "string");
13        editor.putInt("INT_KEY", 0);
14        editor.putBoolean("BOOLEAN_KEY", true);
15        editor.commit();
16        
17 //返回STRING_KEY的值
18        Log.d("SP", sp.getString("STRING_KEY", "none"));
19 //如果NOT_EXIST不存在,则返回值为"none"
20        Log.d("SP", sp.getString("NOT_EXIST", "none"));
21     }
22 

SP.xml文件的具体内容如下:

1 <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2 <map>
3 <string name="STRING_KEY">string</string>
4 <int name="INT_KEY" value="0"/>
5 <boolean name="BOOLEAN_KEY" value="true"/>
6 </map>

1、Context.getSharedPreferences(String name,int mode)来得到一个指定name的SharedPreferences实例

         name:是指文件名称,不需要加后缀.xml,系统会自动为我们添加上。一般这个文件存储在/data/data/<package name>/shared_prefs下(这个面试常问到)

 

        mode:是指定读写方式,其值有三种,分别为:

            Context.MODE_PRIVATE:指定该SharedPreferences数据只能被本应用程序读、写

            Context.MODE_WORLD_READABLE:指定该SharedPreferences数据能被其他应用程序读,但不能写,读到的数据也是缓存数据,并不是最新的。

            Context.MODE_WORLD_WRITEABLE:指定该SharedPreferences数据能被其他应用程序读写。

            Context.MODE_MULIT_PROCESS:可以从存取中拿到最新的数据,而不是缓存。

2、PreferenceManager.getDefaultSharedPreferences(Context)来获取在同一个包名下的SharedPreferences实例

      context:为上下文对象

常用方法:
    public abstract boolean contains (String key) ----- 判断SharedPreferences是否包含特定key的数据
    public abstract SharedPreferences.Editor edit () ----- 返回一个Edit对象用于操作SharedPreferences
    public abstract Map<String, ?> getAll () ------ 获取SharedPreferences数据里全部的key-value对象
    getXXX(String key,XXX defvlaue) ------ 获取SharedPreferences数据指定key所对应的value,如果该key不存在,返回默认值defValue。其中XXX可以是boolean、float、int、long、String等基本类型的值
Editor接口的常用方法:
    public abstract SharedPreferences.Editor clear () ---- 清空SharedPreferences里所有的数据
    public abstract boolean commit () ----- 当Editor编辑完成后,调用该方法可以提交修改,而且必须要调用这个数据才修改
    public abstract SharedPreferences.Editor putXXX (String key, boolean XXX) ---- 向SharedPreferences存入指定的key对应的数据,其中XXX可以是boolean、float、int、long、String等基本类型的值
    public abstract SharedPreferences.Editor remove (String key) ----- 删除SharedPreferences里指定key对应的数据项

重点:apply() 与 commit()方法的区别

1. apply没有返回值而commit返回boolean表明修改是否提交成功 
2. apply是将修改数据原子提交到内存, 而后异步真正提交到硬件磁盘, 而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内容,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。 
3. apply方法不会提示任何失败的提示。 
由于在一个进程中,sharedPreference是单实例,一般不会出现并发冲突,如果对提交的结果不关心的话,建议使用apply,当然需要确保提交成功且有后续操作的话,还是需要用commit的。

4.在API9以上才能使用apply()

总结:

 SharedPreferences对象与SQLite数据库相比,免去了创建数据库,创建表,写SQL语句等诸多操作,相对而言更加方便,简洁。但是SharedPreferences也有其自身缺陷,比如其职能存储boolean,int,float,long和String五种简单的数据类型,比如其无法进行条件查询等。所以不论SharedPreferences的数据存储操作是如何简单,它也只能是存储方式的一种补充,而无法完全替代如SQLite数据库这样的其他数据存储方式

                                                Sqlite存取

SQLite是一种转为嵌入式设备设计的轻型数据库,其只有五种数据类型,分别是:

    NULL: 空值

    INTEGER: 整数

    REAL: 浮点数

    TEXT: 字符串

    BLOB: 大数据

  在SQLite中,并没有专门设计BOOLEAN和DATE类型,因为BOOLEAN型可以用INTEGER的0和1代替true和false,而DATE类型则可以拥有特定格式的TEXT、REAL和INTEGER的值来代替显示,为了能方便的操作DATE类型,SQLite提供了一组函数,详见:http://www.sqlite.org/lang_datefunc.html。这样简单的数据类型设计更加符合嵌入式设备的要求。关于SQLite的更多资料,请参看:http://www.sqlite.org/

  在Android系统中提供了android.database.sqlite包,用于进行SQLite数据库的增、删、改、查工作。其主要方法如下:

  beginTransaction(): 开始一个事务。

  close(): 关闭连接,释放资源。

  delete(String table, String whereClause, String[] whereArgs): 根据给定条件,删除符合条件的记录。

  endTransaction(): 结束一个事务。

  execSQL(String sql): 执行给定SQL语句。

  insert(String table, String nullColumnHack, ContentValues values): 根据给定条件,插入一条记录。 

  openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory): 根据给定条件连接数据库,如果此数据库不存在,则创建。

  query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy): 执行查询。

  rawQuery(String sql, String[] selectionArgs): 根据给定SQL,执行查询。

  update(String table, ContentValues values, String whereClause, String[] whereArgs): 根据给定条件,修改符合条件的记录。

  除了上诉主要方法外,Android还提供了诸多实用的方法,总之一句话:其实Android访问数据库是一件很方便的事儿。

  一、 创建数据库

  通过openOrCreateDatabase(String path, SQLiteDatabase.CursorFactory factory)方法创建数据库。    

1 SQLiteDatabase db =this.openOrCreateDatabase("test_db.db", Context.MODE_PRIVATE, null);
2 SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase("/data/data/com.test/databases/test_db2.db3", null);

  如上两种方式均能创建数据库,this.openOrCreateDatabase是对SQLiteDatabase.openOrCreateDatabase而来,如代码所见,原生的SQLiteDatabase.openOrCreateDatabase()方法第一参数要求输入绝对路劲,而所有的数据库都是储存于“data/data/应用报名/databases”目录下,所以输入完全的绝对路劲是一件重复且繁杂的工作。采用this.openOrCreateDatabase则省去了此操作。执行操作后的结果如下图:  

 

  另外还可以通过写一个继承SQLiteOpenHelper类的方式创建数据库,此种方式是一种更加进阶的创建方式,所以在此不做描述。

  二、创建数据表,插入数据。

  Android系统并没有提供特别的创建数据表的方法,数据表通过SQL语句创建,代码如下:

1 db.execSQL("create table tab(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)");

  表创建好之后,通过insert(String table, String nullColumnHack, ContentValues values)方法插入数据,其中参数含义分别为:

    table: 目标表名

    nullColumnHack: 指定表中的某列列名。因为在SQLite中,不允许不允许插入所有列均为null的记录,因此初始值有值为空时,此列需显式赋予null

    values: ContentValues对象,类似于Java中的Map。以键值对的方式保存数据。

  数据插入代码如下: 

1  ContentValues values =new ContentValues();
2 for(int i=0;i<10;i++){
3        values.put("name", "test"+ i);
4        db.insert("tab", "_id", values);
5    }

  执行此操作后,会新增一个名为“tab”的数据表,利用SQLite客户端(推荐:SQLite Expert Personal 3)可轻松查看此表结构和数据。如下图:

  

  三、修改数据

   update(String table, ContentValues values, String whereClause, String[] whereArgs)方法用于修改数据,其四个参数的具体含义如下:

    table: 目标表名

    values: 要被修改成为的新值

    whereClause: where子句,除去where关键字剩下的部分,其中可带?占位符。如没有子句,则为null。

    whereArgs: 用于替代whereClause参数中?占位符的参数。如不需传入参数,则为null。

  数据修改代码如下:

1 ContentValues values =new ContentValues();
2 values.put("name", "name");
3 db.update("tab", values, "_id=1", null);
4 db.update("tab", values, "_id=?", new String[]{"5"});

  执行结果如下图,_id=1和_id=5的数据,name字段的值被修改为了“name”。

  四、查询数据。

  之前一直使用SQLite客户端查看数据情况了,这里就使用android提供的query()和rowQuery()方法执行查询。具体代码如下:  

1 Cursor c = db.query("tab", null, null, null, null, null, null);
 2 c.moveToFirst();
 3 while(!c.isAfterLast()){
 4 int index = c.getColumnIndex("name");
 5     Log.d("SQLite", c.getString(index));
 6     c.moveToNext();
 7 }
 8 c = db.rawQuery("select * from tab", null);
 9 c.moveToFirst();
10 while(!c.isAfterLast()){
11 int index = c.getColumnIndex("name");
12     Log.d("SQLite", c.getString(index));
13     c.moveToNext();
14 }

 

  查询结果如下图:

  可以清晰的在查询结果中,红线上下的数据是完全一致的,也就是说query和rawQuery方法在的不同仅仅在于所需参数的不同。rawQuery方法需要开发者手动写出查询SQL,而query方法是由目标表名、where子句、order by子句、having子句等诸多子句由系统组成SQL语句。两方法同返回Cursor对象,所以两方在使用时孰优孰劣,就看具体情况了。本人更喜欢rawQuery的方式,因为此方式更接近传统Java开发,也可以由专业DBA来书写SQL语句,这样更符合MVC的思想,而且这样的代码可读性更高。(query方法里面参数实在太多,有点记不住谁是order by子句,谁是having子句了)

  Cursor对象可以理解为游标对象,凡是对数据有所了解的人,相信对此对象都不会陌生,在这里机不再累述。只提醒一点,在第一次读取Cursor对象中的数据时,一定要先移动游标,否则此游标的位置在第一条记录之前,会引发异常。

  五、删除数据

  删除数据也是一件很简单的事,只需要调用delete方法,传入参数即可,delete(String table, String whereClause, String[] whereArgs)的参数三个参数具体含义如下:

    table: 目标表名

    whereClause: where子句,除去where关键字剩下的部分,其中可带?占位符。如没有子句,则为null。

 

    whereArgs: 用于替代whereClause参数中?占位符的参数。如不需传入参数,则为null。

 

  具体代码如下:

db.delete("tab", "_id=? or name=?", new String[]{"8", "name"});

 

  执行结果如下:

 

 

  其中_id=8和name=‘name’的数据统统被删除了。

  整个数据库的CRUD操作到此演示完了。最后提醒一点,在操作完数据后,一定要记得调用close()方法关闭连接,释放资源。这个原因相信大家都是懂的。

 

转载于:https://my.oschina.net/quguangle/blog/755389

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值