您的位置:首页 > 移动开发 > Android开发

Android数据存储之:SQLite数据库存储

2015-11-17 17:10 459 查看
Android常见的存储方式 

 (1)SQLite数据库存储:详见本文

(2)SharedPreferences存储,详见

(3)文件存储:详见

(4)网络存储,

(5)ContentProvider存储数据:详见

部分内容来自:http://blog.csdn.net/zuolongsnail/article/details/6529096,http://fine36.blog.163.com/blog/static/189251005201183053216297/,。。。。

Android系统竟然是内置了数据库SQLite的!SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,
通常只需要几百K的内存就足够了, 因而特别适合在移动设备上使用

在Android应用中创建数据库后数据库文件是存储在/data/ data/应用包名/databases/下。

 

在Android中使用SQLite会涉及到以下三个类(或接口):


1.SQLiteOpenHelper


*SQLiteOpenHelper 构造方法,一般传递一个要创建的数据库名称name参数

*onCreate 创建数据库时调用(创建所有表的操作)

*onUpgrade 版本更新时调用

*getReadableDatabase 创建或打开一个只读数据库

*getWritableDatabase 创建或打开一个读写数据库
(不同的是,当数据库不可写入的时候(如磁盘空间已满)getReadableDatabase()方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase()方法则将出现异常。)

2.SQLiteDatabase
*openOrCreateDatabase 打开或者创建数据库

*insert 添加一条记录

*delete 删除一条记录

*query 查询记录

*update 更新记录

*execSQL 执行一条SQL语句

*close 关闭数据库
3.Cursor
*getCount 总记录条数

*isFirst 判断是否第一条记录

*isLast 判断是否最后一条记录

*moveToFirst 移动到第一条记录

*moveToLast 移动到最后一条记录

*move 移动到指定记录

*moveToNext 移动到下一条记录

*moveToPrevious 移动到上一条记录

*getColumnIndexOrThrow根据列名称获得列索引

*getInt 获得指定列索引的int类型值

*getString 获得指定列索引的String类型值
注:某些方法是有重载的,可以结合docs熟悉下。

 
 android.database.sqlite.SQLiteDatabase是Android
SDK中操作数据库的核心类之一。使用SQLiteDatabase可以打开数据库,也可以对数据库进行操作。然而,为了数据库升级的需要以及使用更方便,往往使用SQLiteOpenHelper的子类来完成创建、打开数据库及各种数据库的操作。
  SQLiteOpenHelper是一个抽象类,在该类中有如下两个抽象方法,因此,SQLiteOpenHelper的子类必须实现这两个方法。public
abstract void onCreate(SQLiteDatabase db);
      public abstract void onUpgrade(SQLiteDatabase db,int oldVersion, int newVersion);

onCreate()调用:
SQLiteOpenHelper会自动检测数据库文件是否存在。如果数据库文件存在,会打开这个数据库。如果数据库文件不存在的时候,SQLiteOpenHelper首先会创建一个数据库文件,然后打开这个数据库,最后会调用onCreate方法。因此,onCreate方法一般被用来在新创建的数据库中建立表、视图等数据库组件。也就是说,onCreate方法在数据库文件第一次被创建时调用。

onUpgrade()调用:

SQLiteOpenHelper类的构造方法:
public SQLiteOpenHelper(Context context, Stringname, CursorFactory factory, int
version);

参数意义:
name参数表示数据库文件名(不包含文件路径),SQLiteOpenHelper会根据这个文件名创建数据库文件。
version表示数据库的版本号。
如果当前传递的数据库版本号比上次创建或升级的数据库版本号高。SQLiteOpenHelper就会调用onUpgrade方法。也就是说,当数据库第1次创建时会有一个初始的版本号。当需要对数据库中表、视图等组件升级时可以增大版本号。这时SQLiteOpenHelper会调用onUpgrade方法。当调用完onUpgrade方法后,系统会更新数据库的版本号。这个当前的版本号就是通过SQLiteOpenHelper类的最后一个参数version传入SQLiteOpenHelper对象的。
因此,在onUpgrade方法中一般会先删除之前版本创建的表、视图等组件,再重新创建它们。

(举例:如果版本1创建了表book,后来版本升级后,版本2里面有表book,同时多了表category,代码如下图:

public class MyDatabaseHelper extends SQLiteOpenHelper {
……
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("drop table if exists Book");
onCreate(db);
}
}


用户分为两种:

新用户:因为是第一次进入,发现没有数据库,所以直接调用onCreate方法创建这个版本所需要的表。
老用户:升级软件,系统识别已经创建了数据库,所以不会调用onCreate(),因为版本有改变,会调用onUpgrade方法,这个方法里面就要去删除以前的表book的操作(因为如果在创建表时发现这张表已经存在了,就会直接报错,所以先全部删除掉),升级版本的情况下系统不会自动的调用onCreate方法,所以可能会错过新版本的表category,所以在onUpgrade方法里面手动的调用onCreate方法去创建表。
注意:如果在onUpgrade()方法里面不去删除所有表,那么在onCreate里面创表的语句一定要用create
table if not exists xxx,如果不存在就创建,存在直接用。

升级数据库的最佳写法
  上面的升级数据库的方式是非常粗暴的,为了保证数据库中的表是最新的,我们只是简单地在onUpgrade()方法中删除掉了当前所有的表,然后强制重新执行了一遍onCreate()方法。这种方式在产品的开发阶段确实可以用,但是当产品真正上线了之后就绝对不行了。想象以下场景,比如你编写的某个应用已经成功上线,并且还拥有了不错的下载量。现在由于添加新功能的原因,使得数据库也需要一起升级,然后用户更新了这个版本之后发现以前程序中存储的本地数据全部丢失了!那么很遗憾,你的用户群体可能已经流失一大半了。听起来好像挺恐怖的样子,难道说在产品发布出去之后还不能升级数据库了?当然不是,其实只需要进行一些合理的控制,就可以保证在升级数据库的时候数据并不会丢失了。
  如何实现这样的功能,你已经知道,每一个数据库版本都会对应一个版本号, 当指定的数据库版本号大于当前数据库版本号的时候,就会进入到onUpgrade()方法中去执行更新操作。这里需要为每一个版本号赋予它各自改变的内容,然后在onUpgrade()方法中对当前数据库的版本号进行判断,再执行相应的改变就可以了。

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
default:
}
}


可以看到,在onCreate()方法里我们新增了一条建表语句,然后又在onUpgrade()方法中添加了一个switch判断,当用户是直接安装的第二版的程序时,就会将两张表一起创建。而当用户是使用第2版的程序覆盖安装第1版的程序时,就会进入到升级数据库的操作中,此时由于Book表已经存在了,因此只需要创建一张Category表即可。
但是没过多久,新的需求又来了,这次要给Book表和Category表之间建立关联,需要在Book表中添加一个category_id的字段。

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL(CREATE_CATEGORY);
case 2:
db.execSQL("alter table Book add column category_id integer");
default:
}
}


  可以看到,首先我们在Book表的建表语句中添加了一个category_id列,这样当用户直接安装第3版的程序时,这个新增的列就已经自动添加成功了。然而,如果用户之前已经安装了某一版本的程序,现在需要覆盖安装,就会进入到升级数据库的操作中。在onUpgrade()方法里,我们添加了一个新的case,如果当前数据库的版本号是2,就会执行alter命令来为Book表新增一个category_id列。
  这里请注意一个非常重要的细节,switch中每一个case的最后都是没有使用break,为什么要这么做呢?这是为了保证在跨版本升级的时候,每一次的数据库修改都能被全部执行到。比如用户当前是从第二版程序升级到第三版程序的,那么case 2会执行,当用户从1跨版本升级到3的时候,就会执行case1和case2,这样保证了中间版本2的操作一定会被执行到。

在数据库使用过程中使用事务
  前面我们已经知道,SQLite数据库是支持事务的,事务的特性可以保证让某一系列的操作要么全部完成,要么一个都不会完成。那么在什么情况下才需要使用事务呢?想象以下场景,比如你正在进行一次转账操作,银行会将转账的金额先从你的账户中扣除,然后再向收款方的账户中添加等量的金额。看上去好像没什么问题吧?可是,如果当你账户中的金额刚刚被扣除,这时由于一些异常原因导致对方收款失败,这一部分钱就凭空消失了!当然银行肯定已经充分考虑到了这种情况,它会保证扣钱和收款的操作要么一起成功,要么都不会成功,而使用的技术当然就是事务了。
 
接下来我们看一看如何在Android中使用事务吧,比如Book表中的数据都已经很老了,现在准备全部废弃掉替换成新数据,可以先使用delete()方法将Book表中的数据删除,
然后再使用insert()方法将新的数据添加到表中。我们要保证的是,删除旧数据和添加新数据的操作必须一起完成,否则就还要继续保留原来的旧数据。

dbHelper = new MyDatabaseHelper(this, "BookStore.db", null, 2);
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction(); //开启事务
try {
db.delete("Book", null, null);
if (true) {
//在这里手动抛出一个异常,让事务失败
throw new NullPointerException();
}
ContentValues values = new ContentValues();
values.put("name", "Game of Thrones");
values.put("author", "George Martin");
values.put("pages", 720);
values.put("price", 20.85);
db.insert("Book", null, values);
db.setTransactionSuccessful(); //事务已经执行成功
} catch (Exception e) {
e.printStackTrace();
} finally {
db.endTransaction(); //结束事务
}


上述代码就是Android中事务的标准用法,首先调用SQLiteDatabase的beginTransaction()方法来开启一个事务,然后在一个异常捕获的代码块中去执行具体的数据库操作,当所有的操作都完成之后,调用setTransactionSuccessful()表示事务已经执行成功了,最后在finally代码块中调用endTransaction()来结束事务。注意观察,我们在删除旧数据的操作完成后手动抛出了一个NullPointerException,这样添加新数据的代码就执行不到了。不过由于事务的存在,中途出现异常会导致事务的失败,此时旧数据应该是删除不掉的。
264

下面贴上数据库操作的代码,完整代码下载地址:android_sqlite.rar
1.创建数据库只要自定义一个类继承SQLiteOpenHelper即可。在SQLiteOpenHelper的子类中至少需要实现三个方法:

(1)构造方法,调用父类SQLiteOpenHelper的构造函数。需要四个参数:上下文环境(例如一个Activity);数据库名称;一个可选的游标工厂(通常是null);一个正在使用的数据库版本。

(2)onCreate方法,需要一个SQLiteDatabase对象作为参数,根据需要对这个对象填充表和初始化数据。

(3)onUpgrade方法,需要三个参数:一个SQLiteDatabase对象,一个旧的版本号和一个新的版本号。

(为了数据库的多次使用,同时避免错误,建议一个工程的数据库操作的代码文件都放在一个独立的包里面,里面包含以下3个java文件:一个是继承SQLiteOpenHelper的子类(负责创建数据库和各种表视图等,同时解决版本升级的问题),一个是数据库操作的类(负责数据库的增删改查),一个专门存储数据库的常量的java类

(1)SQLiteOpenHelper的子类

/**
* 数据库操作助手类
*
* @author zuolongsnail
*/
public class AndroidSQLiteOpenHelper extends SQLiteOpenHelper {

// 数据库名称
public static final String DBNAME = "android.db";
// 数据库版本
public static final int VERSION = 2;
// 建表语句,大小写不敏感
private static final String CREATETABLE = "create table "
+ Person.TABLENAME
+ "(id string, name string, gender int, age int)";

public AndroidSQLiteOpenHelper(Context context) {
super(context, DBNAME, null, VERSION);
}

// 创建表
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATETABLE);
}

// 更新表
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
this.deleteDB(db);
this.onCreate(db);
}

// 删除表
private void deleteDB(SQLiteDatabase db) {
db.execSQL("drop table if exists " + Person.TABLENAME);
}
}

w
plaincop  
(2)数据库操作的类(负责数据库的增删改查)
        对数据库表进行操作的方法有两种:
1)使用execSQL方法执行SQL语句;
2)使用insert、delete、update和query方法,把SQL语句的一部分作为参数。
注:查询数据库时执行SQL语句是使用SQLiteDatabase的rawQuery方法而不是execSQL。 

/**
* 数据库管理类
*
* @author zuolongsnail
*
*/
public class DatabaseManager {

private AndroidSQLiteOpenHelper dbHelper;

public DatabaseManager(Context context) {
dbHelper = new AndroidSQLiteOpenHelper(context);
}

// 插入记录
public int insert(Person person) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try {
db.execSQL("insert into " + Person.TABLENAME
+ " values(?, ?, ?, ?)", new Object[] { person.id,
person.name, person.gender, person.age });
db.setTransactionSuccessful();
} catch (Exception e) {
return 0;
} finally {
db.endTransaction();
}
db.close();
return 1;
}

// 删除记录
public int delete(Person person) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try {
db.execSQL("delete from " + Person.TABLENAME + " where id = ?",
new Object[] { person.id });
db.setTransactionSuccessful();
} catch (Exception e) {
return 0;
} finally {
db.endTransaction();
}
db.close();
return 1;
}

// 更新记录
public int update(Person person) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try {
db.execSQL("update " + Person.TABLENAME
+ " set name=?, gender=?, age=? where id=?", new Object[] {
person.name, person.gender, person.age, person.id });
db.setTransactionSuccessful();
} catch (Exception e) {
return 0;
} finally {
db.endTransaction();
}
db.close();
return 1;
}

// 查询记录
public ArrayList<Person> query(String id) {
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor;
Person person;
ArrayList<Person> list = new ArrayList<Person>();
// 若fileId为null或""则查询所有记录
if (id == null || id.equals("")) {
cursor = db.rawQuery("select * from " + Person.TABLENAME, null);
} else {
cursor = db.rawQuery("select * from " + Person.TABLENAME
+ " where id=?", new String[] { id });
}
while (cursor.moveToNext()) {
person = new Person();
person.id = cursor.getString(cursor.getColumnIndex("id"));
person.name = cursor.getString(cursor.getColumnIndex("name"));
person.gender = cursor.getString(cursor.getColumnIndex("gender"));
person.age = cursor.getInt(cursor.getColumnIndex("age"));
list.add(person);
}
cursor.close();
db.close();
if (list.size() == 0) {
Log.e("SQLite", "****表中无数据****");
}
return list;
}
}


(3)数据库操作需要的常量类

/**
* 数据库的工具类
* 里面封装了很多常量,便于用数据库的时候直接访问
* @author Administrator
*
*/
public class DBConfig {
/**数据库名*/
public final static String DB_NAME ="UserDatabase.db";
/**创建表语句的头部*/
public static final String CREAT_TABLE_HEAD="create table if not exists ";
/**创建表语句的主件自增*/
public static final String PRIMARY_KEY=" integer primary key autoincrement,";
/**版本号*/
public final static  int VERSION=1;
/**表名称*/
public static final String TB_USER="user";
/**字段名id*/
public static final String C_ID="id";
/**字段名name*/
public static final String C_NAME="name";
/**字段名tel*/
public static final String C_TEL="tel";
}
SQL语句不小心就出个小错,所以我们写成常量,要用的时候直接调用,避免他人使用这些常量的时候出错,也便于我们统一修改。


有了这个类之后,需要创建表的语句:

sql语句:create table if not exists DBConfig.tb_note(id integer primary key autoincrement,title verchar(30),content text
,time verchar(30))

下面为调用了DBConfig.java类的sql语句,这样可以避免书写出现的错误

static final String CREAT_TABLE_USER = DBConfig.CREAT_TABLE_HEAD+ DBConfig.TB_USER+"("
+DBConfig.C_ID+DBConfig.PRIMARY_KEY
+DBConfig.C_NAME+" verchar(30),"
+DBConfig.C_TEL+" verchar(30))";


有了常量之后的数据库操作类可以这样写:

<span style="font-size:18px;">/**
* 数据库的工具类 负责表的增删改查
*
* @author Administrator
*
*/
public class DBOperator {
SQLiteDatabase mySql;
MyDBHelper helper;

public DBOperator(Context context) {
helper = new MyDBHelper(context);
mySql = helper.getReadableDatabase();
}

/**
* 插入数据
* @param user
* @return
*/
public long addUser(User user) {
if(user!=null){
return mySql.insert(DBConfig.TB_USER, null, buildValues(user));
}
return 0;
}

private ContentValues buildValues(User user){
ContentValues values=new ContentValues();
values.put(DBConfig.C_NAME, user.getName());
values.put(DBConfig.C_TEL, user.getTel());
return values;
}

/**
* 删除数据
* @param id
* @return
*/
public int deleteUserById(int id){
//mySql.delete("tb_note", "time=?", new String[]{time});
return mySql.delete(DBConfig.TB_USER, DBConfig.C_ID+"="+id, null);
}
/**
* 更新数据
* @param user
* @return
*/
public int changeUser(User user){
if(user==null || user.getId()==0){
//如果传进来的user是刚new之后的user那user不为null,但是id等int型默认为0
return 0;
}else{
return mySql.update(DBConfig.TB_USER, buildValues(user), DBConfig.C_ID+"="+user.getId(), null);
}
}

/**
* 通过id查询指定数据
* @param id
* @return
*/
public User queryById(int id){
//sql语句:mySql.rawQuery("select * from "+DBConfig.TB_USER+" where "+DBConfig.C_ID+"="+id, null);
Cursor cursor=mySql.query(DBConfig.TB_USER, null, DBConfig.C_ID+"="+id, null, null, null, null);
User user=null;
if(cursor.moveToNext()){
user=cursorToGetUser(cursor);
}
cursor.close();
return user;
}

/**
* 查询表中所有东西
* @return
*/
public  List<User> queryUserAll(){
Cursor cursor=mySql.rawQuery("select * from "+DBConfig.TB_USER, null);
List<User> list=new ArrayList<User>();
while(cursor.moveToNext()){
User user=cursorToGetUser(cursor);
list.add(user);
user=null;
}
cursor.close();
return list;
}

private User cursorToGetUser(Cursor cursor){
User user=new User();
user.setId(cursor.getInt(cursor.getColumnIndex(DBConfig.C_ID)));
user.setName(cursor.getString(cursor.getColumnIndex(DBConfig.C_NAME)));
user.setTel(cursor.getString(cursor.getColumnIndex(DBConfig.C_TEL)));
return user;
}
/**
* 关闭数据库
*/
public void dbClose(){
if(mySql!=null){
mySql.close();
}
}

}</span>


SQLiteDatabase的使用(建议使用上面的SQLiteOpenHelper)

private SQLiteDatabase mySql;
* 创建数据库
*/
private void createDataBase() {
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File file = Environment.getExternalStorageDirectory();
File newFile = new File(file, "noteBook.db");
mySql=SQLiteDatabase.openOrCreateDatabase(newFile, null);
String sql="create table if not exists tb_note(id integer primary key autoincrement,title verchar(30),content text ,time verchar(30))";
mySql.execSQL(sql);
}
}


删除数据:

<span style="font-size:18px;">final String time=note.getTime();
mySql.delete("tb_note", "time=?", new String[]{time});</span>


查询表中所有数据:

/**
<span style="font-size:18px;"> * 获取数据
* @return
*/
private List<NoteBook> queryDatabase() {
list=new ArrayList<NoteBook>();
Cursor cursor=mySql.query("tb_note", null, null, null, null, null, null);
int count=cursor.getCount();//得到行数
if(count>0){
while(cursor.moveToNext()){
String title=cursor.getString(cursor.getColumnIndex("title"));
String time=cursor.getString(cursor.getColumnIndex("time"));
//取数据存入note对象中
NoteBook note=new NoteBook();
note.setTitle(title);
note.setTime(time);
//把对象存list
list.add(note);
note=null;//方便回收
}
cursor.close();
}
return list;
}</span>


cursor游标的使用:转载:http://blog.sina.com.cn/s/blog_618199e60101fskp.html

在你理解和使用 Android Cursor 的时候你必须先知道关于 Cursor 的几件事情:

Cursor是每行的集合,使用 moveToFirst() 定位第一行。你必须知道每一列的名称。你必须知道每一列的数据类型。Cursor 是一个随机的数据源。所有的数据都是通过下标取得。

关于 Cursor 的重要方法:

·close()——关闭游标,释放资源

·copyStringToBuffer(int columnIndex, CharArrayBuffer buffer)——在缓冲区中检索请求的列的文本,将将其存储

·getColumnCount()——返回所有列的总数

·getColumnIndex(String columnName)——返回指定名称所在的列,如果不存在返回-1

·getColumnIndexOrThrow(String columnName)——从零开始返回指定列名称,如果不存在将抛出IllegalArgumentException 异常。

·getColumnName(int columnIndex)——从给定的索引返回列名

·getColumnNames()——返回一个字符串数组的列名

·getCount()——返回Cursor 中的行数

·moveToFirst()——移动光标到第一行

·moveToLast()——移动光标到最后一行

·moveToNext()——移动光标到下一行

·moveToPosition(int position)——移动光标到一个绝对的位置

·moveToPrevious()——移动光标到上一行

 

cursor.moveToFirst() == false //表示Cursor为空

cursor.moveToNext()==false;//表示Cursor已经取到最后一行

访问Cursor 的下标获得其中的数据

int nameColumnIndex = cur.getColumnIndex(People.NAME);

String name = cur.getString(nameColumnIndex);

现在让我们看看如何循环 Cursor 取出我们需要的数据

<span style="font-size:18px;">while(cur.moveToNext()) {
//光标移动成功
String email = cursor.getString(cursor.getColumnIndex(RuiXin.EMAIL));
cursor.close();//查找后关闭游标
//把数据取出
}</span>


当cur.moveToNext()==false时将跳出循环,即 Cursor 数据循环完毕。

如果你喜欢用 for 循环而不想用While 循环可以使用Google 提供的几下方法:

·isBeforeFirst()——返回游标是否指向之前第一行的位置

·isAfterLast()——返回游标是否指向第最后一行的位置

·isClosed()——如果返回 true 即表示该游戏标己关闭

有了以上的方法,可以如此取出数据

<span style="font-size:18px;">for(cur.moveToFirst();!cur.isAfterLast();cur.moveToNext()){
int nameColumn = cur.getColumnIndex(People.NAME);
int phoneColumn = cur.getColumnIndex(People.NUMBER);
String name = cur.getString(nameColumn);
String phoneNumber = cur.getString(phoneColumn);
}</span>

Tip:在Android 查询数据是通过Cursor类来实现的。当我们使用 SQLiteDatabase.query()方法时,就会得到Cursor对象,Cursor所指向的就是每一条数据。

Cursor位于android.database.Cursor类,可见出它的设计是基于数据库服务产生的。

另:Activity.startManagingCursor方法,将获得的Cursor对象交与Activity管理,这样Cursor对象的生命周期便能与当前的Activity自动同步,省去了自己对Cursor的管理。

1.这个方法使用的前提是:游标结果集里有很多的数据记录。所以,在使用之前,先对Cursor是否为null进行判断,如果Cursor != null,再使用此方法
2.如果使用这个方法,最后也要用stopManagingCursor()来把它停止掉,以免出现错误。
3.使用这个方法的目的是把获取的Cursor对象交给Activity管理,这样Cursor的生命周期便能和Activity自动同步,省去自己手动管理。手动处理cursor关闭:cursor.close();
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android