Android的binder机制分析
转载别人的文章,谢谢分享:http://blog.csdn.net/nadyhuayue/article/details/8363029
android的binder机制提供一种进程间通信的方法,使不同一个进程可以以类似远程过程调用的形式调用另一个进程所提供的功能。
android的代码中,与C/C++的binder包括一些类型和接口的定义和实现,相关的代码在下面这几个文件中:
frameworks/base/include/utils/IInterface.h
frameworks/base/include/utils/Binder.h
frameworks/base/include/utils/BpBinder.h
frameworks/base/include/utils/IBinder
frameworks/base/include/utils/Parcel.h
frameworks/base/include/utils/IPCThreadState.h
frameworks/base/include/utils/ProcessState.h
frameworks/base/libs/utils/Binder.cpp
frameworks/base/libs/utils/BpBinder.cpp
frameworks/base/libs/utils/IInterface.cpp
frameworks/base/libs/utils/IPCThreadState.cpp
frameworks/base/libs/utils/Parcel.cpp
frameworks/base/libs/utils/ProcessState.cpp
为了了解这些类、接口之间的关系以及binder的实现机制,最好是结合一个例子来进行研究。我选择的例子是android自带的媒体播放器的实现。其媒体播放器的相关代码在下面这些目录中:
frameworks/base/include/media
frameworks/base/media
使用startUML的反向工程功能分析上面这些代码,并进行了一定的整理之后。
android的媒体播放功能分成两部分,一部分是媒体播放应用,一部分是媒体播放服务(MediaServer,在系统启动时由init所启动,具可参考init.rc文件)。这两部分分别跑在不同的进程中。媒体播放应用包括Java程序和部分C++代码,媒体播放服务是C++代码,并且需要调用外部模块opencore来实现真正的媒体播放。媒体播放应用和媒体播放服务之间需要通过binder机制来进行相互调用,这些调用包括:
(1)媒体播放应用向媒体播放服务发控制指令
(2)媒体播放服务向媒体播放应用发事件通知(notify)
媒体播放服务对外提供多个接口,在上面得图中包括其中的2个接口:IMediaService和IMediaPlayer,IMediaplayer用于创建和管理播放实例,而IMediaplayer接口则是播放接口,用于实现指定媒体文件的播放以及播放过程的控制。
上面的图中还有媒体播放应用向媒体播放服务提供的1个接口:IMediaPlayerClient,用于接收notify。
这些接口因为需要跨进程调用,因此需要用到binder机制。每个接口包括两部分实现,一部分是接口功能的真正实现(BnInterface),这部分运行在接口提供进程中;另一部分是接口的proxy(BpInterface),这部分运行在调用接口的进程中。binder的作用就是让这两部分之间建立联系。下图是整个播放器的一个概要说明。
媒体播放器比较复杂一些,总共实现了3个接口,不过要了解binder的机制,只需要研究其中一个接口就足够了。在这里选择IMediaPlayerService接口来看一下。
IMediaPlayerService接口包括六个功能函数:create(url)、create(fd)、decode(url)、 decode(fd)、createMediaRecord()、createMetadataRetriever()。在这里不介绍这些函数是做什么的,我们只关注如何通过binder还提供这些函数接口。
(二) 接口定义
(1) 定义接口类
首先定义IMediaPlayerService类,这是一个接口类(C++的术语应该叫纯虚类)。该接口类定义在文件frameworks/base/include/media/IMediaPlayerService.h。代码如下:
class IMediaPlayerService: public IInterface
{
public:
DECLARE_META_INTERFACE(MediaPlayerService);
virtual sp createMediaRecorder(pid_t pid) = 0;
virtual sp createMetadataRetriever(pid_t pid) = 0;
virtual sp create(pid_t pid, const sp& client, const char* url) = 0;
virtual sp create(pid_t pid, const sp& client, int fd, int64_t offset, int64_t length) = 0;
virtual sp decode(const char* url, uint32_t *pSampleRate, int* pNumChannels, int* pFormat) = 0;
virtual sp decode(int fd, int64_t offset, int64_t length, uint32_t *pSampleRate, int* pNumChannels, int* pFormat) = 0;
};
可以看到,在这个接口类中定义了IMediaPlayerService需要提供的6个函数接口,因为是接口类,所以定义为纯虚函数。需要注意这个接口类的名称有严格要求,必须是以大写字母I开始。
重点关注在这些函数前面的一个宏定义: DECLARE_META_INTERFACE(MediaPlayerService)。这个宏定义必须要有,其中封装了实现binder所需要的一些类成员变量和成员函数通过这些成员函数可以为一个binder实现创建proxy。这个宏定义在问价frameworks/base/include /utils/IInterface.h里,在后面还会讲到。这个宏定义的参数必须是接口类的名称去除字母I后剩下的部分。
另外说明一下,可以看到接口类中所定义的函数的返回值都是sp的形式,看起来有点怪异。sp是android中定义的一个模板类,用于实现智能指针功能。sp就是IMediaPlayer的智能指针,可以简单地把它看成是标准C++中的指针定义即 IMediaPlayer* 即可。
(2) 定义和实现binder类
binder类包括两个,一个是接口实现类,一个接口代理类。接口代理类继承自BpInterface,接口实现类继承自BnInterface。这两个基类都是模板类,封装了binder的进程间通信机制,这样使用者无需关注底层通信实现细节。
对于IMediaPlayerService接口,其binder接口实现类为BnMediaPlayerService,接口代理类为 BpMediaPlayerService。需注意这两个类的名称有严格要求,必须以Bn和Bp开头,并且后面的部分必须是前面所定义的接口类的名称去除字母''''I’。比如前面所定义的接口类为IMediaPlayerService,去除字母I后是MediaPlayerService,所以两个 binder类的名称分别是BnMediaPlayerService和BpMediaPlayerService。为什么有这样的要求?原因就在前面提到的宏定义DECLARE_META_INTERFACE()和另一个宏定义IMPLEMENT_META_INTERFACE()里面。有兴趣的话可以去看一下,这两个宏定义都在文件frameworks/base/include/utils/IInterface.h里。
BpMediaPlayerService是一个最终实现类。定义并且实现在在文件frameworks/base/media/libmidia /IMediaPlayerService.cpp中。在看BpMediaPlayerService的代码之前,先看一下在 IMediaPlayerService.cpp文件的开始部分的一个枚举定义:
enum {
CREATE_URL = IBinder::FIRST_CALL_TRANSACTION,
CREATE_FD,
DECODE_URL,
DECODE_FD,
CREATE_MEDIA_RECORDER,
CREATE_METADATA_RETRIEVER,
};
这些6个枚举定义对应于IMediaPlayerService接口所提供的6个功能函数,可以称为这些功能函数的功能代码,用于在进程之间进行RPC是标识需要调用哪个函数。如果不想定义这些枚举值,在后面需要用到这些值的地方直接写上1,2,3,4,5,6也是可以的,不过……一个合适的程序员会这么干吗?
下面看一下BpMediaPlayerService的代码。
(3) BpMediaPlayerService代码分析
class BpMediaPlayerService: public BpInterface
{
public:
BpMediaPlayerService(const sp& impl)
: BpInterface(impl)
{
}
virtual sp createMetadataRetriever(pid_t pid)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeInt32(pid);
remote()->transact(CREATE_METADATA_RETRIEVER, data, &reply);
return interface_cast(reply.readStrongBinder());
}
virtual sp create(pid_t pid, const sp& client, const char* url)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeInt32(pid);
data.writeStrongBinder(client->asBinder());
data.writeCString(url);
remote()->transact(CREATE_URL, data, &reply);
return interface_cast(reply.readStrongBinder());
}
virtual sp createMediaRecorder(pid_t pid)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeInt32(pid);
remote()->transact(CREATE_MEDIA_RECORDER, data, &reply);
return interface_cast(reply.readStrongBinder());
}
virtual sp create(pid_t pid, const sp& client, int fd, int64_t offset, int64_t length)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeInt32(pid);
data.writeStrongBinder(client->asBinder());
data.writeFileDescriptor(fd);
data.writeInt64(offset);
data.writeInt64(length);
remote()->transact(CREATE_FD, data, &reply);
return interface_cast(reply.readStrongBinder());
}
virtual sp decode(const char* url, uint32_t *pSampleRate, int* pNumChannels, int* pFormat)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeCString(url);
remote()->transact(DECODE_URL, data, &reply);
*pSampleRate = uint32_t(reply.readInt32());
*pNumChannels = reply.readInt32();
*pFormat = reply.readInt32();
return interface_cast(reply.readStrongBinder());
}
virtual sp decode(int fd, int64_t offset, int64_t length, uint32_t *pSampleRate, int* pNumChannels, int* pFormat)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeFileDescriptor(fd);
data.writeInt64(offset);
data.writeInt64(length);
remote()->transact(DECODE_FD, data, &reply);
*pSampleRate = uint32_t(reply.readInt32());
*pNumChannels = reply.readInt32();
*pFormat = reply.readInt32();
return interface_cast(reply.readStrongBinder());
}
};
首先可以看到,这个类继承自模板类BpInterface,指定类型为接口类IMediaPlayerService。BpInterface模板类定义在文件IInterface.h。看一下BpInterface的定义就可以发现,BpMediaPlayerService这样定义了以后,事实上间接继承了IMediaPlayerService,从而可以提供IMediaPlayerService接口所定义的接口函数。 BpMediaPlayerService需要实现这些接口函数。在一个简单的构造函数之后,就是这些接口函数的实现。可以看到,所有的接口函数的实现方法都是一致的,都是通过binder所提供的机制将参数仍给binder的实现类,并获取返回值。这也就是这个类之所以成为代理类的原因。下面具体看一下一个接口函数。这里选的是函数create(url)。
virtual sp create(pid_t pid, const sp& client, const char* url)
{
Parcel data, reply;
data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());
data.writeInt32(pid);
data.writeStrongBinder(client->asBinder());
data.writeCString(url);
remote()->transact(CREATE_URL, data, &reply);
return interface_cast(reply.readStrongBinder());
}
这个接口函数的参数指定了一个URL,函数将为这个URL创建一个播放器实例用于播放该URL。
函数首先定义了两个局部变量data和reply,变量的类型都是Parcel。Parcel是一个专为binder通信的数据传送而定义的类,该类提供了对多种类型的数据的封装功能,同时提供多个数据读取和写入函数,用于多种类型的数据的写入和读取,支持的数据类型既包括简单数据类型,也包括对象。这里定义的变量data是用于封装create()函数调用所需要的输入参数,而reply则是用于封装调用的返回数据(包括输出参数的值和函数返回值)。
函数首先向data中写入各种数据。第一个写入的是接口的一个描述字符串,binder的实现类中会用这个字符串来对接口做验证,防止调用错误。这个字符串也可以不写,如果不写,在binder实现类中相应的也就不要做验证了。跟在描述字符串后面写入的是该接口函数所需要的各种的输入参数。需要说明的是,Pacel提供一种先入先出的数据存储方式,即数据的写入顺序和读取顺序必须严格一致,否则将会出错。
完成数据写入后,函数调用remote()->transact()用于完成binder通信。transact()函数的第一个参数就是前面提到过的功能代码。transact()的功能是将data中的数据传给binder的实现类,函数调用结束后,reply中将包含返回数据。首先来看看 remote()成员函数。前面讲到过BpMediaPlayerService通过继承BpInterface模板类间接继承了 IMediaPlayerService接口类,其实BpInterface类是一个有两个父类的多重继承子类,另一个父类是 BpRefbase(frameworks/base/include/utils/Binder.h)。remote()就是继承自BpRefBase 类的一个成员函数,该函数返回BpRefBase类中定义的一个私有属性mRemote。mRemote是对IBinder接口类的子类BpBinder 的一个对象的引用(参考前面的类关系图)。transact()函数在IBinder接口类中定义(frameworks/base/include /utils/Binder.h),并在BpBinder类中实现(frameworks/base/include/utils /BpBinder.h、frameworks/base/libs/utils/BpBinder.cpp)。在transact()函数中将调用 IPCThreadState类的transact()函数,并进而通过Lniux内核中的android共享内存驱动来实现进程间通信。不过这些细节这里就不多说了。在这里BpBinder类对象是一个关键,是实现Binder代理的核心之一。BpBinder类可以看成是一个通信handle(类似于网络编程中的socket),用于实现进程间通信。接下来需要研究的是这个BpBinder类对象(即mRemote成员变量的值)是从哪里来的。
回过头来BpMediaPlayerService的构造函数(看前面的代码)。该构造函数的参数是一个IBinder对象的引用。mRemote的值就是在这里传进来的这个对象。那么这个对象又是怎么来的呢?要搞清楚这一点就需要找到创建BpMediaPlayerService类的实例的代码,这个代码就就跟在该类的定义代码的下面。继续看IMediaPlayerService.cpp文件,在BpMediaPlayerService类定义的后面,是下面这样一行代码:
IMPLEMENT_META_INTERFACE(MediaPlayerService, "android.hardware.IMediaPlayerService");
这行代码调用了一个宏定义IMPLEMENT_META_INTERFACE()。这个宏定义与前面提到过的 DECLARE_META_INTERFACE()相呼应。看名字就知道,IMPLEMENT_META_INTERFACE()宏是对 DECLARE_META_INTERFACE()所定义的成员函数的具体实现。这个宏的第一个参数与DECLARE_META_INTERFACE() 的参数需完全一样,第二参数是接口的描述字符串(这个字符串前面也已经讲到过了)。描述字符串不重要,重要的是宏里面定义的一个静态成员函数 asInterface()。BpMediaPlayerService的类实例是在IMediaPlayerService的静态成员函数 asInterface()中创建的,在IInterface.h中定义了一个内联函数interface_cast(),对这个成员函数进行了封装。通过看代码容易知道,BpMediaPlayerService的构造函数的参数是通过interface_cast()的参数传进来的。
好,下面就该看看这个interface_cast()是在哪里调用的,它的参数到底是什么。找到frameworks/base/media /libmedia/mediaplayer.cpp文件,其中的MediaPlayer::getMediaPlayerService()的实现代码:
const sp& MediaPlayer::getMediaPlayerService()
{
Mutex::Autolock _l(sServiceLock);
if (sMediaPlayerService.get() == 0) {
sp sm = defaultServiceManager();
sp binder;
do {
binder = sm->getService(String16("media.player"));
if (binder != 0)
break;
LOGW("MediaPlayerService not published, waiting...");
usleep(500000); // 0.5 s
} while(true);
if (sDeathNotifier == NULL) {
sDeathNotifier = new DeathNotifier();
}
binder->linkToDeath(sDeathNotifier);
sMediaPlayerService = interface_cast(binder);
}
LOGE_IF(sMediaPlayerService==0, "no MediaPlayerService!?");
return sMediaPlayerService;
}
看一下上面这段代码中的红色字体部分。结合前面的分析,可知BpBinder类的对象实例是从android的服务管理器的getService()函数中获取,进一步追进去,会发现下面这样一段代码:
{
Parcel data, reply;
data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
data.writeString16(name);
remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
return reply.readStrongBinder();
}
Android的服务管理器是一个单独的进程,也向外提供接口。这段代码的含义,是通过Android的服务管理器的接口代理,请求调用服务管理器的 checkService()接口函数,查找指定的服务(上面就是查找media.player服务),查找成功后返回一个BpBinder类的对象实例,用于供IMediaPlayerService代理使用。这个对象BpBinder是在Parcel::readStrongBinder()函数里面创建的。那么到底是怎么创建出来的呢?在这里没有必要追到ServiceManager的实现代码里去,毕竟我们只是想知道BpBinder的对象是如何创建的,我们可以换一个例子来看。回到前面的BpMediaPlayerService::create()函数的实现,是不是很眼熟。没错,在那个函数里也创建了一个BpBinder类对象,那个对象是是给IMediaPlayer接口代理使用的。虽然接口不同,但是创建原理是一样的。我们继续,下面该到binder的另一个类——实现类的代码了。
(3) BnMediaPlayerService代码分析
BnMediaPlayerService类的定义在文件frameworks/base/include/media /IMediaPlayService.h,实现则与BpMediaPlayerService一样是在文件frameworks/base/media /libmidia/IMediaPlayerService.cpp中。类定义的代码如下:
class BnMediaPlayerService: public BnInterface
{
public:
virtual status_t onTransact( uint32_t code,
const Parcel& data,
Parcel* reply,
uint32_t flags = 0);
};
这个类继承自BnInterface模板类,约束类型为IMediaPlayerService。看一下BnInterface模板类的定义(IInterface.h)就可以知道,BnMediaPlayerService间接继承了IMediaPlayerService接口类。不过 BnInterface类并没有实现IMediaPlayerService所定义的6个接口函数,因此BnInterface还是一个纯虚类。这些接口需要在BnMediaPlayerService的子类中真正实现,这个子类就是MediaPlayerService(frameworks/base /media/libmidiaservice/MediaPlayerService.h,frameworks/base/media /libmidiaservice/MediaPlayerService.cpp)。在BnMediaPlayerService的成员函数 onTransact()中,需要调用这6个接口函数。BnMediaPlayerService中主要就是定义并实现了onTransact()函数。当在代理那边调用了transact()函数后,这边的onTransact()函数就会被调用。BnMediaPlayerService的实现代码如下:
#define CHECK_INTERFACE(interface, data, reply) /
do { if (!data.enforceInterface(interface::getInterfaceDescriptor())) { /
LOGW("Call incorrectly routed to " #interface); /
return PERMISSION_DENIED; /
} } while (0)
status_t BnMediaPlayerService::onTransact(
uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
switch(code) {
case CREATE_URL: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
pid_t pid = data.readInt32();
sp client = interface_cast(data.readStrongBinder());
const char* url = data.readCString();
sp player = create(pid, client, url);
reply->writeStrongBinder(player->asBinder());
return NO_ERROR;
} break;
case CREATE_FD: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
pid_t pid = data.readInt32();
sp client = interface_cast(data.readStrongBinder());
int fd = dup(data.readFileDescriptor());
int64_t offset = data.readInt64();
int64_t length = data.readInt64();
sp player = create(pid, client, fd, offset, length);
reply->writeStrongBinder(player->asBinder());
return NO_ERROR;
} break;
case DECODE_URL: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
const char* url = data.readCString();
uint32_t sampleRate;
int numChannels;
int format;
sp player = decode(url, &sampleRate, &numChannels, &format);
reply->writeInt32(sampleRate);
reply->writeInt32(numChannels);
reply->writeInt32(format);
reply->writeStrongBinder(player->asBinder());
return NO_ERROR;
} break;
case DECODE_FD: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
int fd = dup(data.readFileDescriptor());
int64_t offset = data.readInt64();
int64_t length = data.readInt64();
uint32_t sampleRate;
int numChannels;
int format;
sp player = decode(fd, offset, length, &sampleRate, &numChannels, &format);
reply->writeInt32(sampleRate);
reply->writeInt32(numChannels);
reply->writeInt32(format);
reply->writeStrongBinder(player->asBinder());
return NO_ERROR;
} break;
case CREATE_MEDIA_RECORDER: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
pid_t pid = data.readInt32();
sp recorder = createMediaRecorder(pid);
reply->writeStrongBinder(recorder->asBinder());
return NO_ERROR;
} break;
case CREATE_METADATA_RETRIEVER: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
pid_t pid = data.readInt32();
sp retriever = createMetadataRetriever(pid);
reply->writeStrongBinder(retriever->asBinder());
return NO_ERROR;
} break;
default:
return BBinder::onTransact(code, data, reply, flags);
}
}
首先是一个宏定义CHECK_INTERFACE(),这个宏定义的作用是检查接口的描述字符串,这个前面也提到过,不需细说。然后就是 onTrasact()函数的实现。这个函数的结构也很简单,就是根据参数code的值分别执行不同的功能调用。code的取值就是前面提到过的接口功能代码。函数的参数除了code,还包括Parcel类的两个对象data和reply,分别用于传送输入参数和返回数据,与transact()函数的参数相对应。还有一个参数flag在这里用不上,不讨论。对应我们前面所选择的接口函数的例子create(url),看看这边对应的实现:
case CREATE_URL: {
CHECK_INTERFACE(IMediaPlayerService, data, reply);
pid_t pid = data.readInt32();
sp client = interface_cast(data.readStrongBinder());
const char* url = data.readCString();
sp player = create(pid, client, url);
reply->writeStrongBinder(player->asBinder());
return NO_ERROR;
}
首先是从data对象中依次取出各项输入参数,然后调用接口函数create()(将在子类MediaPlayerService中实现),最后向reply中写入返回数据。这个函数返回后,代理那边的transact()也会跟着返回。
那么onTransact()函数是怎么被调用的呢?通过查看BnInterface模板类的定义可以看到,这个类也是一个多重继承类,另一个父类是 BBinder(frameworks/base/include/utils/Binder.h,frameworks/base/libs /utils/Binder.cpp)。BBinder类继承自IBinder,也实现了transact()函数,在这个函数中调用 onTransact()函数。而BBinder对象的transact()函数则是在IPCThreadState类的 executeCommand()成员函数中调用的。这已经涉及到较底层的实现,在这里不再多说。
上面这部分代码还与前面提到过的BpBinder对象的创建有关系。看其中的红色字体部分,通过create()函数调用会创建一个 IMediaPlayer接口类的子类的对象,这个对象其实是MediaPlayerService::Client类(可以看一下 MediaPlayerService的定义)的对象实例,而MediaPlayerService::Client类是继承自 BnMediaPlayer类的,与BnMediaPlayerService类类似,BnMediaPlayer其实也是一个binder实现类(是 BBinder的子类,进而也是IBinder的子类)。在上述代码中,通过Parcel的writeStrongBinder()函数将这个对象写入 reply,而在代理侧,通过Parcel的readStrongBinder()函数读取则可以得到一个BpBinder的对象。至于类的具体创建过程已经封装在Parcel类的定义中,这里就不再多说了。
(4) 接口功能的真正实现
到这里两个binder类就已经定义完了,下面就是IMediaPlayerService接口函数的真正实现。前面已经说过这些函数在类 MediaPlayerService中实现。这个类继承自BnMediaPlayerService,也间接地继承了 IMediaPlayerService接口类定义的6个功能函数,只需要按照正常方式实现这6个功能函数即可,当然为了实现这6个函数就需要其它一大堆的东西,不过这些具体的实现方法已经与binder机制无关,不再多说。
在MediaPlayerService类中定义了一个静态函数instantiate(),在这个函数中创建MediaPlayerService的对象实例,并将这个对象注册到服务管理器中。这样需要使用的时候就可以从服务管理器获取IMediaPlayerService的代理对象。这个 instantiate()是在MediaServer程序的main()函数中调用的。
void MediaPlayerService::instantiate() {
defaultServiceManager()->addService(
String16("media.player"), new MediaPlayerService());
}
(三) 总结一下
说了这么多,总结一下。下图是binder机制的层次模型。
如果一个服务需要通过binder机制对外提供跨进程的接口,需要做下面这些事情。
(1) 第一步,需要为这个接口定义一个继承自IInterface的接口类,假设叫做IMyService。
(2) 第二步,需要定义两个binder类,其中一个是代理类BpMyService,需继承自BpInterface;另一个是实现类BnMyService,需继承自BnInterface。
(3) 第三步,定义BnMyService的子类,这个子类可以是任何名字,比如就叫MyService,在其中真正实现接口所提供的各个函数。
(4) 第四步,创建MyService的实例,注册到服务管理器(如IMediaPlayerService),也可以在其它接口的函数中创建(如上面的IMediaPlayer)。
阅读(245) | 评论(0) | 转发(0) |