[转]结合perfetto-systrace分析Vsync部分源码流程
原文 App-Sf的Vsync部分源码流程结合perfetto-systrace分析
本节将使用perfetto的trace来巩固Vsync的源码分析的部分的流程。
具体抓取trace方法及相关操作建议:
a.抓取trace期间需要主要不能让画面一直刷新,因为这样一直刷新不方便看vsync的结束和开始
b.建议选着桌面,滑动桌面一下后停止1左右,再继续滑动,尽量让抓取的trace可以有如下图的间隔效果
c.需要在surfaceflinger中额外补充自己加的一些ATRACE代码方便追踪流程
1、app请求Vsync部分
这里我们回忆一下app的Vsync的申请,一般都是app主动申请的,一般都是应用端
Choreographer#scheduleVsyncLocked方法跨进程调用到Surfaceflinger端的requestNextVsync方法,具体trace如下图
应用端如下:
surfaceflinger作为服务端如下:
所有的Vsync逻辑其实就是从SurfaceFlinger的requestNextVsync方法开始的:
binder::Status EventThreadConnection::requestNextVsync() {
ATRACE_CALL();
//这里加个ATRACE标准方便确定是app申请还是appSf申请的Vsync
ATRACE_BEGIN(((impl::EventThread*)mEventThread)->toNameString());
mEventThread->requestNextVsync(this);
ATRACE_END();
return binder::Status::ok();
}
调用到了 mEventThread->requestNextVsync(this);
代码如下
void EventThread::requestNextVsync(const sp<EventThreadConnection>& connection) {
ATRACE_NAME("EventThread::requestNextVsync");
if (connection->resyncCallback) {
connection->resyncCallback();
}
std::lock_guard<std::mutex> lock(mMutex);
if (connection->vsyncRequest == VSyncRequest::None) { //如果这个是第一次申请进入一般这里就是None
connection->vsyncRequest = VSyncRequest::Single;
ATRACE_NAME("connection->vsyncRequest None notify_all");
mCondition.notify_all();//第一次则会通过mCondition唤醒等待
} else if (connection->vsyncRequest == VSyncRequest::SingleSuppressCallback) { //这里代表前一次刚刚已经进入vsync,准备进入停止回调状态
ATRACE_NAME("connection->vsyncRequest SingleSuppressCallback ");
connection->vsyncRequest = VSyncRequest::Single; //这里不需要唤醒,代表thread运行等待vsync,需要靠vsync时间到了才唤醒
}
}
这里都会吧vsyncRequest变变成Single,最重要mCondition.notify_all()会唤醒一直等待的EventThread的线程,让他继续执行:
这里唤醒也可以通过perfetto看出来:
对于的代码:
void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {
DisplayEventConsumers consumers;
while (mState != State::Quit) {
if (!mPendingEvents.empty()) {//检测是否有mPendingEvent,遍历各个Event,
event = mPendingEvents.front();
mPendingEvents.pop_front();
switch (event->header.type) {
case DisplayEventReceiver::DISPLAY_EVENT_HOTPLUG:
if (event->hotplug.connected && !mVSyncState) {
mVSyncState.emplace(event->header.displayId);
} else if (!event->hotplug.connected && mVSyncState &&
mVSyncState->displayId == event->header.displayId) {
mVSyncState.reset();
}
break;
case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
if (mInterceptVSyncsCallback) {
mInterceptVSyncsCallback(event->header.timestamp);
}
break;
}
}
bool vsyncRequested = false;//初始化为false
// Find connections that should consume this event.
auto it = mDisplayEventConnections.begin();
while (it != mDisplayEventConnections.end()) {//遍历一下所有的connection,和EventTread绑定的app链接
if (const auto connection = it->promote()) {
vsyncRequested |= connection->vsyncRequest != VSyncRequest::None;//这里会检测一下是前面设置过的request是不是还是None,明显前面设置成了Single
if (event && shouldConsumeEvent(*event, connection)) {//这里会判断当前event是否要让这个connection消费
consumers.push_back(connection);
ATRACE_FORMAT("shouldConsumeEvent consumers push %s",toString(*connection).c_str());
}
++it;
} else {
it = mDisplayEventConnections.erase(it);
}
}
if (!consumers.empty()) {//进行相关的事件派发
dispatchEvent(*event, consumers);
consumers.clear();
}
ATRACE_FORMAT("vsyncRequested = %d",vsyncRequested);
State nextState;
//设置下一个nextstate,这里注意nextstate很关键,如果没有vsyncRequested,那么就nextstate就一定为Idle
if (mVSyncState && vsyncRequested) {
nextState = mVSyncState->synthetic ? State::SyntheticVSync : State::VSync;
} else {
ALOGW_IF(!mVSyncState, "Ignoring VSYNC request while display is disconnected");
nextState = State::Idle;
}
//nextState和当前state进行比较不相等进入
if (mState != nextState) {
if (mState == State::VSync) {//如果当前是vsync,那么nextState不是Vsync了说明vsync要停止
ATRACE_NAME("mVSyncSource->setVSyncEnabled(false)");
mVSyncSource->setVSyncEnabled(false);
} else if (nextState == State::VSync) {//mState当前不是Vsync,下一个state是Vsync,就会进入以下关键方法
ATRACE_NAME("mVSyncSource->setVSyncEnabled(true)");
mVSyncSource->setVSyncEnabled(true);//调用启动vsync相关
}
//上面操作完成就代表状态已经切换
mState = nextState;
}
if (event) {//如果有event不会进入下面的wait而是会继续从头开始执行
continue;
}
// Wait for event or client registration/request.
if (mState == State::Idle) {
mCondition.wait(lock);//开始长时间没有vsync申请,就在这等待,第一次app申请时候就会唤醒这里
} else {
// Generate a fake VSYNC after a long timeout in case the driver stalls. When the
// display is off, keep feeding clients at 60 Hz.
const std::chrono::nanoseconds timeout =
mState == State::SyntheticVSync ? 16ms : 1000ms;
//进行超时等待
if (mCondition.wait_for(lock, timeout) == std::cv_status::timeout) {
//省略
}
}
}
}
EventThread的执行情况如下:
这里发现主要业务在 mVSyncSource->setVSyncEnabled(true)代码中,这里面开启了一系列的vsync计算和定时任务
大概上面可以看出调用栈如下:
VSyncSource->setVSyncEnabled(true)
----》 VSyncDispatchTimerQueue::schedule
----------》rearmTimerSkippingUpdateFor
--------------》setTimer
这样就完成了设置定时触发操作,具体这里不进行详细讲述定时这部分,因为较为复杂需要单独章节,这里大家只要知道最后获取了自己app的vsync触发时间targetTime,定时器设置了到时间就触发,回调相关的timeCallback方法。
到此就EventThread就执行到了等待定时等待状态,等待定时时间的到来。
2、Vsync时间到了后触发timeCallback
上面步骤的定时器触发后,回调执行是在单独的TimerDispatch线程,具体trace的体现如下:
来看看timeCallback里面又干啥了,代码如下:
void VSyncDispatchTimerQueue::timerCallback() {
ATRACE_CALL();
std::vector<Invocation> invocations;
{
//遍历3个vsync,app,sf,appSf
for (auto it = mCallbacks.begin(); it != mCallbacks.end(); it++) {
auto& callback = it->second;
auto const wakeupTime = callback->wakeupTime();
if (!wakeupTime) {//如果没有wakupTime直接下一个,因为压根没有vsync的任何申请踪迹
continue;
}
auto const readyTime = callback->readyTime();
auto const lagAllowance = std::max(now - mIntendedWakeupTime, static_cast<nsecs_t>(0));
//判断的wakeupTime是不是比(mIntendedWakeupTime + mTimerSlack + lagAllowance)小,正常第一app请求肯定会进入这里
if (*wakeupTime < mIntendedWakeupTime + mTimerSlack + lagAllowance) {
callback->executing();
invocations.emplace_back(Invocation{callback, *callback->lastExecutedVsyncTarget(),
*wakeupTime, *readyTime});
}
}
mIntendedWakeupTime = kInvalidTime;//设置一个无穷大的值
rearmTimer(mTimeKeeper->now());//重新准备设置定时器
}
for (auto const& invocation : invocations) {//开始回调上面获取的callback
invocation.callback->callback(invocation.vsyncTimestamp, invocation.wakeupTimestamp,
invocation.deadlineTimestamp);
}
}
主要干的几件事如下:
1、遍历3个类型vsync看看谁是有定时任务的,比较wakeupTime和intentedTime符合回调情况
2、符合回调情况的,会调用executing清除定时器,和wakeupTime,并吧intentedTime设置成很大数字
3、继续下一个rearmTimer定时任务
4、回调相关类型vsync,vsync到来
上面四个任务就是TimeDispatch完成的。
这里要注意一下mCallbacks这个变量哪来的,是怎么对应刚好就是app,appSf,sf,3个vsync的呢?
mCallbacks都是通过这个方法register来注册的:
VSyncDispatchTimerQueue::CallbackToken VSyncDispatchTimerQueue::registerCallback(
Callback callback, std::string callbackName) {
std::lock_guard lock(mMutex);
return CallbackToken{
mCallbacks
.emplace(++mCallbackToken,//这里构造出来了VSyncDispatchTimerQueueEntry的实体
std::make_shared<VSyncDispatchTimerQueueEntry>(std::move(callbackName),
std::move(callback),
mMinVsyncDistance))
.first->first};
}
那么什么地方调用注册呢?实在构造VSyncCallbackRegistration时候
VSyncCallbackRegistration::VSyncCallbackRegistration(VSyncDispatch& dispatch,
VSyncDispatch::Callback callback,
std::string callbackName)
: mDispatch(dispatch),
//给mToken赋值时候,直接调用的注册callback返回的token
mToken(dispatch.registerCallback(std::move(callback), std::move(callbackName))),
mValidToken(true) {}
那么这个VSyncCallbackRegistration是在什么时候构造的呢?
对于sf是在MessageQueue中进行的:
void MessageQueue::initVsync(scheduler::VSyncDispatch& dispatch,
frametimeline::TokenManager& tokenManager,
std::chrono::nanoseconds workDuration) {
setDuration(workDuration);
mVsync.tokenManager = &tokenManager;
//构造VSyncCallbackRegistration
mVsync.registration = std::make_unique<
scheduler::VSyncCallbackRegistration>(dispatch,
std::bind(&MessageQueue::vsyncCallback, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3),
"sf");
}
对于EventThread如下代码进行的:
CallbackRepeater(VSyncDispatch& dispatch, VSyncDispatch::Callback cb, const char* name,
std::chrono::nanoseconds workDuration, std::chrono::nanoseconds readyDuration,
std::chrono::nanoseconds notBefore)
: mName(name),
mCallback(cb),
//构造VSyncCallbackRegistration
mRegistration(dispatch,
std::bind(&CallbackRepeater::callback, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3),
mName),
mStarted(false),
mWorkDuration(workDuration),
mReadyDuration(readyDuration),
mLastCallTime(notBefore) {}
那么上面就知晓各自的callback在哪里了。
接下来这里重点关注一下回调vsync部分的任务,app类型的vsync回调任务代码如下:
frameworks/native/services/surfaceflinger/Scheduler/DispSyncSource.cpp的CallbackRepeater的callback方法:
void callback(nsecs_t vsyncTime, nsecs_t wakeupTime, nsecs_t readyTime) {
ATRACE_CALL();
//调用mCallback方法,其实就是DispSyncSource::onVsyncCallback
mCallback(vsyncTime, wakeupTime, readyTime);
{
std::lock_guard lock(mMutex);
if (!mStarted) {//注意一下这个值哈,和前面的EventThread的中有停止Vsync有关系
ATRACE_NAME("callback return not mRegistration.schedule");
return;
}
auto const scheduleResult =
mRegistration.schedule({.workDuration = mWorkDuration.count(),
.readyDuration = mReadyDuration.count(),
.earliestVsync = vsyncTime});
LOG_ALWAYS_FATAL_IF(!scheduleResult.has_value(), "Error rescheduling callback");
}
}
这里的mCallback在哪里赋值的呢?
DispSyncSource::DispSyncSource(VSyncDispatch& vSyncDispatch, VSyncTracker& vSyncTracker,
std::chrono::nanoseconds workDuration,
std::chrono::nanoseconds readyDuration, bool traceVsync,
const char* name)
: mName(name),
mValue(base::StringPrintf("VSYNC-%s", name), 0),
mTraceVsync(traceVsync),
mVsyncOnLabel(base::StringPrintf("VsyncOn-%s", name)),
mVSyncTracker(vSyncTracker),
mWorkDuration(base::StringPrintf("VsyncWorkDuration-%s", name), workDuration),
mReadyDuration(readyDuration) {
//这里进行了关键的DispSyncSource::onVsyncCallback传递到CallbackRepeater对象
mCallbackRepeater =
std::make_unique<CallbackRepeater>(vSyncDispatch,
std::bind(&DispSyncSource::onVsyncCallback, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3),
name, workDuration, readyDuration,
std::chrono::steady_clock::now().time_since_epoch());
}
//这里的第二个参数就是VSyncDispatch::Callback
CallbackRepeater(VSyncDispatch& dispatch, VSyncDispatch::Callback cb, const char* name,
std::chrono::nanoseconds workDuration, std::chrono::nanoseconds readyDuration,
std::chrono::nanoseconds notBefore)
: mName(name),
mCallback(cb),
mRegistration(dispatch,
std::bind(&CallbackRepeater::callback, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3),
mName),
mStarted(false),
mWorkDuration(workDuration),
mReadyDuration(readyDuration),
mLastCallTime(notBefore) {}
重点看看 DispSyncSource::onVsyncCallback方法:
void DispSyncSource::onVsyncCallback(nsecs_t vsyncTime, nsecs_t targetWakeupTime,
nsecs_t readyTime) {
ATRACE_CALL();
VSyncSource::Callback* callback;
{
std::lock_guard lock(mCallbackMutex);
callback = mCallback;
}
if (mTraceVsync) {
mValue = (mValue + 1) % 2; //这里就是前面讲解的vsync的方波图展示部分靠这里
}
if (callback != nullptr) {//这里关键一步会调用到EventTread中
callback->onVSyncEvent(targetWakeupTime, {vsyncTime, readyTime});
}
}
void EventThread::onVSyncEvent(nsecs_t timestamp, VSyncSource::VSyncData vsyncData) {
std::lock_guard<std::mutex> lock(mMutex);
//注意这里进行了PendingEvents的插入
mPendingEvents.push_back(makeVSync(mVSyncState->displayId, timestamp, ++mVSyncState->count,
vsyncData.expectedPresentationTime,
vsyncData.deadlineTimestamp));
mCondition.notify_all();//进行线程唤醒,又切换回了EventThread工作了
}
但是这里唤醒App的EventThread后trace中没看到具体干啥活,因为我们没有加相关ATRACE,加上一下相关TRACE如下:
明显看到这里其实就是开始派发Vsync相关的Event到app端。
callback这里除了onVsyncCallback之外,下一步就是执行新的一次schedule进行相关的定时触发Vsync任务。
代码就是上面callback方法的
auto const scheduleResult =
mRegistration.schedule({.workDuration = mWorkDuration.count(),
.readyDuration = mReadyDuration.count(),
.earliestVsync = vsyncTime});
这里面又回到最开的是设置定时器任务了,启动下一个vsync的定时,这里其实就可以知道,也就是app发起第一次的vsync请求,一般都会有两个vsync定时任务哈。
3、app的vsync继续请求和sf的vsync申请
针对上面分析的已经知道了第一次的app请求vsync情况,但是一般app都是会很多次的vsync连续请求,因为比较少见就一次刷新情况。那么看看产生连续的vsync请求会是什么样。
明显看到第二次的requestVsync就和 第一次不一样了,这个时候的并没有唤醒app的EventThread线程。因为前面代码就说了这个情况
void EventThread::requestNextVsync(const sp<EventThreadConnection>& connection) {
if (connection->vsyncRequest == VSyncRequest::None) { //如果这个是第一次申请进入一般这里就是None
connection->vsyncRequest = VSyncRequest::Single;
ATRACE_NAME("connection->vsyncRequest None notify_all");
mCondition.notify_all();//第一次则会通过mCondition唤醒等待
} else if (connection->vsyncRequest == VSyncRequest::SingleSuppressCallback) { //这里代表前一次刚刚已经进入vsync,准备进入停止回调状态
ATRACE_NAME("connection->vsyncRequest SingleSuppressCallback ");
connection->vsyncRequest = VSyncRequest::Single; //这里不需要唤醒,代表thread运行等待vsync,需要靠vsync时间到了才唤醒
}
}
也就是一旦Vsync的定时器启动后,EventThread的线程就会阻塞,知道定时器时间到了才会继续执行,后面的app的requestVsync根本不会唤醒。
那么他的唤醒靠谁呢?当然是靠第一次timeCallback时候的定时器触发啦
那么app的vsync就是这样循环只要不断有app的requestVsync,那么SurfaceFlinger的Vsync呢?
首先我们得知道sf的vsync也是由app层面进行queuebuffer后,通过跨进程setTransactionState方法调用到SurfaceFlinger端,SurfaceFlinger端会检测transation是否有相关的变化,有变化则触发申请vsync信号:
void SurfaceFlinger::setTransactionFlags(uint32_t mask, TransactionSchedule schedule,
const sp<IBinder>& applyToken, FrameHint frameHint) {
ATRACE_CALL();
modulateVsync(&VsyncModulator::setTransactionSchedule, schedule, applyToken);
std::atomic<int32_t> tmp = getTransactionFlags();
if (const bool scheduled = mTransactionFlags.fetch_or(mask) & mask; !scheduled) {
ATRACE_BEGIN("scheduleCommit");
scheduleCommit(frameHint);//申请vsync
ATRACE_END();
}
}
void SurfaceFlinger::scheduleCommit(FrameHint hint) {
//省略部分
mScheduler->scheduleFrame();
}
void MessageQueue::scheduleFrame() {
//调用到了registration->schedule
mVsync.scheduledFrameTime =
mVsync.registration->schedule({.workDuration = mVsync.workDuration.get().count(),
.readyDuration = 0,
.earliestVsync = mVsync.lastCallbackTime.count()});
}
//下面是VSyncCallbackRegistration的schedule
ScheduleResult VSyncCallbackRegistration::schedule(VSyncDispatch::ScheduleTiming scheduleTiming) {
if (!mValidToken) {
return std::nullopt;
}
return mDispatch.get().schedule(mToken, scheduleTiming);
}
上面的调用关系都很简单,没啥业务,直到调用到了dispatch的schedule才是核心
ScheduleResult VSyncDispatchTimerQueue::schedule(CallbackToken token,
ScheduleTiming scheduleTiming) {
ScheduleResult result;
{
//这里又会调用到callback的schedule,其实就另一个重载方法,这里会获取出具体的wakeupTime等,是核心部分
result = callback->schedule(scheduleTiming, mTracker, now);
//这里会判断上面计算的wakeupTime是否小于mIntendedWakeupTime,小于才需要重新定时,如果大于就不需要了,直接return
if (callback->wakeupTime() < mIntendedWakeupTime - mTimerSlack) {
rearmTimerSkippingUpdateFor(now, it);//启动定时任务相关
}
}
return result;
}
来看看核心方法callback->schedule是怎么计算的这个wakeup等重要参数的:
ScheduleResult VSyncDispatchTimerQueueEntry::schedule(VSyncDispatch::ScheduleTiming timing,
VSyncTracker& tracker, nsecs_t now) {
//核心方法,给一个时间点,然后可以获取到这个时间点对于的硬件vsync,即上屏的vsync时间点就是nextVsyncTime
auto nextVsyncTime = tracker.nextAnticipatedVSyncTimeFrom(
std::max(timing.earliestVsync, now + timing.workDuration + timing.readyDuration));
//通过nextVsyncTime来获取唤醒时间部分
auto nextWakeupTime = nextVsyncTime - timing.workDuration - timing.readyDuration;
//省略
//上面计算出来的时间,然后包装到mArmedInfo中
mArmedInfo = {nextWakeupTime, nextVsyncTime, nextReadyTime};
//返回时间点
return getExpectedCallbackTime(nextVsyncTime, timing);
}
//其实是返回wakeuptime
nsecs_t getExpectedCallbackTime(nsecs_t nextVsyncTime,
const VSyncDispatch::ScheduleTiming& timing) {
return nextVsyncTime - timing.readyDuration - timing.workDuration;
}
核心方法是nextAnticipatedVSyncTimeFrom,这个方法很复杂,大家先把他当做个功能黑盒,后面会带大家详细分析。
所以Sf接收到跨进程transaction的请求后申请vsync,计算出来的vsync时间,从而得出wakeupTime,一般这里的wakeupTime会大于前面app
Vsync时候定时的wakeupTime,所以这里不会进行任何的定时。
结合trace看看vsync的情况:
sf的vsync申请触发部分情况:
和app vsync一起合作的解释部分:
这里展开一下app ,sf都被回调的trace,对应的代码就是上面的timeCallback方法:
4、app vsync结束部分
vsync有申请就肯定有结束,不可能app没有申请情况下,vsync还一直不断的运行,这样对于系统的功耗影响巨大,而且也是无用功,所以vsync坚持的原则就是有需要用时候app主动申请,不需要了就停止。
上面只分析了vsync怎么开始的,接下来分析vsync的结束部分逻辑
逻辑是如下情况:
时间到了timeCallback
-----》在onVsyncCallback里面会唤醒EventThread
-------》EventThread运行,但是因为没有app进行requestVsync了,所以vsyncRequest = 0
-----》EventThread继续执行时候发现没有vsyncRequest,故需要调用setVsyncEnbale(false)关闭定时
关闭定时器步骤trace:
该部分对应核心代码回顾:
app的EventThread执行时候,会把vsyncRequest变成0,代表没有app的vsync请求了
void EventThread::threadMain(std::unique_lock<std::mutex>& lock) {
DisplayEventConsumers consumers;
while (mState != State::Quit) {
std::optional<DisplayEventReceiver::Event> event;
ATRACE_FORMAT("threadMain");
bool vsyncRequested = false;
// Find connections that should consume this event.
auto it = mDisplayEventConnections.begin();
while (it != mDisplayEventConnections.end()) {
if (const auto connection = it->promote()) {
//这里会进行遍历connection,有vsync那么就为true,没有就是false
vsyncRequested |= connection->vsyncRequest != VSyncRequest::None;
//注意这里会对每个connection的vsyncRequest进行改变,注意这里的需要event不为null
if (event && shouldConsumeEvent(*event, connection)) {
consumers.push_back(connection);
}
++it;
} else {
it = mDisplayEventConnections.erase(it);
}
}
}
//这个方法里面会改变每个connection的vsyncRequest
bool EventThread::shouldConsumeEvent(const DisplayEventReceiver::Event& event,
const sp<EventThreadConnection>& connection) const {
switch (event.header.type) {
case DisplayEventReceiver::DISPLAY_EVENT_MODE_CHANGE: {
return connection->mEventRegistration.test(
ISurfaceComposer::EventRegistration::modeChanged);
}
case DisplayEventReceiver::DISPLAY_EVENT_VSYNC:
switch (connection->vsyncRequest) {
case VSyncRequest::None:
return false;
//第一次SingleSuppressCallback后,就改成None
case VSyncRequest::SingleSuppressCallback:
connection->vsyncRequest = VSyncRequest::None;
return false;
case VSyncRequest::Single: {
if (throttleVsync()) {
return false;
}
//第一次Single后,就改成SingleSuppressCallback
connection->vsyncRequest = VSyncRequest::SingleSuppressCallback;
return true;
}
case VSyncRequest::Periodic:
if (throttleVsync()) {
return false;
}
return true;
default:
return event.vsync.count % vsyncPeriod(connection->vsyncRequest) == 0;
}
}
}
总结如下:
1、EventThread最核心的vsyncRequest是app进行requestVsync进行改变成Single
2、vsync定时时间到会回调触发EventThread加入个event,event就是来派发Vsync事件给具体的connection即app,通过socket方式
3、消费了event同时,需要吧前面app设置的vsyncRequest变成SingleSuppressCallback
4、下次如果vsync再次触发,但是没见到新的app吧vsyncRequest变成Single,如果还是上次的SingleSuppressCallback,那么vsyncRequest就变成了None,变成None之后就会调用
mVSyncSource->setVSyncEnabled(false)关闭vsync
本文链接:[转]结合perfetto-systrace分析Vsync部分源码流程 - https://h89.cn/archives/293.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。
评论已关闭