platform
R K 3568 + A n d r o i d 11 RK3568 + Android 11 R K 3568+Android11
overview
The official note is
that an "Application Not Responding" (ANR) error will be triggered if the UI thread of an Android application is blocked for too long. If the app is in the foreground, the system presents a dialog to the user, and the ANR dialog gives the user the option to force quit the app.
ANR processing flow
- First of all, the time definitions of the three ANRs:
through debugging: view the dispatchingTimeout of the window information in the adb shell dumpsys input, different windows may be different, it depends on the actual time setting, the following three values are all set to 5 seconds , the displayed value is 5 seconds.
App如Launhcer:
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
//当前的超时设置为5's
public static final int KEY_DISPATCHING_TIMEOUT_MS = 5 * 1000;
Navigation bar and status bar of SystemUI:
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
///
// 这个时间关联到的是: //
///
// Default input dispatching timeout in nanoseconds.
static final long DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS = 5000 * 1000000L;
h.dispatchingTimeoutNanos = DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS;
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// Default input dispatching timeout if there is no focused application or paused window
// from which to determine an appropriate dispatching timeout.
constexpr std::chrono::nanoseconds DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5s;
- Second, the string definition (under the frameworks/base/core/res directory):
<string name="anr_activity_application" msgid="8121716632960340680">"<xliff:g id="APPLICATION">%2$s</xliff:g>没有响应"</string>
<string name="anr_activity_process" msgid="3477362583767128667">"<xliff:g id="ACTIVITY">%1$s</xliff:g>没有响应"</string>
<string name="anr_application_process" msgid="4978772139461676184">"<xliff:g id="APPLICATION">%1$s</xliff:g>没有响应"</string>
<string name="anr_process" msgid="1664277165911816067">"进程“<xliff:g id="PROCESS">%1$s</xliff:g>”没有响应"</string>
<string name="force_close" msgid="9035203496368973803">"确定"</string>
<string name="report" msgid="2149194372340349521">"报告"</string>
<string name="wait" msgid="7765985809494033348">"等待"</string>
timing
key code
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::doNotifyAnrLockedInterruptible(CommandEntry* commandEntry) {
sp<IBinder> token =
commandEntry->inputChannel ? commandEntry->inputChannel->getConnectionToken() : nullptr;
mLock.unlock();
const nsecs_t timeoutExtension =
mPolicy->notifyAnr(commandEntry->inputApplicationHandle, token, commandEntry->reason);
mLock.lock();
if (timeoutExtension > 0) {
extendAnrTimeoutsLocked(commandEntry->inputApplicationHandle, token, timeoutExtension);
} else {
// stop waking up for events in this connection, it is already not responding
sp<Connection> connection = getConnectionLocked(token);
if (connection == nullptr) {
return;
}
cancelEventsForAnrLocked(connection);
}
}
void InputDispatcher::onAnrLocked(const sp<Connection>& connection) {
// Since we are allowing the policy to extend the timeout, maybe the waitQueue
// is already healthy again. Don't raise ANR in this situation
if (connection->waitQueue.empty()) {
ALOGI("Not raising ANR because the connection %s has recovered",
connection->inputChannel->getName().c_str());
return;
}
/**
* The "oldestEntry" is the entry that was first sent to the application. That entry, however,
* may not be the one that caused the timeout to occur. One possibility is that window timeout
* has changed. This could cause newer entries to time out before the already dispatched
* entries. In that situation, the newest entries caused ANR. But in all likelihood, the app
* processes the events linearly. So providing information about the oldest entry seems to be
* most useful.
*/
DispatchEntry* oldestEntry = *connection->waitQueue.begin();
const nsecs_t currentWait = now() - oldestEntry->deliveryTime;
std::string reason =
android::base::StringPrintf("%s is not responding. Waited %" PRId64 "ms for %s",
connection->inputChannel->getName().c_str(),
ns2ms(currentWait),
oldestEntry->eventEntry->getDescription().c_str());
updateLastAnrStateLocked(getWindowHandleLocked(connection->inputChannel->getConnectionToken()),
reason);
std::unique_ptr<CommandEntry> commandEntry =
std::make_unique<CommandEntry>(&InputDispatcher::doNotifyAnrLockedInterruptible);
commandEntry->inputApplicationHandle = nullptr;
commandEntry->inputChannel = connection->inputChannel;
commandEntry->reason = std::move(reason);
postCommandLocked(std::move(commandEntry));
}
/**
* Check if any of the connections' wait queues have events that are too old.
* If we waited for events to be ack'ed for more than the window timeout, raise an ANR.
* Return the time at which we should wake up next.
*/
nsecs_t InputDispatcher::processAnrsLocked() {
const nsecs_t currentTime = now();
nsecs_t nextAnrCheck = LONG_LONG_MAX;
// Check if we are waiting for a focused window to appear. Raise ANR if waited too long
if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
if (currentTime >= *mNoFocusedWindowTimeoutTime) {
onAnrLocked(mAwaitedFocusedApplication);
mAwaitedFocusedApplication.clear();
return LONG_LONG_MIN;
} else {
// Keep waiting
const nsecs_t millisRemaining = ns2ms(*mNoFocusedWindowTimeoutTime - currentTime);
ALOGW("Still no focused window. Will drop the event in %" PRId64 "ms", millisRemaining);
nextAnrCheck = *mNoFocusedWindowTimeoutTime;
}
}
// Check if any connection ANRs are due
nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());
if (currentTime < nextAnrCheck) {
// most likely scenario
return nextAnrCheck; // everything is normal. Let's check again at nextAnrCheck
}
// If we reached here, we have an unresponsive connection.
sp<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());
if (connection == nullptr) {
ALOGE("Could not find connection for entry %" PRId64, mAnrTracker.firstTimeout());
return nextAnrCheck;
}
connection->responsive = false;
// Stop waking up for this unresponsive connection
mAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());
//ANR触发位置
onAnrLocked(connection);
return LONG_LONG_MIN;
}
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX;
{
// acquire lock
std::scoped_lock _l(mLock);
mDispatcherIsAlive.notify_all();
// Run a dispatch loop if there are no pending commands.
// The dispatch loop might enqueue commands to run afterwards.
if (!haveCommandsLocked()) {
dispatchOnceInnerLocked(&nextWakeupTime);
}
// Run all pending commands if there are any.
// If any commands were run then force the next poll to wake up immediately.
if (runCommandsLockedInterruptible()) {
nextWakeupTime = LONG_LONG_MIN;
}
// If we are still waiting for ack on some events,
// we might have to wake up earlier to check if an app is anr'ing.
const nsecs_t nextAnrCheck = processAnrsLocked();
nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
// We are about to enter an infinitely long sleep, because we have no commands or
// pending or queued events
if (nextWakeupTime == LONG_LONG_MAX) {
mDispatcherEnteredIdle.notify_all();
}
} // release lock
// Wait for callback or timeout or wake. (make sure we round up, not down)
nsecs_t currentTime = now();
int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
mLooper->pollOnce(timeoutMillis);
}
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
nsecs_t NativeInputManager::notifyAnr(const sp<InputApplicationHandle>& inputApplicationHandle,
const sp<IBinder>& token, const std::string& reason) {
#if DEBUG_INPUT_DISPATCHER_POLICY
ALOGD("notifyANR");
#endif
ATRACE_CALL();
JNIEnv* env = jniEnv();
ScopedLocalFrame localFrame(env);
jobject inputApplicationHandleObj =
getInputApplicationHandleObjLocalRef(env, inputApplicationHandle);
jobject tokenObj = javaObjectForIBinder(env, token);
jstring reasonObj = env->NewStringUTF(reason.c_str());
jlong newTimeout = env->CallLongMethod(mServiceObj,
gServiceClassInfo.notifyANR, inputApplicationHandleObj, tokenObj,
reasonObj);
if (checkAndClearExceptionFromCallback(env, "notifyANR")) {
newTimeout = 0; // abort dispatch
} else {
assert(newTimeout >= 0);
}
return newTimeout;
}
GET_METHOD_ID(gServiceClassInfo.notifyANR, clazz,
"notifyANR",
"(Landroid/view/InputApplicationHandle;Landroid/os/IBinder;Ljava/lang/String;)J");
frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
// Native callback.
private long notifyANR(InputApplicationHandle inputApplicationHandle, IBinder token,
String reason) {
return mWindowManagerCallbacks.notifyANR(inputApplicationHandle,
token, reason);
}
frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java
/**
* Notifies the window manager about an application that is not responding.
* Returns a new timeout to continue waiting in nanoseconds, or 0 to abort dispatch.
*
* Called by the InputManager.
*/
@Override
public long notifyANR(InputApplicationHandle inputApplicationHandle, IBinder token,
String reason) {
final long startTime = SystemClock.uptimeMillis();
try {
return notifyANRInner(inputApplicationHandle, token, reason);
} finally {
// Log the time because the method is called from InputDispatcher thread. It shouldn't
// take too long that may affect input response time.
Slog.d(TAG_WM, "notifyANR took " + (SystemClock.uptimeMillis() - startTime) + "ms");
}
}
private long notifyANRInner(InputApplicationHandle inputApplicationHandle, IBinder token,
String reason) {
ActivityRecord activity = null;
WindowState windowState = null;
boolean aboveSystem = false;
int windowPid = INVALID_PID;
preDumpIfLockTooSlow();
//TODO(b/141764879) Limit scope of wm lock when input calls notifyANR
synchronized (mService.mGlobalLock) {
// Check if we can blame a window
if (token != null) {
windowState = mService.mInputToWindowMap.get(token);
if (windowState != null) {
activity = windowState.mActivityRecord;
windowPid = windowState.mSession.mPid;
// Figure out whether this window is layered above system windows.
// We need to do this here to help the activity manager know how to
// layer its ANR dialog.
aboveSystem = isWindowAboveSystem(windowState);
}
}
// Check if we can blame an embedded window
if (token != null && windowState == null) {
EmbeddedWindow embeddedWindow = mService.mEmbeddedWindowController.get(token);
if (embeddedWindow != null) {
windowPid = embeddedWindow.mOwnerPid;
WindowState hostWindowState = embeddedWindow.mHostWindowState;
if (hostWindowState == null) {
// The embedded window has no host window and we cannot easily determine
// its z order. Try to place the anr dialog as high as possible.
aboveSystem = true;
} else {
aboveSystem = isWindowAboveSystem(hostWindowState);
}
}
}
// Check if we can blame an activity. If we don't have an activity to blame, pull out
// the token passed in via input application handle. This can happen if there are no
// focused windows but input dispatcher knows the focused app.
if (activity == null && inputApplicationHandle != null) {
activity = ActivityRecord.forTokenLocked(inputApplicationHandle.token);
}
if (windowState != null) {
Slog.i(TAG_WM, "Input event dispatching timed out "
+ "sending to " + windowState.mAttrs.getTitle()
+ ". Reason: " + reason);
} else if (activity != null) {
Slog.i(TAG_WM, "Input event dispatching timed out "
+ "sending to application " + activity.stringName
+ ". Reason: " + reason);
} else {
Slog.i(TAG_WM, "Input event dispatching timed out "
+ ". Reason: " + reason);
}
mService.saveANRStateLocked(activity, windowState, reason);
}
// All the calls below need to happen without the WM lock held since they call into AM.
mService.mAtmInternal.saveANRState(reason);
if (activity != null && activity.appToken != null) {
// Notify the activity manager about the timeout and let it decide whether
// to abort dispatching or keep waiting.
final boolean abort = activity.keyDispatchingTimedOut(reason, windowPid);
if (!abort) {
// The activity manager declined to abort dispatching.
// Wait a bit longer and timeout again later.
return activity.mInputDispatchingTimeoutNanos;
}
} else if (windowState != null || windowPid != INVALID_PID) {
// Notify the activity manager about the timeout and let it decide whether
// to abort dispatching or keep waiting.
long timeout = mService.mAmInternal.inputDispatchingTimedOut(windowPid, aboveSystem,
reason);
if (timeout >= 0) {
// The activity manager declined to abort dispatching.
// Wait a bit longer and timeout again later.
return timeout * 1000000L; // nanoseconds
}
}
return 0; // abort dispatching
}
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
if (checkCallingPermission(FILTER_EVENTS) != PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires permission " + FILTER_EVENTS);
}
ProcessRecord proc;
long timeout;
synchronized (this) {
synchronized (mPidsSelfLocked) {
proc = mPidsSelfLocked.get(pid);
}
timeout = proc != null ? proc.getInputDispatchingTimeout() : KEY_DISPATCHING_TIMEOUT_MS;
}
if (inputDispatchingTimedOut(proc, null, null, null, null, aboveSystem, reason)) {
return -1;
}
return timeout;
}
final class UiHandler extends Handler {
//Ignored....
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_ERROR_UI_MSG: {
mAppErrors.handleShowAppErrorUi(msg);
ensureBootCompleted();
} break;
case SHOW_NOT_RESPONDING_UI_MSG: {
mAppErrors.handleShowAnrUi(msg);
ensureBootCompleted();
} break;
//Ignored .....
}
frameworks/base/services/core/java/com/android/server/am/AnrHelper.java
void appNotResponding(ProcessRecord anrProcess, String annotation) {
appNotResponding(anrProcess, null /* activityShortComponentName */, null /* aInfo */,
null /* parentShortComponentName */, null /* parentProcess */,
false /* aboveSystem */, annotation);
}
void appNotResponding(ProcessRecord anrProcess, String activityShortComponentName,
ApplicationInfo aInfo, String parentShortComponentName,
WindowProcessController parentProcess, boolean aboveSystem, String annotation) {
synchronized (mAnrRecords) {
mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName, aInfo,
parentShortComponentName, parentProcess, aboveSystem, annotation));
}
startAnrConsumerIfNeeded();
}
private void startAnrConsumerIfNeeded() {
if (mRunning.compareAndSet(false, true)) {
new AnrConsumerThread().start();
}
}
frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
String parentShortComponentName, WindowProcessController parentProcess,
boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
//Ignored...
// mUiHandler can be null if the AMS is constructed with injector only. This will only
// happen in tests.
if (mService.mUiHandler != null) {
// Bring up the infamous App Not Responding dialog
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(this, aInfo, aboveSystem);
mService.mUiHandler.sendMessage(msg);
}
}
}
class ErrorDialogControlle
//AppErrors 调用
void showAnrDialogs(AppNotRespondingDialog.Data data) {
List<Context> contexts = getDisplayContexts(isSilentAnr() /* lastUsedOnly */);
mAnrDialogs = new ArrayList<>();
for (int i = contexts.size() - 1; i >= 0; i--) {
final Context c = contexts.get(i);
mAnrDialogs.add(new AppNotRespondingDialog(mService, c, data));
}
mService.mUiHandler.post(() -> {
List<AppNotRespondingDialog> dialogs;
synchronized (mService) {
dialogs = mAnrDialogs;
}
if (dialogs != null) {
forAllDialogs(dialogs, Dialog::show);
}
});
}
frameworks/base/services/core/java/com/android/server/am/AppErrors.java
void handleShowAnrUi(Message msg) {
List<VersionedPackage> packageList = null;
synchronized (mService) {
AppNotRespondingDialog.Data data = (AppNotRespondingDialog.Data) msg.obj;
final ProcessRecord proc = data.proc;
if (proc == null) {
Slog.e(TAG, "handleShowAnrUi: proc is null");
return;
}
if (!proc.isPersistent()) {
packageList = proc.getPackageListWithVersionCode();
}
if (proc.getDialogController().hasAnrDialogs()) {
Slog.e(TAG, "App already has anr dialog: " + proc);
MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,
AppNotRespondingDialog.ALREADY_SHOWING);
return;
}
boolean showBackground = Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.ANR_SHOW_BACKGROUND, 0) != 0;
if (mService.mAtmInternal.canShowErrorDialogs() || showBackground) {
proc.getDialogController().showAnrDialogs(data);
//Ignored .....
}
frameworks/base/services/core/java/com/android/server/am/AppNotRespondingDialog.java
对话框的代码就省略吧.
至此, 显示ANR对话框的流程走完
InputDispatcher中窗口句柄
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
void SurfaceFlinger::updateInputFlinger() {
ATRACE_CALL();
if (!mInputFlinger) {
return;
}
if (mVisibleRegionsDirty || mInputInfoChanged) {
mInputInfoChanged = false;
updateInputWindowInfo();
} else if (mInputWindowCommands.syncInputWindows) {
// If the caller requested to sync input windows, but there are no
// changes to input windows, notify immediately.
setInputWindowsFinished();
}
mInputWindowCommands.clear();
}
void SurfaceFlinger::updateInputWindowInfo() {
std::vector<InputWindowInfo> inputHandles;
mDrawingState.traverseInReverseZOrder([&](Layer* layer) {
if (layer->needsInputInfo()) {
// When calculating the screen bounds we ignore the transparent region since it may
// result in an unwanted offset.
inputHandles.push_back(layer->fillInputInfo());
}
});
mInputFlinger->setInputWindows(inputHandles,
mInputWindowCommands.syncInputWindows ? mSetInputWindowsListener
: nullptr);
}
frameworks/native/services/inputflinger/InputManager.cpp
void InputManager::setInputWindows(const std::vector<InputWindowInfo>& infos,
const sp<ISetInputWindowsListener>& setInputWindowsListener) {
std::unordered_map<int32_t, std::vector<sp<InputWindowHandle>>> handlesPerDisplay;
std::vector<sp<InputWindowHandle>> handles;
for (const auto& info : infos) {
handlesPerDisplay.emplace(info.displayId, std::vector<sp<InputWindowHandle>>());
handlesPerDisplay[info.displayId].push_back(new BinderWindowHandle(info));
}
mDispatcher->setInputWindows(handlesPerDisplay);
if (setInputWindowsListener) {
setInputWindowsListener->onSetInputWindowsFinished();
}
}
frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
///
// 通过InputManagerService 添加窗口句柄 //
///
/**
* Called from InputManagerService, update window handle list by displayId that can receive input.
* A window handle contains information about InputChannel, Touch Region, Types, Focused,...
* If set an empty list, remove all handles from the specific display.
* For focused handle, check if need to change and send a cancel event to previous one.
* For removed handle, check if need to send a cancel event if already in touch.
*/
void InputDispatcher::setInputWindowsLocked(){
updateWindowHandlesForDisplayLocked...
}
///
// 更新mWindowHandlesByDisplay中 //
///
void InputDispatcher::updateWindowHandlesForDisplayLocked(
const std::vector<sp<InputWindowHandle>>& inputWindowHandles, int32_t displayId) {
//Ignored.....
// Insert or replace
mWindowHandlesByDisplay[displayId] = newHandles;
}
///
// 从mWindowHandlesByDisplay中查询当前窗口 //
///
sp<InputWindowHandle> InputDispatcher::getWindowHandleLocked(
const sp<IBinder>& windowHandleToken) const {
if (windowHandleToken == nullptr) {
return nullptr;
}
for (auto& it : mWindowHandlesByDisplay) {
const std::vector<sp<InputWindowHandle>> windowHandles = it.second;
for (const sp<InputWindowHandle>& windowHandle : windowHandles) {
if (windowHandle->getToken() == windowHandleToken) {
return windowHandle;
}
}
}
return nullptr;
}
nsecs_t InputDispatcher::getDispatchingTimeoutLocked(const sp<IBinder>& token) {
sp<InputWindowHandle> window = getWindowHandleLocked(token);
if (window != nullptr) {
return window->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT).count();
}
return DEFAULT_INPUT_DISPATCHING_TIMEOUT.count();
}
frameworks/base/services/core/java/com/android/server/wm/WindowProcessController.java
public long getInputDispatchingTimeout() {
synchronized (mAtm.mGlobalLock) {
return isInstrumenting() || isUsingWrapper()
? INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT_MS : KEY_DISPATCHING_TIMEOUT_MS;
}
}
frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java
public long getInputDispatchingTimeout() {
return mWindowProcessController.getInputDispatchingTimeout();
}
扩展
编译及替换
#编译
mmm frameworks/native/services/inputflinger/ -j2
#替换
adb push out/target/product/rk3566_r/system/bin/inputflinger /system/bin/
adb push out/target/product/rk3566_r/system/lib/libinputflinger.so /system/lib/
adb push out/target/product/rk3566_r/system/lib64/libinputflinger.so /system/lib64/
调试: adb shell dumpsys input
0: name='Window{57ce99a u0 NavigationBar0}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=false, canReceiveKeys=false, flags=0x21840068, type=0x000007e3, frame=[0,752][1280,800], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,752][1280,800], inputFeatures=0x00000000, ownerPid=614, ownerUid=10119, dispatchingTimeout=7000ms
1: name='Window{61c1033 u0 StatusBar}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=false, canReceiveKeys=false, flags=0x81800408, type=0x000007d0, frame=[0,0][1280,24], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,0][1280,24], inputFeatures=0x00000000, ownerPid=614, ownerUid=10119, dispatchingTimeout=7000ms
2: name='Window{e0e47c4 u0 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=true, hasWallpaper=true, visible=true, canReceiveKeys=true, flags=0x81910120, type=0x00000001, frame=[0,0][1280,800], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,0][1280,800], inputFeatures=0x00000000, ownerPid=1039, ownerUid=10121, dispatchingTimeout=15000ms
3: name='Window{bbadc9c u0 com.android.systemui.ImageWallpaper}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x00014318, type=0x000007dd, frame=[-64,-304][1344,1104], globalScale=1.000000, windowScale=(0.909091,0.909091), touchableRegion=[-64,-304][1344,1104], inputFeatures=0x00000000, ownerPid=614, ownerUid=10119, dispatchingTimeout=7000ms
参考