ContentProvider域名替换小工具
开发项目域名想怎么换就怎么换,就是这么任性!
这是一个很有意思的小工具!
这是一个方便开发人员和测试人员的小工具!!
吐槽:
一直在做Android开发,一直总有一个问题存在:做自己公司的apk开发时,线上包和测试包不可兼得~总是在 卸载、安装、卸载、安装。。。的循环操作。很是麻烦,而且另外一个不得不正视的问题就是:只要跟服务端人员进行联调时,就得修改项目中的测试域名,重新打包,也是够麻烦的。最近报名了公司的一个服务,就不得不使用线上包了,被逼无奈想起了这个小设计。
原理:
使用ContentProvider数据共享~
展示图:
设计思路及源码解析:
1.前期准备
a.ContentProvider在android中的作用是对外共享数据, 也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中的数据进行添删改查。
当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法:
public class PersonContentProvider extends ContentProvider{
public boolean onCreate()
public Uri insert(Uri uri, ContentValues values)
public int delete(Uri uri, String selection, String[] selectionArgs)
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
public String getType(Uri uri)
}
b.第二步需要在AndroidManifest.xml使用对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider ,ContentProvider采用了authorities(主机名/域名)对它进行唯一标识,你可以把ContentProvider看作是一个网 站(想想,网站也是提供数据者),authorities 就是他的域名
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xc1217.contentprovider"> <application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activity.UrlListActivity"
android:theme="@style/AppTheme.NoActionBar">
</activity> <provider android:name=".db.MyContentProvider"
android:authorities="com.xc1217.contentprovider.myprovider"
android:exported="true"/>
</application> </manifest>
2.数据库设计
/**
* Created by ding on 2016/11/15.
*/
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DBNAME = "1217provider.db"; //数据库名称
private static final int DBVER = 1;//数据库版本 public DBOpenHelper(Context context) {
super(context, DBNAME, null, DBVER);
} @Override
public void onCreate(SQLiteDatabase db) {
// id 主键id, url 路径 selected 1 选中,0 未选中
String sql = "CREATE TABLE student (id integer primary key autoincrement, url varchar(500), selected int)";
String sql2 = "CREATE TABLE coach (id integer primary key autoincrement, url varchar(500), selected int)";
db.execSQL(sql);//执行有更改的sql语句
db.execSQL(sql2);
initDb(db);
} private void initDb(SQLiteDatabase db) {
String sql = "INSERT INTO student VALUES (1,'http://www.1217.com/', 0)";
String sq2 = "INSERT INTO student VALUES (2,'http://www.1217.com/', 1)";
String sq3 = "INSERT INTO student VALUES (3,'http://www.1217.com/', 0)";
String sq4 = "INSERT INTO student VALUES (4,'http://www.1217.com/', 0)";
db.execSQL(sql);
db.execSQL(sq2);
db.execSQL(sq3);
db.execSQL(sq4);
initAddDbCoach(db);
} private void initAddDbCoach(SQLiteDatabase db) {
String sql = "INSERT INTO coach VALUES (1,'http://www.1217.com/', 0)";
String sq2 = "INSERT INTO coach VALUES (2,'http://www.1217.com/', 1)";
String sq3 = "INSERT INTO coach VALUES (3,'http://www.1217.com/', 0)";
String sq4 = "INSERT INTO coach VALUES (4,'http://www.1217.com/', 0)";
db.execSQL(sql);
db.execSQL(sq2);
db.execSQL(sq3);
db.execSQL(sq4); } @Override
public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {
db.execSQL("DROP TABLE IF EXISTS student");
db.execSQL("DROP TABLE IF EXISTS coach");
onCreate(db);
} }
3.继承ContentProvider并重写方法
/**
* Created by ding on 2016/11/15.
*/
public class MyContentProvider extends ContentProvider {
private DBOpenHelper dbOpenHelper;
//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
private static final UriMatcher MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
private static final int STUDENTS = 1;
private static final int STUDENT = 2;
private static final int COACHS = 3;
private static final int COACH = 4; static {
//如果match()方法匹配content://com.xc1217.contentprovider.myprovider/student路径,返回匹配码为1
MATCHER.addURI("com.xc1217.contentprovider.myprovider", "student", STUDENTS);
//如果match()方法匹配content://com.xc1217.contentprovider.myprovider/student/123路径,返回匹配码为2
MATCHER.addURI("com.xc1217.contentprovider.myprovider", "student/#", STUDENT);//#号为通配符
MATCHER.addURI("com.xc1217.contentprovider.myprovider", "coach", COACHS);
MATCHER.addURI("com.xc1217.contentprovider.myprovider", "coach/#", COACH);//#号为通配符
} @Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
int count = 0;
long id = 0;
String where = "";
switch (MATCHER.match(uri)) {
case STUDENTS:
count = db.delete("student", selection, selectionArgs);
return count;
case STUDENT:
//ContentUris类用于获取Uri路径后面的ID部分
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
count = db.delete("student", where, selectionArgs);
return count;
case COACHS:
count = db.delete("coach", selection, selectionArgs);
return count;
case COACH:
//ContentUris类用于获取Uri路径后面的ID部分
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
count = db.delete("coach", where, selectionArgs);
return count;
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
} /**
* 该方法用于返回当前Url所代表数据的MIME类型。
* 如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头
* 如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头
*/
@Override
public String getType(Uri uri) {
switch (MATCHER.match(uri)) {
case STUDENTS:
return "vnd.android.cursor.dir/student"; case STUDENT:
return "vnd.android.cursor.item/student";
case COACHS:
return "vnd.android.cursor.dir/coach"; case COACH:
return "vnd.android.cursor.item/coach"; default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
} @Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
long rowid = 0;
Uri insertUri = null;//得到代表新增记录的Uri
switch (MATCHER.match(uri)) {
case STUDENTS:
rowid = db.insert("student", "url", values);
insertUri = ContentUris.withAppendedId(uri, rowid);//得到代表新增记录的Uri
this.getContext().getContentResolver().notifyChange(uri, null);
return insertUri;
case COACHS:
rowid = db.insert("coach", "url", values);
insertUri = ContentUris.withAppendedId(uri, rowid);//得到代表新增记录的Uri
this.getContext().getContentResolver().notifyChange(uri, null);
return insertUri; default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
} @Override
public boolean onCreate() {
this.dbOpenHelper = new DBOpenHelper(this.getContext());
return false;
} @Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
long id = 0;
String where = null;
switch (MATCHER.match(uri)) {
case STUDENTS:
return db.query("student", projection, selection, selectionArgs, null, null, sortOrder);
case STUDENT:
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
return db.query("student", projection, where, selectionArgs, null, null, sortOrder);
case COACHS:
return db.query("coach", projection, selection, selectionArgs, null, null, sortOrder);
case COACH:
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
return db.query("coach", projection, where, selectionArgs, null, null, sortOrder);
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
} @Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
int count = 0;
long id = 0;
String where = null;
switch (MATCHER.match(uri)) {
case STUDENTS:
count = db.update("student", values, selection, selectionArgs);
return count;
case STUDENT:
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
count = db.update("student", values, where, selectionArgs);
return count;
case COACHS:
count = db.update("coach", values, selection, selectionArgs);
return count;
case COACH:
id = ContentUris.parseId(uri);
where = "id = " + id;
if (selection != null && !"".equals(selection)) {
where = selection + " and " + where;
}
count = db.update("coach", values, where, selectionArgs);
return count;
default:
throw new IllegalArgumentException("Unkwon Uri:" + uri.toString());
}
}
}
4.MainActivity. java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); findViewById(R.id.lay_student).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) { Intent intent = new Intent(MainActivity.this, UrlListActivity.class);
intent.putExtra("table", "student");
startActivity(intent);
}
});
findViewById(R.id.lay_coach).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, UrlListActivity.class);
intent.putExtra("table", "coach");
startActivity(intent);
}
}); }
}
5.UrlListActivity.java
public class UrlListActivity extends AppCompatActivity {
private static final String TAG = "UrlListActivity";
String uriString = "content://com.xc1217.contentprovider.myprovider/";
private String table = "";
private List<UrlBean> mList = new ArrayList<UrlBean>();
private ListView listView;
private int position; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_url_list);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Intent intent = getIntent();
if (intent != null) {
table = intent.getStringExtra("table");
if (TextUtils.isEmpty(table))
return;
setTitle(table);
uriString = uriString + table;
}
initUI();
getUrlList(); } private void initUI() {
listView = (ListView) findViewById(R.id.list_url); findViewById(R.id.btn_add).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
showAddPop();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
});
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
position = i;
showClickPop();
}
});
} private void showAddPop() {
final EditText editText = new EditText(this);
editText.setText("http://www.1217.com/");
editText.setHint("http://www.1217.com/");
final AlertDialog alertDialog = new AlertDialog.Builder(this).
setTitle("添加")
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
}
})
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String url = editText.getText().toString();
insertUrl(url);
}
}).setView(editText).create();
alertDialog.show();
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(new View
.OnClickListener() {
@Override
public void onClick(View v) {
String url = editText.getText().toString();
if (TextUtils.isEmpty(url)) {
Toast.makeText(UrlListActivity.this, "不能为空", Toast.LENGTH_SHORT).show();
return;
}
insertUrl(url);
alertDialog.dismiss();
getUrlList();
}
});
} private void showClickPop() {
final String[] arrayFruit = new String[]{"选择", "删除"}; Dialog alertDialog = new AlertDialog.Builder(this).
setTitle("编辑")
.setItems(arrayFruit, new DialogInterface.OnClickListener() { @Override
public void onClick(DialogInterface dialog, int which) { switch (which) {
case 0:
for (int i = 0; i < mList.size(); i++) {
UrlBean urlBean = mList.get(i);
urlBean.selected = (position == i ? 1 : 0);
update(urlBean);
}
UrlAdapter urlAdapter = new UrlAdapter(UrlListActivity.this, mList);
listView.setAdapter(urlAdapter);
break;
case 1:
int id = mList.get(position).id;
deleteById(id);
getUrlList();
break;
default:
break;
} }
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
}
}).
create();
alertDialog.show();
} //往内容提供者添加数据
public void insertUrl(String url) {
try {
ContentResolver contentResolver = this.getContentResolver();
Uri insertUri = Uri.parse(uriString);
ContentValues values = new ContentValues();
values.put("url", url);
values.put("selected", 0);
Uri uri = contentResolver.insert(insertUri, values);
Log.i(TAG, uri.toString());
} catch (Exception e) {
e.printStackTrace();
}
} //更新内容提供者中的数据
public void update(UrlBean urlBean) {
try {
ContentResolver contentResolver = this.getContentResolver();
Uri updateUri = Uri.parse(uriString + "/" + urlBean.id);
ContentValues values = new ContentValues();
values.put("url", urlBean.url);
values.put("selected", urlBean.selected);
contentResolver.update(updateUri, values, null, null);
} catch (Exception e) {
e.printStackTrace();
}
} //从内容提供者中删除数据
public void deleteById(Integer id) {
try {
ContentResolver contentResolver = this.getContentResolver();
Uri deleteUri = Uri.parse(uriString + "/" + id);
contentResolver.delete(deleteUri, null, null);
} catch (Exception e) {
e.printStackTrace();
}
} //获取内容提供者中的数据
public void getUrlList() {
try {
ContentResolver contentResolver = this.getContentResolver();
Uri selectUri = Uri.parse(uriString);
Cursor cursor = contentResolver.query(selectUri, null, null, null, null);
if (cursor == null)
return;
mList.clear();
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndex("id"));
String url = cursor.getString(cursor.getColumnIndex("url"));
int selected = cursor.getInt(cursor.getColumnIndex("selected"));
Log.i(TAG, "id=" + id + ",url=" + url + ",selected=" + selected);
UrlBean urlBean = new UrlBean(id, url, selected);
mList.add(urlBean);
}
UrlAdapter urlAdapter = new UrlAdapter(this, mList);
listView.setAdapter(urlAdapter);
} catch (Exception e) {
e.printStackTrace();
}
} }
6.UrlAdapter.java
public class UrlAdapter extends BaseAdapter {
private Context mContext;
private List<UrlBean> mList; public UrlAdapter(Context c, List<UrlBean> l) {
// TODO Auto-generated constructor stub
mContext = c;
mList = l;
} @Override
public int getCount() {
// TODO Auto-generated method stub
return mList.size();
} @Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return mList.get(arg0);
} @Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return arg0;
} @Override
public View getView(final int i, View view, ViewGroup arg2) {
ViewHoldel holdel;
if (view == null) {
holdel = new ViewHoldel();
view = LayoutInflater.from(mContext).inflate(R.layout.url_item,
null);
holdel.tvContent = (TextView) view.findViewById(R.id.tv_content);
holdel.imageView = (ImageView) view.findViewById(R.id.imv_right);
view.setTag(holdel);
} else {
holdel = (ViewHoldel) view.getTag();
}
holdel.tvContent.setText(mList.get(i).url);
if (mList.get(i).selected == 1) {
holdel.imageView.setVisibility(View.VISIBLE);
} else {
holdel.imageView.setVisibility(View.GONE);
} return view;
} class ViewHoldel {
TextView tvContent;
ImageView imageView;
} }
7.url_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_horizontal_margin"
android:orientation="horizontal"> <TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text=",,"
android:textSize="15sp"/>
<ImageView
android:id="@+id/imv_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/icon_true_big"/>
</LinearLayout>
重点来了!!!
开发人员接入代码,替换域名:
代码加入到 Application 初始化方法中;根据需要修改下代码即可。
private void changeServerHostFromProvider() {
Uri uri = Uri.parse("content://com.xc1217.contentprovider.myprovider/student");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String url = cursor.getString(cursor.getColumnIndex("url"));
int selected = cursor.getInt(cursor.getColumnIndex("selected"));// 1 选中,否则未选中
if (selected == 1 && !TextUtils.isEmpty(url)) {
Urls.BASE_URL_TEST = url;
Urls.BASE_URL_ONLINE = url; return;
}
}
cursor.close();
}
}
ContentProvider域名替换小工具的更多相关文章
- x01.TextProc: 两三分钟完成的一个小工具
在工作中,遇到这么个问题,需要将 Excel 表中类似 2134-1234-4456 的商品编号输入到单位的程序中,而程序只认 213412344456 这种没有 ‘-’ 的输入.数量比较多,一笔一笔 ...
- PHP API接口测试小工具
前端时间给手机客户端做接口,当时弱爆了,写完API接口后,也不怎么测试,最后是等客户端调用的时候检验API的正确性. 后面利用PHP的curl实现Post请求,检验API接口的正确性:配合前面做的一个 ...
- 【开源一个小工具】一键将网页内容推送到Kindle
最近工作上稍微闲点,这一周利用下班时间写了一个小工具,其实功能挺简单但也小折腾了会. 工具名称:Simple Send to Kindle Github地址:https://github.com/zh ...
- 三个 DAL 相关的Java代码小工具
最近在做 DAL (Data Access Layer 数据访问层) 的服务化,发现有不少地方是人工编写比较繁琐的,因此写了几个小工具来完成. 1. 从 DAO 类自动生成 CoreService ...
- 撸一个JS正则小工具
写完正则在浏览器上检测自己写得对不对实在是不方便,于是就撸了一个JS正则小demo出来. demo demo展示 项目地址 代码部分 首先把布局样式先写好. <!DOCTYPE html> ...
- C#使用 SQLite 数据库 开发的配置过程及基本操作类,实例程序:工商银行贵金属行情查看小工具
--首发于博客园, 转载请保留此链接 博客原文地址 本文运行环境: Win7 X64, VS2010 1. SQLite 的优点: SQLite 是一款轻型数据库,开发包只有十几M, 相对于 MSS ...
- 【游戏开发】Excel表格批量转换成CSV的小工具
一.前言 在工作的过程中,我们有时可能会面临将Excel表格转换成CSV格式文件的需求.这尤其在游戏开发中体现的最为明显,策划的数据文档大多是一些Excel表格,且不说这些表格在游戏中读取的速度,但就 ...
- 如何高效的编写与同步博客 (.NET Core 小工具实现)
一.前言 写博客,可以带给我们很多好处,比如可以让我们结识更多志同道合的人:在写博客过程中去查技术资料或者实践可以让我们对知识的掌握和理解更加深刻:通过博客分享能帮助他人收获分享的快乐等等.写博客真的 ...
- 基于百度通用翻译API的一个翻译小工具
前几天写了一个简单的翻译小工具,是基于有道翻译的,不过那个翻译接口有访问限制,超过一定次数后会提示访问过于频繁,偶然发现百度翻译API如果月翻译字符少于200万是不收取费用的,所以就注册了一个百度开发 ...
随机推荐
- NuGet镜像上线试运行
为解决国内访问NuGet服务器速度不稳定的问题,我们用阿里云服务器搭建了一个NuGet镜像,目前已上线试运行. 使用NuGet镜像源的方法如下: 1)NuGet镜像源地址:https://nuget. ...
- JS核心系列:浅谈函数的作用域
一.作用域(scope) 所谓作用域就是:变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的. function scope(){ var foo = "global&quo ...
- 从中间件的历史来看移动App开发的未来
在移动开发领域我们发现一个很奇怪的现象:普通菜鸟新手经过3个月的培训就可以拿到 8K 甚至上万的工作:在北京稍微有点工作经验的 iOS 开发,就要求 2 万一个月的工资.不知道大家是否想过:移动应用开 ...
- iOS开发系列--Swift语言
概述 Swift是苹果2014年推出的全新的编程语言,它继承了C语言.ObjC的特性,且克服了C语言的兼容性问题.Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在 ...
- fir.im Weekly - 关于 iOS10 适配、开发、推送的一切
"小程序"来了,微信变成名副其实的 Web OS,新一轮的Web App 与Native App争论四起.程序员对新技术永远保持灵敏的嗅觉和旺盛的好奇心,@李锦发整理了微信小程序资 ...
- arcgis api for js入门开发系列六地图分屏对比(含源代码)
上一篇实现了demo的地图标绘模块,本篇新增地图地图分屏对比模块,截图如下(源代码见文章底部): 对效果图的简单介绍一下,在demo只采用了两分屏对比,感兴趣的话,可以在两分屏的基础上拓展,修改css ...
- Git使用详细教程(二)
分支 其实在项目clone下来后就有一个分支,叫做master分支.新建分支的步骤:右键项目→Git→Repository...→Branches... master分支应该是最稳定的,开发的时候,建 ...
- SQL 数据优化索引建suo避免全表扫描
首先什么是全表扫描和索引扫描?全表扫描所有数据过一遍才能显示数据结果,索引扫描就是索引,只需要扫描一部分数据就可以得到结果.如果数据没建立索引. 无索引的情况下搜索数据的速度和占用内存就会比用索引的检 ...
- Linux.NET实战手记—自己动手改泥鳅(下)
在上回合中,我们不痛不痒的把小泥鳅的数据库从只能供在Windows下运行的Access数据库改为支持跨平台的MYSQL数据库,毫无营养的修改,本回合中,我们将把我们修改后得来的项目往Linux中部署. ...
- CacheManager:–个通用缓存接口抽象类库
CacheManager是–个缓存通用接口抽象类库,它支持各种高速缓存提供者,例如Memcache,Redis,并且有许多先进的功能特性.具体可以访问官方网站 http://cachemanager ...