【声明】

欢迎转载,但请保留文章原始出处→_→

生命壹号:http://www.cnblogs.com/smyhvae/

文章来源:http://www.cnblogs.com/smyhvae/p/4108017.html

【正文】


一、ContentProvider简介:

ContentProvider内容提供者(四大组件之一)主要用于在不同的应用程序之间实现数据共享的功能

ContentProvider可以理解为一个Android应用对外开放的接口,只要是符合它所定义的Uri格式的请求,均可以正常访问执行操作。其他的Android应用可以使用ContentResolver对象通过与ContentProvider同名的方法请求执行,被执行的就是ContentProvider中的同名方法。所以ContentProvider有很多对外可以访问的方法,在ContentResolver中均有同名的方法,是一一对应的,来看 下面这一张图:

Android附带了许多有用的ContentProvider,但是本文暂时不涉及到这么多(本文将学习如何创建自己的ContentProvider)。Android中自带的ContentProvider包括:

  • Browser:存储如浏览器的信息。
  • CallLog:存储通话记录等信息。
  • Contacts Provider:存储联系人(通讯录)等信息。
  • MediaStore:存储媒体文件的信息。
  • Settings:存储设备的设置和首选项信息。

此外,还有日历、

ContentProvider的方法:

如果要创建自己的内容提供者,需要新建一个类继承抽象类ContentProvider,并重写其中的抽象方法。抽象方法如下:

boolean onCreate()   
初始化提供者 Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
查询数据,返回一个数据Cursor对象。其中参数selection和selectionArgs是外部程序提供的查询条件 Uri insert(Uri uri, ContentValues values)
插入一条数据。参数values是需要插入的值 int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
根据条件更新数据 int delete(Uri uri, String selection, String[] selectionArgs)
根据条件删除数据 String getType(Uri uri)
返回MIME类型对应内容的URI

除了onCreate()和getType()方法外,其他的均为CRUD操作,这些方法中,Uri参数为与ContentProvider匹配的请求Uri,剩下的参数可以参见SQLite的CRUD操作,基本一致。 

备注:还有两个非常有意思的方法,必须要提一下,call()和bulkInsert()方法,使用call,理论上可以在ContentResolver中执行ContentProvider暴露出来的任何方法,而bulkInsert()方法用于插入多条数据。

Uri:

在Android中,Uri是一种比较常见的资源访问方式。而对于ContentProvider而言,Uri也是有固定格式的:<srandard_prefix>://<authority>/<data_path>/<id>

  • <srandard_prefix>:ContentProvider的srandard_prefix始终是content://。
  • <authority>:ContentProvider的名称。
  • <data_path>:请求的数据类型。
  • <id>:指定请求的特定数据。

在ContentProvider的CRUD操作,均会传递一个Uri对象,通过这个对象来匹配对应的请求。那么如何确定一个Uri执行哪项操作呢?需要用到一个UriMatcher对象,这个对象用来帮助内容提供者匹配Uri。它所提供的方法非常简单,仅有两个:

  • void addURI(String authority,String path,int code):添加一个Uri匹配项,authority为AndroidManifest.xml中注册的ContentProvider中的authority属性;path为一个路径,可以设置通配符,#表示任意数字,*表示任意字符;code为自定义的一个Uri代码。
  • int match(Uri uri):匹配传递的Uri,返回addURI()传递的code参数。

二、代码举例:

最终所有工程文件的目录结构如下:

PersonDao是增删改查数据库的工具类,并在PersonContentProvider中得到调用。DBHelper用于初始化SQLite数据库。

PersonContentProvider用于向外提供增删改查的接口。并最终在ContentResolverTest的MyTest.java中进行单元测试,实现CRUD。

本文的核心类是:PersonContentProvider和MyTest

下面来看一下具体的实现步骤。

新建工程文件ContetProviderTest01。

(1)新建类PersonDao:用于进行对SQLite的CRUD操作。代码如下:

PersonDao.java:

 package com.example.contentprovidertest01.dao;

 import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import com.example.contentprovidertest01.db.DBHelper; public class PersonDao {
private DBHelper helper = null; public PersonDao(Context context) {
helper = new DBHelper(context);
} //方法:插入操作,返回的long类型为:插入当前行的行号
public long insertPerson(ContentValues values) {
long id = -1;
SQLiteDatabase database = null;
try {
database = helper.getWritableDatabase();
id = database.insert("person", null, values);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (database != null) {
database.close();
}
}
return id;
} public int deletePerson(String whereClause, String[] whereArgs) {
int count = -1;
SQLiteDatabase database = null;
try {
database = helper.getWritableDatabase();
count = database.delete("person", whereClause, whereArgs);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (database != null) {
database.close();
}
}
return count;
} public int updatePerson(ContentValues values, String whereClause,
String[] whereArgs) {
SQLiteDatabase database = null;
int count = -1;
try {
database = helper.getWritableDatabase();
count = database.update("person", values, whereClause, whereArgs);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != database) {
database.close();
}
}
return count;
} public Cursor queryPersons(String selection, String[] selectionArgs) {
SQLiteDatabase database = null;
Cursor cursor = null;
try {
database = helper.getReadableDatabase();
cursor = database.query(true, "person", null, selection,
selectionArgs, null, null, null, null);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != database) {
// database.close();
}
}
return cursor;
} }

(2)新建类DBHelper:用于初始化SQLiate数据库

DBHelper.java:

 package com.example.contentprovidertest01.db;

 import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; public class DBHelper extends SQLiteOpenHelper { private static String name = "mydb.db"; // 数据库的名字
private static int version = 1; // 数据库的版本 public DBHelper(Context context) {
super(context, name, null, version);
} @Override
public void onCreate(SQLiteDatabase db) {
// 只能支持基本数据类型:varchar int long float boolean text blob clob
// 建表语句执行
String sql = "create table person(id integer primary key autoincrement,name varchar(64),address varchar(64))";
db.execSQL(sql);
} @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
String sql = "alter table person add sex varchar(8)";
db.execSQL(sql);
} }

(3)【核心】新建类PersonContentProvider,继承ContetProvider

PersonContentProvider.java:

 package com.example.contentprovidertest01;

 import com.example.contentprovidertest01.dao.PersonDao;

 import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log; public class PersonContentProvider extends ContentProvider { private final String TAG = "PersonContentProvider";
private PersonDao personDao = null;
private static final UriMatcher URI_MATCHER = new UriMatcher(
UriMatcher.NO_MATCH);// 默认的规则是不匹配的
private static final int PERSON = 1; // 操作单行记录
private static final int PERSONS = 2; // 操作多行记录
// 往UriMatcher中添加匹配规则。注意,这里面的url不要写错了,我就是因为写错了,半天没调试出来。哎···
static {
// 添加两个URI筛选
URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
"person", PERSONS);
// 使用通配符#,匹配任意数字
URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
"person/#", PERSON);
} public PersonContentProvider() { } @Override
public boolean onCreate() {
// 初始化一个数据持久层
personDao = new PersonDao(getContext());
//Log.i(TAG, "--->>onCreate()被调用");
return true;
} @Override
public Uri insert(Uri uri, ContentValues values) {
Uri resultUri = null;
// 解析Uri,返回Code
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSONS:
//调用数据库的访问方法
long id = personDao.insertPerson(values); //执行插入操作的方法,返回插入当前行的行号
resultUri = ContentUris.withAppendedId(uri, id);
Log.i(TAG,"--->>插入成功, id=" + id);
Log.i(TAG,"--->>插入成功, resultUri=" + resultUri.toString());
System.out.println("insert success");
break;
}
return resultUri;
} //方法:删除记录。注:参数:selection和selectionArgs是查询的条件,是由外部(另一个应用程序)传进来的
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = -1; //影响数据库的行数
try {
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSON:
// delete from student where id=?
// 单条数据,使用ContentUris工具类解析出结尾的Id
long id = ContentUris.parseId(uri);
String where_value = "id = ?";
String[] args = { String.valueOf(id) };
count = personDao.deletePerson(where_value, args);
break;
case PERSONS:
count = personDao.deletePerson(selection, selectionArgs);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
Log.i(TAG, "--->>删除成功,count=" + count);
return count;
} @Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int count = -1;
try {
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSON:
long id = ContentUris.parseId(uri);
String where_value = " id = ?";
String[] args = { String.valueOf(id) };
count = personDao.updatePerson(values, where_value, args);
break;
case PERSONS:
count = personDao
.updatePerson(values, selection, selectionArgs);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
Log.i(TAG, "--->>更新成功,count=" + count);
return count;
} @Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Cursor cursor = null;
try {
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSON:
long id = ContentUris.parseId(uri);
String where_value = " id = ?";
String[] args = { String.valueOf(id) };
cursor = personDao.queryPersons(where_value, args);
break;
case PERSONS:
cursor = personDao.queryPersons(selection, selectionArgs);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
Log.i(TAG, "--->>查询成功,Count=" + cursor.getCount());
return cursor;
} @Override
public String getType(Uri uri) {
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSON:
return "vnd.android.cursor.item/person"; // 如果是单条记录,则为vnd.android.cursor.item/
// + path case PERSONS:
return "vnd.android.cursor.dir/persons"; // 如果是多条记录,则为vnd.android.cursor.dir/
// + path
}
return null;
} @Override
public Bundle call(String method, String arg, Bundle extras) {
Log.i(TAG, "--->>" + method);
Bundle bundle = new Bundle();
bundle.putString("returnCall", "call被执行了");
return bundle;
}
}

18行的UriMatcher类的作用是:匹配内容uri,默认的规则是不匹配的。UriMatcher提供了一个addURI方法:

  • void android.content.UriMatcher.addURI(String authority, String path, int code)

这三个参数分别代表:权限、路径、和一个自定义代码。一般第一个参数是uri(包名.内容提供者的类名),第二个参数一般是数据库的表名。

27行:匹配规则的解释:*表示匹配任意字符,#表示匹配任意数字。注:如果内部的匹配规则越多,越容易访问。

138行的getType(Uri uri)方法:所有的内容提供者都必须提供的一个方法。用于获取uri对象所对应的MIME类型。

然后,每编写一个内容提供者,都必须在清单文件中进行声明。在AndroidManifest.xml中<application>节点中增加,格式如下:

<provider
android:name=".内容提供者的类名"
android:authorities="包名.内容提供者的类名" >
</provider>

第3行表示的是uri路径,毕竟Contet Provider是通过路径来访问的。

所以在本程序中,在AndroidManifest.xml的<application>节点中增加如下代码:

<provider
  android:name=".PersonContentProvider"
  android:authorities="com.example.contentprovidertest01.PersonContentProvider" >
</provider>

(4)单元测试类:

这里需要涉及到另外一个知识:ContentResolver内容访问者

要想访问ContentProvider,则必须使用ContentResolver。可以通过ContentResolver来操作ContentProvider所暴露处理的接口。一般使用Content.getContentResolver()方法获取ContentResolver对象。第一段中已经提到:ContentProvider有很多对外可以访问的方法,在ContentResolver中均有同名的方法,是一一对应的。所以它也存在insert、query、update、delete等方法。于是单元测试类可以这样写:(注:单元测试如果不清楚,可以参考另外一篇文章:JUnit单元测试的使用

MyTest.java:

 package com.example.contentresolvertest;

 import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.test.AndroidTestCase;
import android.util.Log; public class MyTest extends AndroidTestCase { public MyTest() {
// TODO Auto-generated constructor stub } public void calltest() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
Bundle bundle = contentResolver.call(uri, "method", null, null);
String returnCall = bundle.getString("returnCall");
Log.i("main", "-------------->" + returnCall);
} //测试方法:向数据库中添加记录。如果之前没有数据库,则会自动创建
public void insert() {
// 使用内容解析者ContentResolver访问内容提供者ContentProvider
ContentResolver contentResolver = getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put("name", "生命贰号");
values.put("address", "湖北");
// content://authorities/person
// http://
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
contentResolver.insert(uri, values);
} //测试方法:删除单条记录。如果要删除所有记录:content://com.example.contentprovidertest01.PersonContentProvider/person
public void delete() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person/2");//删除id为1的记录
contentResolver.delete(uri, null, null);
} //测试方法:根据条件删除记录。
public void deletes() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
String where = "address=?";
String[] where_args = { "HK" };
contentResolver.delete(uri, where, where_args); //第二个参数表示查询的条件"address=?",第三个参数表示占位符中的具体内容
} //方法:根据id修改记录。注:很少有批量修改的情况。
public void update() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person/2");
ContentValues values = new ContentValues();
values.put("name", "李四");
values.put("address", "上海");
contentResolver.update(uri, values, null, null);
} //方法:根据条件来修改记录。
public void updates() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person/student");
ContentValues values = new ContentValues();
values.put("name", "王五");
values.put("address", "深圳");
String where = "address=?";
String[] where_args = { "beijing" };
contentResolver.update(uri, values, where, where_args);
} //测试方法:查询所有记录。如果要查询单条记录:content://com.example.contentprovidertest01.PersonContentProvider/person/1
public void query() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
Cursor cursor = contentResolver.query(uri, null, null, null, null);
while (cursor.moveToNext()) {
Log.i("MyTest",
"--->>"
+ cursor.getString(cursor.getColumnIndex("name")));
}
} //测试方法:根据条件查询所有记录。
public void querys() {
ContentResolver contentResolver = getContext().getContentResolver();
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
String where = "address=?";
String[] where_args = { "深圳" };
Cursor cursor = contentResolver.query(uri, null, where, where_args,
null);
while (cursor.moveToNext()) {
Log.i("main",
"-------------->"
+ cursor.getString(cursor.getColumnIndex("name")));
}
} }

既然ContetProvider实现的是跨应用访问数据,那这个测试类Test.java就应该写在另一个应用程序中才行。于是,我们新建另外一个工程文件ContentResolverTest,在里面添加单元测试,里面的代码其实和上方的Test.java的代码是一模一样的。运行单元测试,依然能在ContentResolverTest中实现对ContentProviderTest01中的CRUD.核心在于:使用应用1中的内容解析者ContentResolver访问应用2中的内容提供者ContentProvider

现在运行ContentProviderTest01中的单元测试类:

1、运行insert()方法,实现插入操作。后台打印如下:

上图中红框部分表明,这个uri就是代表内容提供者中,person表中,id为1的数据。

此时,打开file Explorer,进行查看,发现确实多了个文件:

注意:如果SQLite中之前没有mydb.db这个数据库,当实现插入操作时,会自动创建mydb.db这个数据库,并自动创建person表(因为在PersonDao类中执行了getWritableDatabase()方法)。

现在将上图中的mydb.db导出,然后用SQLiteExpert软件打开,输入sql查询语句,就可以看到person表中的数据了:

如果再执行insert()方法,又会继续添加一条记录(id是自动增长的)。

2、运行query()方法,查询所有记录(目前一共两条记录)。后台输出效果如下:

经测试,其他方法也都是可以执行的。

事实证明,新建的另外一个工程文件ContentResolverTest中,在里面运行单元测试,也是可以执行的(单元测试的代码不变,实现的CRUD功能也一模一样),也就是说,能够对ContentProviderTest01中的SQLite进行CRUD操作。例如,运行query()方法,后台输出如下:

这样,我们的目的也就达到了。

【特别注意】

需要特别注意的是,代码中uri不要写错了,这些错误一旦发生,很难被发现。具体表现在:

1、清单文件中:

<provider
android:name=".内容提供者的类名"
android:authorities="包名.内容提供者的类名" >
</provider>

如:

        <provider
android:name=".PersonContentProvider"
android:authorities="com.example.contentprovidertest01.PersonContentProvider" >
</provider>

2、ContentProvider类中的UriMatcher中的uri:

     private static final UriMatcher URI_MATCHER = new UriMatcher(
UriMatcher.NO_MATCH);// 默认的规则是不匹配的
private static final int PERSON = 1; // 操作单行记录
private static final int PERSONS = 2; // 操作多行记录
// 往UriMatcher中添加匹配规则。注意,这里面的url不要写错了,我就是因为写错了,半天没调试出来。哎···
static {
// 添加两个URI筛选
URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
"person", PERSONS);
// 使用通配符#,匹配任意数字
URI_MATCHER.addURI("com.example.contentprovidertest01.PersonContentProvider",
"person/#", PERSON);
}

3、ContentProvider类中的getType()方法里面的代码:

     @Override
public String getType(Uri uri) {
int flag = URI_MATCHER.match(uri);
switch (flag) {
case PERSON:
return "vnd.android.cursor.item/person"; // 如果是单条记录,则为vnd.android.cursor.item/
// + path
case PERSONS:
return "vnd.android.cursor.dir/persons"; // 如果是多条记录,则为vnd.android.cursor.dir/
// + path
}
return null;
}

4、ContentResolver类中的uri:(以insert()方法为例)

     //测试方法:向数据库中添加记录。如果之前没有数据库,则会自动创建
public void insert() {
// 使用内容解析者ContentResolver访问内容提供者ContentProvider
ContentResolver contentResolver = getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put("name", "生命贰号");
values.put("address", "湖北");
// content://authorities/person
// http://
Uri uri = Uri
.parse("content://com.example.contentprovidertest01.PersonContentProvider/person");
contentResolver.insert(uri, values);
}

【工程文件】

链接:http://pan.baidu.com/s/1hq7VO12

密码:0a49

Android组件系列----ContentProvider内容提供者的更多相关文章

  1. Android组件系列----ContentProvider内容提供者【1】

    [正文] 一.ContentProvider简单介绍: ContentProvider内容提供者(四大组件之中的一个)主要用于在不同的应用程序之间实现数据共享的功能. ContentProvider能 ...

  2. Android组件系列----ContentProvider内容提供者【4】

    (4)单元測试类: 这里须要涉及到另外一个知识:ContentResolver内容訪问者. 要想訪问ContentProvider.则必须使用ContentResolver. 能够通过ContentR ...

  3. Android组件系列----ContentProvider内容提供商【5】

    2.执行query()方法,查询全部记录(眼下一共两条记录).后台输出效果例如以下: 经測试,其它方法也都是能够运行的. 事实证明,新建的另外一个project文件ContentResolverTes ...

  4. Android开发学习—— ContentProvider内容提供者

    * 应用的数据库是不允许其他应用访问的* 内容提供者的作用就是让别的应用访问到你的数据库.把私有数据暴露给其他应用,通常,是把私有数据库的数据暴露给其他应用. Uri:包含一个具有一定格式的字符串的对 ...

  5. android 53 ContentProvider内容提供者

    ContentProvider内容提供者:像是一个中间件一样,一个媒介一样,可以以标准的增删改差操作对手机的文件.数据库进行增删改差.通过ContentProvider查找sd卡的音频文件,可以提供标 ...

  6. android contentprovider内容提供者

    contentprovider内容提供者:让其他app可以访问私有数据库(文件) 1.AndroidManifest.xml 配置provider <?xml version="1.0 ...

  7. contentProvider 内容提供者

    http://blog.csdn.net/woshixuye/article/details/8280879 实例代码当数据需要在应用程序间共享时,我们就可以利用ContentProvider为数据定 ...

  8. contentProvider内容提供者

    contentProvider内容提供者 15. 四 / android基础 / 没有评论   步骤 权限在application中注册 Source code     <provider an ...

  9. Android 进阶11:进程通信之 ContentProvider 内容提供者

    学习启舰大神,每篇文章写一句励志的话,与大家共勉. When you are content to be simply yourself and don't compare or compete, e ...

随机推荐

  1. .NET Core的“dotnet restore”、“dotnet build”和“dotnet run”命令都是用来干什么的?

    dotnet restore 源代码:https://github.com/dotnet/cli/tree/rel/1.0.0/src/dotnet/commands/dotnet-restore 入 ...

  2. D/A转换器

    电荷:带正负电的基本粒子.电的本质是使正负电荷分开,使电荷发生移动,实质是电子的转移,并不是创造电荷.电压:单位正电荷受电场力作用从A点移动到B点所做的功.电压方向从高电位指向低点位.电压是推动电荷定 ...

  3. SDK Build Tools revision (19.0.3) is too low for project Min

    SDK Build Tools revision (19.0.3) is too low for project Min(转)       如果你正在使用Android Studio工具进行开发,且将 ...

  4. CSS3中的calc()

    什么是calc()? calc是英文单词calculate(计算)的缩写,是css3的一个新增的功能; MDN的解释为可以用在任何长度,数值,时间,角度,频率等处; /* property: calc ...

  5. C语言异常与断言接口与实现

    程序中通常会出现三种错误:用户错误.运行期错误以及异常 欢迎关注我的个人博客:www.wuyudong.com, 更多精彩文章与您分享 标准库函数setjmp和longjmp 在C语言中,标准库函数s ...

  6. 错误:找不到类org.springframework.web.context.ContextLoaderListener

    严重: Error configuring application listener of class org.springframework.web.context.ContextLoaderLis ...

  7. Windows平台的Eclipse-javaEE-mars相关配置

    平台:winddow10 前提: 1>  搭建好了jdk1.8.0_60环境 2>  下载放置好了apache-tomcat-8.0.24 3>  下载好了eclipse-jee-m ...

  8. Windows 编 程中的字符串

    (1)在win32编程中,如何使用string类型 #include <string> using namespace std; LPTSTR    lpCmdLine = L" ...

  9. TCP & UDP 的区别

    一.概念 ① TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议. “面向连接”就是在正式通信前必须要与对方建立起连 ...

  10. python yield

    http://www.jb51.net/article/15717.htm  这里还不错 只是粗略的知道yield可以用来为一个函数返回值塞数据,比如下面的例子: def addlist(alist) ...