IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 内容提供者ContentProvider(跨进程) -> 正文阅读

[移动开发]Android 内容提供者ContentProvider(跨进程)

1.内容提供者ContentProvider
ContentProvider是android四大组件之一,它为不同的应用之间实现数据共享,提供统一的接口,也就是说ContentProvider可以实现进程间的数据共享,实现跨进程通信。
ContentProvider主要功能是为不同的app之间数据共享提供统一的接口,而且ContentProvider是以类似数据库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”,那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基本一样,只不过是采用URI来表示外界需要访问的“数据库”。
也就是说,如果我们想让其他的应用使用我们自己程序内的数据,就可以使用ContentProvider定义一个对外开放的接口,从而使得其他的应用可以使用我们自己应用中的文件、数据库内存储的信息。比如,在Android系统中,很多数据如:联系人信息、短信信息、图片库、音频库等,这些信息在开发中还是经常用到的,这些信息谷歌工程师已经帮我们封装好了,我们可以使用谷歌给我们的Uri去直接访问这些数据。

2.ContentProvider的优势
虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。
ContentProvider为应用间的数据交互提供了一个安全的环境。
ContentProvider提供了对底层数据存储方式的抽象,比如下图,底层使用了Sqlit数据库,在用ContentProvider进行封装后,把sqlit换成其他数据库也不会影响其功能。
在这里插入图片描述
3.ContentProvider的用法
首先我们需要知道三个类:
ContentProvider(内容提供者)
ContentResolver(内容解析者)
ContentObserver(内容观察者)

假如我们现在有个应用A提供了数据 ,应用B要操作应用A的数据,那么应用A需要使用ContentProvider去共享自己数据;应用B使用ContentResolver去操作应用A的数据,通过ContentObserver去监听应用A的数据变化,当应用A的数据发生改变时,通知ContentObserver去告诉应用B数据变化了及时更新。
这就是通信的大致流程,在了解更加详细的流程之前,我们还需要知道几个概念。

4.ContentProvider中的URI
Uri — 通用资源标志符(Universal Resource Identifier),Uri代表要操作的数据,Android中可用的每种资源,比如图像、视频片段等都可以用Uri来表示。
①URI格式
ContentProvider中的URI是有固定格式的,例如:
在这里插入图片描述
Authority:授权信息,用来区别不同的ContentProvider。
path:表名,用来区分ContentProvider中不同的数据表。
id:id号,用来区分表中的不同数据。
②UriMatch
UriMatch主要为了匹配URI,比如应用A提供了数据,但是并不是说所有的应用都可以操作这些数据,只有提供了满足应用A的URI,才能访问A的数据,而UriMatch就是做这个匹配工作的,他有如下方法:
(1)public UriMatcher(int code) :它的作用就是创建一个UriMatch对象。
用法如下:
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//匹配不成功返回NO_MATCH(-1)
(2)public void addURI(String authority,String path, int code):它的作用是在ContentProvider添加一个用于匹配的Uri,当匹配成功时返回第三个参数code。
参数说明:
authority:主机名(用于唯一标示一个ContentProvider,这个需要和清单文件中的authorities属性相同);
path:路径路径(可以用来表示我们要操作的数据,路径的构建应根据业务而定);
code:返回值(用于匹配uri的时候,作为匹配成功的返回值)
(3)public int match(Uri uri) :这里的Uri就是传过来的要进行验证匹配的Uri。
Uri可以是精确的字符串,Uri中带有*表示可匹配任意text,#表示只能匹配数字。
假如add的uri是: content://com.example.test/student/#,那么content://com.example.test/student/10 可以匹配成功,这里的10可以是任意的数字。

5.ContentProvider
要使用ContentProvider,需要自定义自己的内容提供者,继承自ContentProvider,并实现下列方法:
①public boolean onCreate()在创建ContentProvider时调用。
②public Cursor query (Uri, String[], String, String[], String) 用于查询指定Uri的ContentProvider,返回一个Cursor。
③public Uri insert (Uri, ContentValues) 用于添加数据到指定Uri的ContentProvider中(外部应用向ContentProvider中添加数据)。
④public int update (Uri, ContentValues, String, String[]) 用于更新指定Uri的ContentProvider中的数据。
⑤public int delete (Uri, String, String[]) 用于从指定Uri的ContentProvider中删除数据。
⑥public String getType (Uri) 用于返回指定的Uri中的数据的MIME类型。

注意:数据访问的方法(如:insert和update可能被多个线程同时调用,此时必须是线程安全的。其他方法(如:onCreate)只能被应用的主线程调用,它应当避免冗长的操作。

6.ContentResolver
我们知道ContentProvider共享数据是通过定义一个对外开放的统一的接口来实现的。 然而,其他应用程序并不直接调用这些方法,而是使用一个 ContentResolver对象,通过调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,通过与其合作来对所有相关交互通讯进行管理。 当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver类来完成。
要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法:ContentResolver cr = getContentResolver();
在上面我们提到ContentProvider可以向其他应用程序提供数据,与之对应的ContentResolver则负责获取ContentProvider提供的数据,修改、添加、删除、更新数据等。

ContentResolver 类也提供了与ContentProvider类相对应的四个方法:
(1)public Uri insert(Uri uri, ContentValues values) 该方法用于往ContentProvider添加数据。
(2)public int delete(Uri uri, String selection, String[] selectionArgs) 该方法用于从ContentProvider删除数据。
(3)public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 该方法用于更新ContentProvider中的数据。
(4)public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 该方法用于从ContentProvider中获取数据。
这些方法的第一个参数为Uri,代表要操作的是哪个ContentProvider和对其中的什么数据进行操作,假设给定的是 Uri.parse(“content://com.qstingda.provider.personprovider/contact/15”),那么将会对主机名为com.qstingda.provider.personprovider的ContentProvider进行操作,path为contact/15的数据。

ContentResolver(内容解析者)请求被自动转发到合适的内容提供者实例 ,所以子类不需要担心跨进程调用的细节。

7.ContentObserver用法
ContentObserver内容观察者,用于观察指定的Uri引起的数据库的变化,然后通知主线程,根据需求做我们想要做的处理。比方说当观察到ContentProvider的数据变化时会自动调用谷歌工程师给我们提供好的方法,可以在此方法中通知主线程数据改变等等。
要实现这样的功能,首先要做的就是注册这个观察者,这里的注册是在需要监测ContentProvider的应用中进行注册,并不是在ContentProvider中。在ContentProvider中要做的就是当数据变化时进行通知,这里通知的方法谷歌已经帮我们写好,直接调用就行了。

注册内容观察者:
public final void registerContentObserver (Uri uri, boolean notifyForDescendents, ContentObserver observer)
这样就注册了一个观察者实例,当指定的Uri发生改变时,这个实例会回调实例对象做相应处理。
其中参数的意义:
uri:需要观察的Uri
notifyForDescendents:如果为true,表示以这个Uri为开头的所有Uri都会被匹配到;如果为false,表示精确匹配,即只会匹配这个给定的Uri。

举个例子,假如有这么几个Uri:
①content://com.example.studentProvider/student
②content://com.example.studentProvider/student/#
③content://com.example.studentProvider/student/10
④content://com.example.studentProvider/student/teacher
假如观察的Uri为content://com.example.studentProvider/student,当notifyForDescendents为true时,则以这个Uri开头的Uri的数据变化时都会被捕捉到,在这里也就是①②③④的Uri的数据的变化都能被捕捉到;当notifyForDescendents为false时,则只有①中Uri变化时才能被捕捉到。

看到registerContentObserver 这个方法,立马应该能够想到ContentResolver中的另一个方法
public final voidunregisterContentObserver(ContentObserver observer),它的作用就是取消对注册的那个Uri的观察,这里传进去的参数就是在registerContentObserver中传递进去的ContentObserver对象。

那么问题来了,怎么去写一个ContentObserver呢?其实它的实现很简单,直接创建一个类继承ContentObserver,需要注意的是,必须要实现它的构造方法:
public ContentObserver(Handlerhandler)
这里传进去的是一个Handler对象,这个Handler对象的作用一般要依赖于ContentObserver的另一个方法,即:
public void onChange(boolean selfChange)
这个方法的作用就是当指定的Uri的数据发生变化时会回调的方法,此时可以借助构造方法中的Handler对象将这个变化的消息发送给主线程,当主线程接收到这个消息之后就可以按照我们的需求来完成相应的操作。
下面通过一个例子更深入的了解一下ContentProvider的用法。

8.举例
我们自定义一个ContentProvider,然后在其它应用中来访问自定义的ContentProvider的数据。
这个案例的运行效果如下:
在这里插入图片描述
这里的插入数据,是在一个项目中向另一个项目中的ContentProvider中插入一条数据,其他的操作也是,接下来就来看看怎么实现上述的效果。

自定义的PeopleContentProvider的代码如下:
public class PeopleContentProvider extends ContentProvider {
//这里的AUTHORITY就是我们在AndroidManifest.xml中配置的authorities,这里的authorities可以随便写
private static final String AUTHORITY = “com.example.studentProvider”;
//匹配成功后的匹配码
private static final int MATCH_ALL_CODE = 100;
private static final int MATCH_ONE_CODE = 101;
private static UriMatcher uriMatcher;
private SQLiteDatabase db;
private DBOpenHelper openHelper;
private Cursor cursor = null;
//数据改变后指定通知的Uri
private static final Uri NOTIFY_URI = Uri.parse(“content://” + AUTHORITY + “/student”);

//在静态代码块中添加要匹配的 Uri
static {
//匹配不成功返回NO_MATCH(-1)
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, “student”, MATCH_ALL_CODE);// 匹配记录集合
uriMatcher.addURI(AUTHORITY, “student/#”, MATCH_ONE_CODE);// 匹配单条记录
}

@Override
public boolean onCreate() {
openHelper = new DBOpenHelper(getContext());
db = openHelper.getWritableDatabase();
return false;
}

//删除
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
switch (uriMatcher.match(uri)) {
//uriMatcher通过语句uriMatcher.addURI(AUTHORITY, “student”, MATCH_ALL_CODE);加入的Uri为content://com.example.studentProvider/student,如果传进来的uri跟这个uri匹配成功,则uri matched.match(uri)会返回MATCH_ALL_CODE,我们可以在这里对这个ContentProvider中的数据库进行删除等操作。这里如果匹配成功,我们将删除所有的数据
case MATCH_ALL_CODE:
int count=db.delete(“personData”, null, null);
if(count>0){
notifyDataChanged();
return count;
}
break;
case MATCH_ONE_CODE:
// 这里可以做删除单条数据的操作。
break;
default:
throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
}
return 0;
}

@Override
public String getType(Uri uri) {
return null;
}

//插入
@Override
public Uri insert(Uri uri, ContentValues values) {
//如果传入的uri为content://com.example.studentProvider/student则能匹配成功,在这里进行插入操作
int match=uriMatcher.match(uri);
if(match!=MATCH_ALL_CODE){
throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
}

long rawId = db.insert(“personData”, null, values);
Uri insertUri = ContentUris.withAppendedId(uri, rawId);
if(rawId>0){
notifyDataChanged();
return insertUri;
}
return null;
}

//查询
@Override
public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder) {
switch (uriMatcher.match(uri)) {
//如果传入的uri为content://com.example.studentProvider/student则能匹配成功,就根据条件查询数据并将查询出的cursor返回
case MATCH_ALL_CODE:
cursor = db.query(“personData”, null, null, null, null, null, null);
break;
case MATCH_ONE_CODE:
// 根据条件查询一条数据…
break;
default:
throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
}
return cursor;
}

//更新
@Override
public int update(Uri uri, ContentValues values, String selection,String[] selectionArgs) {
switch (uriMatcher.match(uri)) {
case MATCH_ONE_CODE:
long age = ContentUris.parseId(uri);
selection = “age = ?”;
selectionArgs = new String[] { String.valueOf(age) };
int count = db.update(“personData”, values, selection,selectionArgs);
if(count>0){
notifyDataChanged();
}
break;
case MATCH_ALL_CODE:
// 如果有需求的话,可以对整个表进行操作
break;
default:
throw new IllegalArgumentException(“Unkwon Uri:” + uri.toString());
}
return 0;
}

//通知指定URI数据已改变
private void notifyDataChanged() {
getContext().getContentResolver().notifyChange(NOTIFY_URI, null);
}
}
在我们的PeopleContentProvider中,我们用到了DBHelper类,代码如下:
public class DBHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = “provider.db”;
private static final int DATABASE_VERSION = 1;

public DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
String sql = “CREATE TABLE IF NOT EXISTS personData” +
“(_id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR, age INTEGER, info TEXT)”;
db.execSQL(sql);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(“DROP TABLE IF EXISTS personData”);
onCreate(db);
}
}

至此,ContentProvider类就定义好了。注意这个案例牵扯到两个项目,一个是包含我们自定义的ContentProvider,另一个项目是访问这个包含ContentProvider项目中的数据。
写好PeopleContentProvider之后,千万不要忘了在清单文件中注册:
< provider
android:name=“com.example.contentproviderpractice.PeopleContentProvider”
android:authorities=“com.example.student”
android:exported=“true” >
< /provider>
这里的authorities是唯一标识内容提供者的,为内容提供者指定一个唯一的标识,这样其他应用才可以唯一获取此provider,exported的值为[flase|true],当为true时: 当前提供者可以被其它应用使用,此时任何应用都可以使用Provider通过URI 来获得它,也可以通过相应的权限来使用Provider;当为false时: 当前提供者不能被其它应用使用,默认为true。
注册好之后运行到手机上,此时其它的应用就可以通过ContentResolver来访问这个PeopleContentProvider了。

然后我们再新建另一个项目B,在MainActivity的代码如下:
public class MainActivity extends Activity implements OnClickListener {
private ContentResolver contentResolver;
private ListView lvShowInfo;
private MyAdapter adapter;
private Button btnInit;
private Button btnInsert;
private Button btnDelete;
private Button btnUpdate;
private Button btnQuery;
private Cursor cursor;

private static final String AUTHORITY = “com.example.studentProvider”;
private static final Uri STUDENT_ALL_URI = Uri.parse(“content://” + AUTHORITY + “/student”);

private Handler handler=new Handler(){
public void handleMessage(android.os.Message msg) {
//当ContentObsever收到数据改变的消息后,通过handler做一些操作,比方更新listview等,根据业务需求来定…
cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
adapter.changeCursor(cursor);
};
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lvShowInfo=(ListView) findViewById(R.id.lv_show_info);
initData();
}

private void initData() {
btnInit=(Button) findViewById(R.id.btn_init);
btnInsert=findViewById(R.id.btn_insert);
btnDelete=findViewById(R.id.btn_delete);
btnUpdate=findViewById(R.id.btn_update);
btnQuery=findViewById(R.id.btn_query);
btnInit.setOnClickListener(this);
btnInsert.setOnClickListener(this);
btnDelete.setOnClickListener(this);
btnUpdate.setOnClickListener(this);
btnQuery.setOnClickListener(this);
//注册内容观察者
contentResolver = getContentResolver();
contentResolver.registerContentObserver( STUDENT_ALL_URI,true,new PersonOberserver(handler));
//adapter
adapter=new MyAdapter(MainActivity.this,cursor);
lvShowInfo.setAdapter(adapter);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
//初始化
case R.id.btn_init:
ArrayList< Student> students = new ArrayList< Student>();
Student student1 = new Student(“苍老师”,25,“一个会教学的好老师”);
Student student2 = new Student(“柳岩”,26,“大方”);
Student student3 = new Student(“杨幂”,27,“漂亮”);
Student student4 = new Student(“张馨予”,28,“不知道怎么评价”);
Student student5 = new Student(“范冰冰”,29,"。。。");

students.add(student1);
students.add(student2);
students.add(student3);
students.add(student4);
students.add(student5);

for (Student Student : students) {
ContentValues values = new ContentValues();
values.put(“name”, Student.getName());
values.put(“age”, Student.getAge());
values.put(“introduce”, Student.getIntroduce());
contentResolver.insert(STUDENT_ALL_URI, values);
}
break;
//增
case R.id.btn_insert:
Student student = new Student(“小明”, 26, “帅气男人”);
//实例化一个ContentValues对象
ContentValues contentValues = new ContentValues(); contentValues.put(“name”,student.getName()); contentValues.put(“age”,student.getAge()); contentValues.put(“introduce”,student.getIntroduce());
//这里的uri和ContentValues对象经过一系列处理之后会传到ContentProvider中的insert方法中,在我们自定义的ContentProvider中进行匹配操作 contentResolver.insert(STUDENT_ALL_URI,insertContentValues);
break;
//删
case R.id.btn_delete:
//删除所有条目
contentResolver.delete(STUDENT_ALL_URI, null, null);
//删除_id为1的记录
Uri delUri = ContentUris.withAppendedId(STUDENT_ALL_URI,1);
contentResolver.delete(delUri, null, null);
break;
//改
case R.id.btn_update:
ContentValues contentValues = new ContentValues();
contentValues.put(“introduce”,“性感”);
//更新数据,将age=26的条目的introduce更新为"性感"(原来age=26的introduce为"大方"),生成的Uri为:content://com.example.studentProvider/student/26
Uri updateUri = ContentUris.withAppendedId(STUDENT_ALL_URI,26); contentResolver.update(updateUri,contentValues, null, null);
break;
//查
case R.id.btn_query:
Cursor cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
//此处用到了CursorAdapter
adapter=new MyAdapter(this,cursor);
lvShowInfo.setAdapter(adapter);
cursor = contentResolver.query(STUDENT_ALL_URI, null, null, null,null);
adapter.changeCursor(cursor);
break;
}
}
}
可以看出,如果我们想操作ContentProvider,必须要知道该内容提供者的Uri,在正确得到Uri之后,就可以通过ContentResolver对象来操作ContentProvider中的数据了。假如你需要插入数据只需要调用contentResolver.insert(uri, contentValues);把正确的uri和ContentValues键值对传过去就行了,执行这句话系统就会根据我们提供的uri找到对应的ContentProvider,因为我们的uri中包含了authority(主机等各种信息),得到对应的ContentProvider后将调用ContentResolver的与之对应的增删改查方法,并将参数通过ContentResolver的增删改查方法传递到ContentProvider中,从而调用ContentProvider对应的方法。

项目B中,PersonObserver的代码如下:
public class PersonOberserver extends ContentObserver {
private Handler handler;

public PersonOberserver(Handler handler) {
super(handler);
this.handler=handler;
}

//ContentResolver观察到数据变化后,自动回调该方法
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
//向handler发送消息,在handler中进行数据更新后的操作
Message msg = new Message();
handler.sendMessage(msg);
}
我们在构造方法中接收了Handler,然后当监听到指定的Uri的数据变化时就会通过Handler消息机制发送一条消息,然后的操作就由我们自行完成了。

最后,我们来理一理整个操作的运行流程:首先有两个项目,一个是有ContentProvider的项目A,在这个ContentProvider中初始化了一个数据库,我们的目的就是在另一个项目B中来操作项目A中ContentProvider中的数据,例如插入一条数据,查询等。对于怎么在另一个项目中操作ContentProvider中的数据,是通过ContentResolver(内容解析者)对象来操作的,假如我们要进行insert操作,那么需要调用ContentResolver的insert(uri, ContentValues);将Uri和ContentValues对象经过一系列操作传递到ContentProvider的中,然后在ContentProvider会对这个Uri进行匹配,如果匹配成功则按照我们的需求去执行相应的操作,如:插入数据、查询数据等。
整个流程可以简单理解为:app B中想要操作appA中的数据,因此app A提供一个ContentProvider,在app B中通过ContentResolver的增删改查方法,实际上调用了app A中ContentProvider的增删改查方法,增删改查操作后数据发生了变化,自动回调app B中ContentObserver类中的onChange方法,从而在app B中进行数据变化后的操作。(这一切交互的基础是同一个uri,app A中ContentProvider提供该uri,app B中通过ContentResolver调用增删改查方法是都要传入这个uri,同时注册ContentObserver时也要传入这个uri)

下面我们通过一张图再来说一下这个过程:
在这里插入图片描述
从图中可以看出在OtherApplication中注册了ContentObserver之后,当Application1中的数据库发生了变化时,只需要在ContentProvider中调用ContentResolver的notifyChange(Uri, ContentObserver observer),由于在OtherApplication中注册了ContentObserver(注册时用的Uri和ContentProvider中发生变化的Uri一样),因此在ContentObserver中会收到这个变化信息,它就可以将这个消息通过Handler发送给OtherApplication。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-10-15 11:54:32  更:2021-10-15 11:56:49 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:28:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码