安卓InputDispatching Timeout ANR 流程
- 1 ANR的检测逻辑有两个参与者: 观测者A和被观测者B,当然,这两者是不在同一个线程中的。
- 2 A在调用B中的逻辑时,同时在A中保存一个标记F,然后做个延时操作C,延时时间设为T,这一步称为: 埋雷 。
- 3 B中的逻辑如果被执行到,就会通知A去清除标记F,并且通知A解除C,这一步称为: 拆雷 。
- 4 如果C没被拆除,那么在时间T后就会被触发,就会去检测标记F是否还在,如果在,就说明B没有在指定的时间T内完成,那么就提示B发生了ANR,这一步称为: 爆雷 。
- 5 由于A和B是在不同线程中的,所以B即使死循环,也不会影响C的检测过程。
所以,我们可以将ANR更精炼的总结为: 埋雷、拆雷和爆雷三个步骤 。
InputDispatching Timeout: 输入事件(包括按键和触屏事件)在5秒内无响应,就会弹出 ANR 提示框,供用户选择继续等待程序响应或者关闭这个应用程序(也就是杀掉这个应用程序的进程)。输入超时类的 ANR 可以细分为以下两类:
1)处理消息超时: 这一类是指因为消息处理超时而发生的 ANR,在 log,会看到 “Input dispatching timed out (Waiting because the focused window has not finished processing
the input events that were previously delivered to it.)”
2)无法获取焦点: 这一类通常因为新窗口创建慢或旧窗口退出慢而造成窗口无法获得焦点从而发生 ANR,典型 Log “Reason: Waiting because no window has focus but there is a focused application
that may eventually add a window when it finishes starting up.”
这里不分析没有焦点的情形,只分析处理消息超时流程
在 input子系统中,正常逻辑是:InputDispatcher 负责将输入事件分发给 UI 主线程。UI主线程接收到输入事件后,使用 InputConsumer 来处理事件。经过一系列的 InputStage 完成事件分发后,执行finishInputEvent() 方法来告知 InputDispatcher 事件已经处理完成。InputDispatcher 中使用handleReceiveCallback() 方法来处理 UI 主线程返回的消息,最终将 dispatchEntry事件从等待队列中移除。
http://aospxref.com/android-13.0.0_r3/xref/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
3975 void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
// 打开下列的开关,会打印对应的log
3976 if (DEBUG_INBOUND_EVENT_DETAILS) {
3977 ALOGD("notifyMotion - id=%" PRIx32 " eventTime=%" PRId64 ", deviceId=%d, source=0x%x, "
3978 "displayId=%" PRId32 ", policyFlags=0x%x, "
3979 "action=0x%x, actionButton=0x%x, flags=0x%x, metaState=0x%x, buttonState=0x%x, "
3980 "edgeFlags=0x%x, xPrecision=%f, yPrecision=%f, xCursorPosition=%f, "
3981 "yCursorPosition=%f, downTime=%" PRId64,
3982 args->id, args->eventTime, args->deviceId, args->source, args->displayId,
3983 args->policyFlags, args->action, args->actionButton, args->flags, args->metaState,
3984 args->buttonState, args->edgeFlags, args->xPrecision, args->yPrecision,
3985 args->xCursorPosition, args->yCursorPosition, args->downTime);
3986 for (uint32_t i = 0; i < args->pointerCount; i++) {
。。。。
4047 // Just enqueue a new motion event.// 创建 MotionEntry 对象
4048 std::unique_ptr<MotionEntry> newEntry =
4049 std::make_unique<MotionEntry>(args->id, args->eventTime, args->deviceId,
4050 args->source, args->displayId, policyFlags,
4051 args->action, args->actionButton, args->flags,
4052 args->metaState, args->buttonState,
4053 args->classification, args->edgeFlags,
4054 args->xPrecision, args->yPrecision,
4055 args->xCursorPosition, args->yCursorPosition,
4056 args->downTime, args->pointerCount,
4057 args->pointerProperties, args->pointerCoords);
4058
4059 if (args->id != android::os::IInputConstants::INVALID_INPUT_EVENT_ID &&
4060 IdGenerator::getSource(args->id) == IdGenerator::Source::INPUT_READER &&
4061 !mInputFilterEnabled) {
4062 const bool isDown = args->action == AMOTION_EVENT_ACTION_DOWN;
4063 mLatencyTracker.trackListener(args->id, isDown, args->eventTime, args->readTime);
4064 }
4065
// 将 MotionEntry 事件保存到 mInboundQueue 队列中;如果保存之前 mInboundQueue是为空的,则 needWake 为true。
4066 needWake = enqueueInboundEventLocked(std::move(newEntry));
4067 mLock.unlock();
4068 } // release lock
4069 // 这里分析直接去唤醒inputdispatcher 线程
4070 if (needWake) {
4071 mLooper->wake();
4072 }
4073 }
InputDispatcher 是个线程,会循环执行 dispatchOnce 方法
602 void InputDispatcher::dispatchOnce() {
603 nsecs_t nextWakeupTime = LONG_LONG_MAX;// 局部空间,获取同步锁
604 { // acquire lock
605 std::scoped_lock _l(mLock);
606 mDispatcherIsAlive.notify_all();
607
608 // Run a dispatch loop if there are no pending commands.
609 // The dispatch loop might enqueue commands to run afterwards.// 如果没有 mCommandQueue ,没有 command 命令的话,
// 则执行 dispatchOnceInnerLocked,获取到下一次唤醒的时间:nextWakeupTime
// 1)如果有motion 事件,调用 dispatchOnceInnerLocked 方法
610 if (!haveCommandsLocked()) {
611 dispatchOnceInnerLocked(&nextWakeupTime);
612 }
613 // 执行command 命令,如果有的话,直接返回 true,然后马上唤醒线程
616 if (runCommandsLockedInterruptable()) {
617 nextWakeupTime = LONG_LONG_MIN;
618 }
619 // 处理anr 或者获取到下一次anr 的时间
// 2)获取超时anr 的时间:processAnrsLocked
622 const nsecs_t nextAnrCheck = processAnrsLocked();
623 nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
624
625 // We are about to enter an infinitely long sleep, because we have no commands or
626 // pending or queued events// 如果唤醒时间为最大,则表明没有input 事件,是idle 空闲状态
627 if (nextWakeupTime == LONG_LONG_MAX) {
628 mDispatcherEnteredIdle.notify_all();
629 }
630 } // release lock
631
632 // Wait for callback or timeout or wake. (make sure we round up, not down)
633 nsecs_t currentTime = now();
// 获取到唤醒线程的时间
// 调用 mLooper->wake 函数也可以唤醒线程
634 int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
635 mLooper->pollOnce(timeoutMillis);
636 }
1)如果有motion 事件,调用 dispatchOnceInnerLocked 方法
718 void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
719 nsecs_t currentTime = now();
。。。
// 初始 mPendingEvent 为null
746 if (!mPendingEvent) {
// 前面notifymotion 增加了 mInboundQueue
747 if (mInboundQueue.empty()) {
。。。
770 } else {
771 // Inbound queue has at least one entry.
// 这里去获取到motion 事件
772 mPendingEvent = mInboundQueue.front();
773 mInboundQueue.pop_front();
774 traceInboundQueueLengthLocked();
775 }
776
777 // Poke user activity for this event.
778 if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
779 pokeUserActivityLocked(*mPendingEvent);
780 }
781 }
。。。
798 switch (mPendingEvent->type) {
868 case EventEntry::Type::MOTION: {
869 std::shared_ptr<MotionEntry> motionEntry =
870 std::static_pointer_cast<MotionEntry>(mPendingEvent);
871 if (dropReason == DropReason::NOT_DROPPED && isAppSwitchDue) {
872 dropReason = DropReason::APP_SWITCH;
873 }
874 if (dropReason == DropReason::NOT_DROPPED && isStaleEvent(currentTime, *motionEntry)) {
875 dropReason = DropReason::STALE;
876 }
877 if (dropReason == DropReason::NOT_DROPPED && mNextUnblockedEvent) {
878 dropReason = DropReason::BLOCKED;
879 }
// dispatchMotionLocked 方法去分发motion 事件
880 done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
881 break;
882 }
。。。
// 一般是返回true,表示分发motion 事件到应用了
902 if (done) {
903 if (dropReason != DropReason::NOT_DROPPED) {
904 dropInboundEventLocked(*mPendingEvent, dropReason);
905 }
906 mLastDropReason = dropReason;
907
// 返回true,则重新设置 mPendingEvent 为null
908 releasePendingEventLocked();
// 设置 nextWakeupTime 为最小,马上唤醒
909 *nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately
910 }
911 }
// dispatchMotionLocked 方法去分发motion 事件
1634 bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,
1635 DropReason* dropReason, nsecs_t* nextWakeupTime) {
// 会打印对应的trace
1636 ATRACE_CALL();
。。。
1652 const bool isPointerEvent = isFromSource(entry->source, AINPUT_SOURCE_CLASS_POINTER);
1653
1654 // Identify targets.
1655 std::vector<InputTarget> inputTargets;
1656
1657 bool conflictingPointerActions = false;
1658 InputEventInjectionResult injectionResult;
1659 if (isPointerEvent) {
1660 // Pointer event. (eg. touchscreen)// 去根据xy 的坐标点,去找到触摸的 window
1661 injectionResult =
1662 findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
1663 &conflictingPointerActions);
。。。
// 增加到全局触摸事件
1687 addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(*entry));
1688
1689 // Dispatch the motion.
1690 if (conflictingPointerActions) {
1691 CancelationOptions options(CancelationOptions::CANCEL_POINTER_EVENTS,
1692 "conflicting pointer actions");
1693 synthesizeCancelationEventsForAllConnectionsLocked(options);
1694 }
// 分发event
1695 dispatchEventLocked(currentTime, entry, inputTargets);
1696 return true;
1697 }
// 分发event
1753 void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
1754 std::shared_ptr<EventEntry> eventEntry,
1755 const std::vector<InputTarget>& inputTargets) {
1756 ATRACE_CALL();
1757 if (DEBUG_DISPATCH_CYCLE) {
1758 ALOGD("dispatchEventToCurrentInputTargets");
1759 }
1760
1761 updateInteractionTokensLocked(*eventEntry, inputTargets);
1762
1763 ALOG_ASSERT(eventEntry->dispatchInProgress); // should already have been set to true
1764
1765 pokeUserActivityLocked(*eventEntry);
1766
1767 for (const InputTarget& inputTarget : inputTargets) {// 找到对应的窗口的 connection
1768 sp<Connection> connection =
1769 getConnectionLocked(inputTarget.inputChannel->getConnectionToken());
1770 if (connection != nullptr) {
1771 prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);
1772 } else {
1773 if (DEBUG_FOCUS) {
1774 ALOGD("Dropping event delivery to target with channel '%s' because it "
1775 "is no longer registered with the input dispatcher.",
1776 inputTarget.inputChannel->getName().c_str());
1777 }
1778 }
1779 }
1780 }
准备分发事件给应用 prepareDispatchCycleLocked
2887 void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
2888 const sp<Connection>& connection,
2889 std::shared_ptr<EventEntry> eventEntry,
2890 const InputTarget& inputTarget) {
2891 if (ATRACE_ENABLED()) {
2892 std::string message =
2893 StringPrintf("prepareDispatchCycleLocked(inputChannel=%s, id=0x%" PRIx32 ")",
2894 connection->getInputChannelName().c_str(), eventEntry->id);
2895 ATRACE_NAME(message.c_str());
2896 }
。。。。。
2944
2945 // Not splitting. Enqueue dispatch entries for the event as is.
2946 enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
2947 }
2949 void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
2950 const sp<Connection>& connection,
2951 std::shared_ptr<EventEntry> eventEntry,
2952 const InputTarget& inputTarget) {
2953 if (ATRACE_ENABLED()) {
2954 std::string message =
2955 StringPrintf("enqueueDispatchEntriesLocked(inputChannel=%s, id=0x%" PRIx32 ")",
2956 connection->getInputChannelName().c_str(), eventEntry->id);
2957 ATRACE_NAME(message.c_str());
2958 }
2959
2960 bool wasEmpty = connection->outboundQueue.empty();
2961
// 将触摸事件保存到 outboundQueue 中
2962 // Enqueue dispatch entries for the requested modes.
2963 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2964 InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
2965 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2966 InputTarget::FLAG_DISPATCH_AS_OUTSIDE);
2967 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2968 InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);
2969 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2970 InputTarget::FLAG_DISPATCH_AS_IS);
2971 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2972 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
2973 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
2974 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);
2975
2976 // If the outbound queue was previously empty, start the dispatch cycle going.
2977 if (wasEmpty && !connection->outboundQueue.empty()) {
// 开始socket 通信去分发给应用 startDispatchCycleLocked
2978 startDispatchCycleLocked(currentTime, connection);
2979 }
2980 }
// 开始socket 通信去分发给应用 startDispatchCycleLocked
3214 void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
3215 const sp<Connection>& connection) {
3216 if (ATRACE_ENABLED()) {
3217 std::string message = StringPrintf("startDispatchCycleLocked(inputChannel=%s)",
3218 connection->getInputChannelName().c_str());
3219 ATRACE_NAME(message.c_str());
3220 }
3221 if (DEBUG_DISPATCH_CYCLE) {
3222 ALOGD("channel '%s' ~ startDispatchCycle", connection->getInputChannelName().c_str());
3223 }
3224
3225 while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
3226 DispatchEntry* dispatchEntry = connection->outboundQueue.front();
3227 dispatchEntry->deliveryTime = currentTime;
// 获取到应用设置的超时事件,一般为 5 秒
3228 const std::chrono::nanoseconds timeout = getDispatchingTimeoutLocked(connection);
// 设置timeout 的时间
3229 dispatchEntry->timeoutTime = currentTime + timeout.count();
。。。
3252 case EventEntry::Type::MOTION: {
3253 const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);
。。。
// socket 通信发送给 应用,给到应用去处理了
3285 // Publish the motion event.
3286 status = connection->inputPublisher
3287 .publishMotionEvent(dispatchEntry->seq,
3288 dispatchEntry->resolvedEventId,
3289 motionEntry.deviceId, motionEntry.source,
3290 motionEntry.displayId, std::move(hmac),
3291 dispatchEntry->resolvedAction,
3292 motionEntry.actionButton,
3293 dispatchEntry->resolvedFlags,
3294 motionEntry.edgeFlags, motionEntry.metaState,
3295 motionEntry.buttonState,
3296 motionEntry.classification,
3297 dispatchEntry->transform,
3298 motionEntry.xPrecision, motionEntry.yPrecision,
3299 motionEntry.xCursorPosition,
3300 motionEntry.yCursorPosition,
3301 dispatchEntry->rawTransform,
3302 motionEntry.downTime, motionEntry.eventTime,
3303 motionEntry.pointerCount,
3304 motionEntry.pointerProperties, usingCoords);
3305 break;
3306 }
。。。。。
3383 // Re-enqueue the event on the wait queue.
// 将触摸事件从 outboundQueue 移除掉
3384 connection->outboundQueue.erase(std::remove(connection->outboundQueue.begin(),
3385 connection->outboundQueue.end(),
3386 dispatchEntry));
3387 traceOutboundQueueLength(*connection);
// 将其增加到 waitQueue 等待队列中
3388 connection->waitQueue.push_back(dispatchEntry);
// 如果应用没有die,则 将anr 超时时间保存到 mAnrTracker 中,还有对应的应用token
3389 if (connection->responsive) {
3390 mAnrTracker.insert(dispatchEntry->timeoutTime,
3391 connection->inputChannel->getConnectionToken());
3392 }
3393 traceWaitQueueLength(*connection);
3394 }
3395 }
2)获取超时anr 的时间:processAnrsLocked
622 const nsecs_t nextAnrCheck = processAnrsLocked();
// nextWakeupTime 由前面的分析,返回的是为 LONG_LONG_MIN,所以这里的唤醒时间为马上唤醒
623 nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
670 nsecs_t InputDispatcher::processAnrsLocked() {
671 const nsecs_t currentTime = now();
672 nsecs_t nextAnrCheck = LONG_LONG_MAX;// 下列是处理no focus 没有焦点的情况,这里不满足
674 if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
675 if (currentTime >= *mNoFocusedWindowTimeoutTime) {
676 processNoFocusedWindowAnrLocked();
677 mAwaitedFocusedApplication.reset();
678 mNoFocusedWindowTimeoutTime = std::nullopt;
679 return LONG_LONG_MIN;
680 } else {
681 // Keep waiting. We will drop the event when mNoFocusedWindowTimeoutTime comes.
682 nextAnrCheck = *mNoFocusedWindowTimeoutTime;
683 }
684 }
685
// 获取到前面保存到anr 的时间,即前面当前的时间 + timeout 5 秒
687 nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());
// 当前的时间是小于 nextAnrCheck,所以返回 nextAnrCheck的时间
688 if (currentTime < nextAnrCheck) { // most likely scenario
689 return nextAnrCheck; // everything is normal. Let's check again at nextAnrCheck
690 }
691
692 // If we reached here, we have an unresponsive connection.
693 sp<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());
694 if (connection == nullptr) {
695 ALOGE("Could not find connection for entry %" PRId64, mAnrTracker.firstTimeout());
696 return nextAnrCheck;
697 }
698 connection->responsive = false;
699 // Stop waking up for this unresponsive connection
700 mAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());
701 onAnrLocked(connection);
702 return LONG_LONG_MIN;
703 }
但是接下有个处理 :这里返回时 MIN 的,即马上唤醒。所以再走一次线程dispatchOnce 方法
nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
602 void InputDispatcher::dispatchOnce() {
603 nsecs_t nextWakeupTime = LONG_LONG_MAX;
604 { // acquire lock
605 std::scoped_lock _l(mLock);
606 mDispatcherIsAlive.notify_all();
607 // 这时候没有inboundqueue 了
610 if (!haveCommandsLocked()) {
611 dispatchOnceInnerLocked(&nextWakeupTime);
612 }
613
// 也没有cammand
616 if (runCommandsLockedInterruptable()) {
617 nextWakeupTime = LONG_LONG_MIN;
618 }
619
// 这里就可以获取到 anr 的时间了
622 const nsecs_t nextAnrCheck = processAnrsLocked();
623 nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
624
625 // We are about to enter an infinitely long sleep, because we have no commands or
626 // pending or queued events
627 if (nextWakeupTime == LONG_LONG_MAX) {
628 mDispatcherEnteredIdle.notify_all();
629 }
630 } // release lock
631
633 nsecs_t currentTime = now();
634 int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
// 这里去等待anr 超时的时间
635 mLooper->pollOnce(timeoutMillis);
636 }
3)排雷过程
// 在inputdispatcher 与应用创建连接的时候,会设置socket 通信的callback,执行 handleReceiveCallback 方法
5450 Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(const std::string& name) {
5451 if (DEBUG_CHANNEL_CREATION) {
5452 ALOGD("channel '%s' ~ createInputChannel", name.c_str());
5453 }
。。。5475 std::function<int(int events)> callback = std::bind(&InputDispatcher::handleReceiveCallback,
5476 this, std::placeholders::_1, token);
5477
5478 mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, new LooperEventCallback(callback), nullptr);
5479 } // release lock
5480
5481 // Wake the looper because some connections have changed.
5482 mLooper->wake();
5483 return clientChannel;
5484 }
如果应用有发送socket 消息,则执行 handleReceiveCallback 方法
int InputDispatcher::handleReceiveCallback(int events, sp<IBinder> connectionToken) {std::scoped_lock _l(mLock);
// 通过token 获取到对应的connectionsp<Connection> connection = getConnectionLocked(connectionToken);bool notify;
// event 不能为 ALOOPER_EVENT_ERROR 或者 ALOOPER_EVENT_HANGUPif (!(events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP))) {if (!(events & ALOOPER_EVENT_INPUT)) {ALOGW("channel '%s' ~ Received spurious callback for unhandled poll event. ""events=0x%x",connection->getInputChannelName().c_str(), events);return 1;}nsecs_t currentTime = now();bool gotOne = false;status_t status = OK;
// 循环操作for (;;) {
// 通过socket 获取到消息Result<InputPublisher::ConsumerResponse> result =connection->inputPublisher.receiveConsumerResponse();if (!result.ok()) {status = result.error().code();break;}
// 如果result 是 Finished,则执行 finishDispatchCycleLockedif (std::holds_alternative<InputPublisher::Finished>(*result)) {const InputPublisher::Finished& finish =std::get<InputPublisher::Finished>(*result);finishDispatchCycleLocked(currentTime, connection, finish.seq, finish.handled,finish.consumeTime);
。。。
// 设置 gotOne = true
gotOne = true;}if (gotOne) {
// finishDispatchCycleLocked 增加了command,这里去执行 commandrunCommandsLockedInterruptable();if (status == WOULD_BLOCK) {return 1;}}
// 如果result 是 Finished,则执行 finishDispatchCycleLocked
void InputDispatcher::finishDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection, uint32_t seq,bool handled, nsecs_t consumeTime) {if (DEBUG_DISPATCH_CYCLE) {ALOGD("channel '%s' ~ finishDispatchCycle - seq=%u, handled=%s",connection->getInputChannelName().c_str(), seq, toString(handled));}if (connection->status == Connection::Status::BROKEN ||connection->status == Connection::Status::ZOMBIE) {return;}// Notify other system components and prepare to start the next dispatch cycle.auto command = [this, currentTime, connection, seq, handled, consumeTime]() REQUIRES(mLock) {doDispatchCycleFinishedCommand(currentTime, connection, seq, handled, consumeTime);};postCommandLocked(std::move(command));
}
前面 runCommandsLockedInterruptable 方法会去执行 doDispatchCycleFinishedCommand 方法
void InputDispatcher::doDispatchCycleFinishedCommand(nsecs_t finishTime,const sp<Connection>& connection, uint32_t seq,bool handled, nsecs_t consumeTime) {// Handle post-event policy actions.std::deque<DispatchEntry*>::iterator dispatchEntryIt = connection->findWaitQueueEntry(seq);if (dispatchEntryIt == connection->waitQueue.end()) {return;}DispatchEntry* dispatchEntry = *dispatchEntryIt;const nsecs_t eventDuration = finishTime - dispatchEntry->deliveryTime;
// 如果应用处理超过 2 秒,则会打印下列的日志if (eventDuration > SLOW_EVENT_PROCESSING_WARNING_TIMEOUT) {ALOGI("%s spent %" PRId64 "ms processing %s", connection->getWindowName().c_str(),ns2ms(eventDuration), dispatchEntry->eventEntry->getDescription().c_str());}
。。。
// 从等待队列中找到 dispatchEntryIt
dispatchEntryIt = connection->findWaitQueueEntry(seq);if (dispatchEntryIt != connection->waitQueue.end()) {dispatchEntry = *dispatchEntryIt;connection->waitQueue.erase(dispatchEntryIt);const sp<IBinder>& connectionToken = connection->inputChannel->getConnectionToken();
// 从anrtracker 中移除这个超时时间,和connectionmAnrTracker.erase(dispatchEntry->timeoutTime, connectionToken);if (!connection->responsive) {connection->responsive = isConnectionResponsive(*connection);if (connection->responsive) {// The connection was unresponsive, and now it's responsive.processConnectionResponsiveLocked(*connection);}}traceWaitQueueLength(*connection);if (restartEvent && connection->status == Connection::Status::NORMAL) {connection->outboundQueue.push_front(dispatchEntry);traceOutboundQueueLength(*connection);} else {releaseDispatchEntry(dispatchEntry);}}// Start the next dispatch cycle for this connection.
// 如果还有下一个事件的话,则调用 startDispatchCycleLocked 继续去处理startDispatchCycleLocked(now(), connection);
}
则下次执行线程的时候,由于mAnrTracker移除了对应的conenction,则 processAnrsLocked不会触发anr 流程
4)爆雷过程
在anr 时间点触发线程执行:dispatchOnce,然后执行 processAnrsLocked 方法
nsecs_t InputDispatcher::processAnrsLocked() {const nsecs_t currentTime = now();nsecs_t nextAnrCheck = LONG_LONG_MAX;
// 不满足下列的条件if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {if (currentTime >= *mNoFocusedWindowTimeoutTime) {processNoFocusedWindowAnrLocked();mAwaitedFocusedApplication.reset();mNoFocusedWindowTimeoutTime = std::nullopt;return LONG_LONG_MIN;} else {// Keep waiting. We will drop the event when mNoFocusedWindowTimeoutTime comes.nextAnrCheck = *mNoFocusedWindowTimeoutTime;}}// Check if any connection ANRs are duenextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());
// 这里当前的时间点是大于 anr 的时间,即前面没有移除if (currentTime < nextAnrCheck) { // most likely scenarioreturn nextAnrCheck; // everything is normal. Let's check again at nextAnrCheck}// 执行下列anr 的流程// 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;}
// 设置应用回复为 falseconnection->responsive = false;// Stop waking up for this unresponsive connection
// 从anrtracker 中移除,防止进入到线程又再次触发anrmAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());
// 走anr 流程:onAnrLockedonAnrLocked(connection);return LONG_LONG_MIN;
}
// 走anr 流程:onAnrLocked
void InputDispatcher::onAnrLocked(const sp<Connection>& connection) {if (connection == nullptr) {LOG_ALWAYS_FATAL("Caller must check for nullness");}// Since we are allowing the policy to extend the timeout, maybe the waitQueue// is already healthy again. Don't raise ANR in this situationif (connection->waitQueue.empty()) {ALOGI("Not raising ANR because the connection %s has recovered",connection->inputChannel->getName().c_str());return;}
// 从等待队列中获取DispatchEntry* oldestEntry = *connection->waitQueue.begin();
// 获取到等待的时间const nsecs_t currentWait = now() - oldestEntry->deliveryTime;
// 设置input dispatcher 时间分发超时的原因 reasonstd::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());sp<IBinder> connectionToken = connection->inputChannel->getConnectionToken();
// 保存到dump 中updateLastAnrStateLocked(getWindowHandleLocked(connectionToken), reason);processConnectionUnresponsiveLocked(*connection, std::move(reason));// Stop waking up for events on this connection, it is already unresponsivecancelEventsForAnrLocked(connection);
}
processConnectionUnresponsiveLocked 处理anr
void InputDispatcher::processConnectionUnresponsiveLocked(const Connection& connection,std::string reason) {const sp<IBinder>& connectionToken = connection.inputChannel->getConnectionToken();std::optional<int32_t> pid;
// 不满足下列条件if (connection.monitor) {ALOGW("Monitor %s is unresponsive: %s", connection.inputChannel->getName().c_str(),reason.c_str());pid = findMonitorPidByTokenLocked(connectionToken);} else {// The connection is a windowALOGW("Window %s is unresponsive: %s", connection.inputChannel->getName().c_str(),reason.c_str());const sp<WindowInfoHandle> handle = getWindowHandleLocked(connectionToken);if (handle != nullptr) {pid = handle->getInfo()->ownerPid;}}sendWindowUnresponsiveCommandLocked(connectionToken, pid, std::move(reason));
}
void InputDispatcher::sendWindowUnresponsiveCommandLocked(const sp<IBinder>& token,std::optional<int32_t> pid,std::string reason) {
// 将command 保存到queue 中,因为前面会直接返回 MIN,所以线程会马山处理auto command = [this, token, pid, reason = std::move(reason)]() REQUIRES(mLock) {scoped_unlock unlock(mLock);mPolicy->notifyWindowUnresponsive(token, pid, reason);};postCommandLocked(std::move(command));
}
// 通知到界面没有反应 notifyWindowUnresponsive
/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
// 前面将 NativeInputManager 保存到 了InputManager中
831 void NativeInputManager::notifyWindowUnresponsive(const sp<IBinder>& token,
832 std::optional<int32_t> pid,
833 const std::string& reason) {
834 #if DEBUG_INPUT_DISPATCHER_POLICY
835 ALOGD("notifyWindowUnresponsive");
836 #endif
// 会打印对应的trace:notifyWindowUnresponsive
837 ATRACE_CALL();
838
839 JNIEnv* env = jniEnv();
840 ScopedLocalFrame localFrame(env);
841
842 jobject tokenObj = javaObjectForIBinder(env, token);
843 ScopedLocalRef<jstring> reasonObj(env, env->NewStringUTF(reason.c_str()));
844
// 调用java 层的方法
845 env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyWindowUnresponsive, tokenObj,
846 pid.value_or(0), pid.has_value(), reasonObj.get());
847 checkAndClearExceptionFromCallback(env, "notifyWindowUnresponsive");
848 }
/frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
2927 // Native callback
2928 @SuppressWarnings("unused")
2929 private void notifyWindowUnresponsive(IBinder token, int pid, boolean isPidValid,
2930 String reason) {
2931 mWindowManagerCallbacks.notifyWindowUnresponsive(token,
2932 isPidValid ? OptionalInt.of(pid) : OptionalInt.empty(), reason);
2933 }
/frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.java
102 @Override
103 public void notifyWindowUnresponsive(@NonNull IBinder token, @NonNull OptionalInt pid,
104 @NonNull String reason) {
105 mService.mAnrController.notifyWindowUnresponsive(token, pid, reason);
106 }
/frameworks/base/services/core/java/com/android/server/wm/AnrController.java
88 void notifyWindowUnresponsive(@NonNull IBinder token, @NonNull OptionalInt pid,
89 @NonNull String reason) {
90 if (notifyWindowUnresponsive(token, reason)) {
91 return;
92 }
93 if (!pid.isPresent()) {
94 Slog.w(TAG_WM, "Failed to notify that window token=" + token + " was unresponsive.");
95 return;
96 }
97 notifyWindowUnresponsive(pid.getAsInt(), reason);
98 }
99
100 /**
101 * Notify a window identified by its input token was unresponsive.
102 *
103 * @return true if the window was identified by the given input token and the request was
104 * handled, false otherwise.
105 */
106 private boolean notifyWindowUnresponsive(@NonNull IBinder inputToken, String reason) {
107 preDumpIfLockTooSlow();
108 final int pid;
109 final boolean aboveSystem;
110 final ActivityRecord activity;
111 synchronized (mService.mGlobalLock) {
112 InputTarget target = mService.getInputTargetFromToken(inputToken);
113 if (target == null) {
114 return false;
115 }
116 WindowState windowState = target.getWindowState();
117 pid = target.getPid();
118 // Blame the activity if the input token belongs to the window. If the target is
119 // embedded, then we will blame the pid instead.
120 activity = (windowState.mInputChannelToken == inputToken)
121 ? windowState.mActivityRecord : null;// 会打印下列的log
122 Slog.i(TAG_WM, "ANR in " + target + ". Reason:" + reason);
123 aboveSystem = isWindowAboveSystem(windowState);
124 dumpAnrStateLocked(activity, windowState, reason);
125 }
// ActivityRecord 不为空
126 if (activity != null) {
127 activity.inputDispatchingTimedOut(reason, pid);
128 } else {
129 mService.mAmInternal.inputDispatchingTimedOut(pid, aboveSystem, reason);
130 }
131 return true;
132 }
/frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java
6649 public boolean inputDispatchingTimedOut(String reason, int windowPid) {
6650 ActivityRecord anrActivity;
6651 WindowProcessController anrApp;
6652 boolean blameActivityProcess;
6653 synchronized (mAtmService.mGlobalLock) {
6654 anrActivity = getWaitingHistoryRecordLocked();
6655 anrApp = app;
6656 blameActivityProcess = hasProcess()
6657 && (app.getPid() == windowPid || windowPid == INVALID_PID);
6658 }
6659
6660 if (blameActivityProcess) {
6661 return mAtmService.mAmInternal.inputDispatchingTimedOut(anrApp.mOwner,
6662 anrActivity.shortComponentName, anrActivity.info.applicationInfo,
6663 shortComponentName, app, false, reason);
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
17647 boolean inputDispatchingTimedOut(ProcessRecord proc, String activityShortComponentName,
17648 ApplicationInfo aInfo, String parentShortComponentName,
17649 WindowProcessController parentProcess, boolean aboveSystem, String reason) {
17650 if (checkCallingPermission(FILTER_EVENTS) != PackageManager.PERMISSION_GRANTED) {
17651 throw new SecurityException("Requires permission " + FILTER_EVENTS);
17652 }
17653
17654 final String annotation;
17655 if (reason == null) {
17656 annotation = "Input dispatching timed out";
// 这里增加 Input dispatching timed out
17657 } else {
17658 annotation = "Input dispatching timed out (" + reason + ")";
17659 }
17660
17661 if (proc != null) {
17662 synchronized (this) {
17663 if (proc.isDebugging()) {
17664 return false;
17665 }
17666
17667 if (proc.getActiveInstrumentation() != null) {
17668 Bundle info = new Bundle();
17669 info.putString("shortMsg", "keyDispatchingTimedOut");
17670 info.putString("longMsg", annotation);
17671 finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info);
17672 return true;
17673 }
17674 }// 这里去弹框
17675 mAnrHelper.appNotResponding(proc, activityShortComponentName, aInfo,
17676 parentShortComponentName, parentProcess, aboveSystem, annotation);
17677 }
17678
17679 return true;
17680 }
相关文章:
安卓InputDispatching Timeout ANR 流程
1 ANR的检测逻辑有两个参与者: 观测者A和被观测者B,当然,这两者是不在同一个线程中的。2 A在调用B中的逻辑时,同时在A中保存一个标记F,然后做个延时操作C,延时时间设为T,这一步称为: 埋雷 。3 B中的逻辑如果…...
【Nginx从入门到精通】03 、安装部署-让虚拟机可以联网
文章目录 总结一、配置联网【Minimal 精简版】1.1、查看网络配置1.2、配置ip地址 : 修改配置文件 <font colororange>ifcfg-ens33Stage 1:输入指令Stage 2:修改参数Stage 3:重启网络Stage 4:测试上网 二、配置联网【Everyth…...
java 增强型for循环 详解
Java 增强型 for 循环(Enhanced for Loop)详解 增强型 for 循环(也称为 “for-each” 循环)是 Java 从 JDK 5 开始引入的一种便捷循环语法,旨在简化对数组或集合类的迭代操作。 1. 基本语法 语法格式 for (类型 变量…...
浪潮云启操作系统(InLinux) bcache宕机问题分析
前言 本文以一次真实的内核宕机问题为切入点,结合实际操作案例,详细展示了如何利用工具 crash对内核转储(kdump)进行深入分析和调试的方法。通过对崩溃日志的解读、函数调用栈的梳理、关键地址的定位以及代码逻辑的排查ÿ…...
038集——quadtree(CAD—C#二次开发入门)
效果如下: using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.T…...
备赛蓝桥杯--算法题目(1)
1. 链表求和 . - 力扣(LeetCode) class Solution { public:ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode *head nullptr, *tail nullptr;int carry 0;while (l1 || l2) {int n1 l1 ? l1->val: 0;int n2 l2 ? l2->val:…...
机器学习100道经典面试题库(二)
机器学习100道经典面试题库(31-60) 在大规模的语料中,挖掘词的相关性是一个重要的问题。以下哪一个信息不能用于确定两个词的相关性。 A、互信息 B、最大熵 C、卡方检验 D、最大似然比 答案:B 解析:最大熵代表了…...
Unet++改进37:添加KACNConvNDLayer(2024最新改进方法)
本文内容:添加KACNConvNDLayer 目录 论文简介 1.步骤一 2.步骤二 3.步骤三 4.步骤四 论文简介 1.步骤一 新建block/kacn_conv.py文件,添加如下代码: import torch import torch.nn as nn##源码地址:https://github.com/SynodicMonth/ChebyKAN class KACNConvNDLaye…...
基于 Levenberg - Marquardt 法的 BP 网络学习改进算法详解
基于 Levenberg - Marquardt 法的 BP 网络学习改进算法详解 一、引言 BP(Back Propagation)神经网络在众多领域有着广泛应用,但传统 BP 算法存在收敛速度慢、易陷入局部最优等问题。Levenberg - Marquardt(LM)算法作…...
MySQL 8.0与PostgreSQL 15.8的性能对比
根据搜索结果,以下是MySQL 8.0与PostgreSQL 15.8的性能对比: MySQL 8.0性能特点: MySQL在处理大量读操作时表现出色,其存储引擎InnoDB提供了行级锁定和高效的事务处理,适用于并发读取的场景。MySQL通过查询缓存来提高读…...
qt连接postgres数据库时 setConnectOptions函数用法
连接选项,而这些选项没有直接的方法对应,你可能需要采用以下策略之一: 由于Qt SQL API的限制,你可能需要采用一些变通方法或查阅相关文档和社区资源以获取最新的信息和最佳实践。如果你确实需要设置特定的连接选项,并且…...
MySQL45讲 第二十七讲 主库故障应对:从库切换策略与 GTID 详解——阅读总结
文章目录 MySQL45讲 第二十七讲 主库故障应对:从库切换策略与 GTID 详解一、一主多从架构与主备切换的挑战(一)一主多从基本结构(二)主备切换的复杂性 二、基于位点的主备切换(一)同步位点的概念…...
JavaWeb笔记整理——Spring Task、WebSocket
目录 SpringTask cron表达式 WebSocket SpringTask cron表达式 WebSocket...
基于SpringBoot+RabbitMQ完成应⽤通信
前言: 经过上面俩章学习,我们已经知道Rabbit的使用方式RabbitMQ 七种工作模式介绍_rabbitmq 工作模式-CSDN博客 RabbitMQ的工作队列在Spring Boot中实现(详解常⽤的⼯作模式)-CSDN博客作为⼀个消息队列,RabbitMQ也可以⽤作应⽤程…...
Flutter踩坑记录(一)debug运行生成的项目,不能手动点击运行
问题 IOS14设备,切后台划掉,二次启动崩溃。 原因 IOS14以上 flutter 不支持debugger模式下的二次启动 。 要二次启动需要以release方式编译工程安装至手机。 操作步骤 清理项目:在命令行中运行flutter clean来清理之前的构建文件。重新构…...
React的hook✅
为什么hook必须在组件内的顶层声明? 这是为了确保每次组件渲染时,Hooks 的调用顺序保持一致。React利用 hook 的调用顺序来跟踪各个 hook 的状态。每当一个函数组件被渲染时,所有的 hook 调用都是按照从上到下的顺序依次执行的。React 内部会…...
2024.5 AAAiGLaM:通过邻域分区和生成子图编码对领域知识图谱对齐的大型语言模型进行微调
GLaM: Fine-Tuning Large Language Models for Domain Knowledge Graph Alignment via Neighborhood Partitioning and Generative Subgraph Encoding 问题 如何将特定领域知识图谱直接整合进大语言模型(LLM)的表示中,以提高其在图数据上自…...
从熟练Python到入门学习C++(record 6)
基础之基础之最后一节-结构体 1.结构体的定义 结构体相对于自定义的一种新的变量类型。 四种定义方式,推荐第一种;第四种适合大量定义,也适合查找; #include <iostream> using namespace std; #include <string.h>…...
jenkins的安装(War包安装)
Jenkins是一个开源的持续集成工具,基于Java开发,主要用于监控持续的软件版本发布和测试项目。 它提供了一个开放易用的平台,使软件项目能够实现持续集成。Jenkins的功能包括持续的软件版本发布和测试项目,以及监控外部调用执行…...
WPS 加载项开发说明wpsjs
wpsjs几个常用的CMD命令: 1.打开cmd输入命令测试版本号 npm -v 2.首次安装nodejs,npm默认国外镜像,包下载较慢时,可切换到国内镜像 //下载速度较慢时可切换国内镜像 npm config set registry https://registry.npmmirror.com …...
【Anomaly Detection论文阅读记录】PaDiM与PatchCore模型的区别与联系
PaDiM与PatchCore模型的区别与联系 背景介绍 PADIM(Pretrained Anomaly Detection via Image Matching)和 PatchCore 都是基于深度学习的异常检测方法,主要用于图像异常检测,尤其是在无监督学习设置下。 PADIM 是一种通过利用预训练的视觉模型(例如,ImageNet预训练的卷…...
uni-app Vue3语法实现微信小程序样式穿透uview-plus框架
1 问题描述 我在用 uni-app vue3 语法开发微信小程序时,在项目中使用了 uview-plus 这一开源 UI 框架。在使用 up-text 组件时,想要给它添加一些样式,之前了解到微信小程序存在样式隔离的问题,也在uview-plus官网-注意事项中找到…...
K8S基础概念和环境搭建
K8S的基础概念 1. 什么是K8S K8S的全称是Kubernetes K8S是一个开源的容器编排平台,用于自动化部署、扩缩、管理容器化应用程序。 2. 集群和节点 集群:K8S将多个机器统筹和管理起来,彼此保持通讯,这样的关系称之为集群。 节点…...
[服务器] 腾讯云服务器免费体验,成功部署网站
文章目录 概要整体架构流程概要 腾讯云服务器免费体验一个月。 整体架构流程 腾讯云服务器体验一个月, 选择预装 CentOS 7.5 首要最重要的是: 添加阿里云镜像。 不然国外源速度慢, 且容易失败。 yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/li…...
vue中el-select 模糊查询下拉两种方式
第一种:先获取所有下拉数据再模糊查询,效果如下 1,页面代码:speciesList是种类列表List, speciesId 是speciesList里面对应的id,filterable是过滤查询标签 <el-form-item label"种类" prop"species…...
深入解析PostgreSQL中的PL/pgSQL语法
在数据库管理系统中,PostgreSQL因其强大的功能和稳定性而受到广泛欢迎。其中,PL/pgSQL作为PostgreSQL的过程化语言,为用户提供了更为灵活和强大的编程能力。本文将深入解析PL/pgSQL的语法,帮助读者更好地掌握这门语言,…...
Vue 3集成海康Web插件实现视频监控
🌈个人主页:前端青山 🔥系列专栏:组件封装篇 🔖人终将被年少不可得之物困其一生 依旧青山,本期给大家带来组件封装篇专栏内容:Vue 3集成海康Web插件实现视频监控 引言 最近在项目中使用了 Vue 3 结合海康Web插件来实…...
多目标优化算法:多目标蛇鹫优化算法(MOSBOA)求解DTLZ1-DTLZ9,提供完整MATLAB代码
一、蛇鹫优化算法 蛇鹫优化算法(Secretary Bird Optimization Algorithm,简称SBOA)由Youfa Fu等人于2024年4月发表在《Artificial Intelligence Review》期刊上的一种新型的元启发式算法。该算法旨在解决复杂工程优化问题,特别是…...
机器翻译基础与模型 之三:基于自注意力的模型
基于RNN和CNN的翻译模型,在处理文字序列时有个问题:它们对序列中不同位置之间的依赖关系的建模并不直接。以CNN的为例,如果要对长距离依赖进行描述,需要多层卷积操作,而且不同层之间信息传递也可能有损失,这…...
如何使用PCL处理ROS Bag文件中的点云数据并重新保存 ubuntu20.04
如何使用PCL处理ROS Bag文件中的点云数据并重新保存 要精确地处理ROS bag中的点云数据并使用PCL进行处理,再将处理后的数据保存回新的ROS bag文件,以下方案提供了详细、专业和严谨的步骤。 步骤 1: 环境设置 确保安装了ROS和PCL,并配置好环…...
做苗木选择哪个网站/简单网页设计模板html
题目链接 https://www.nowcoder.com/practice/5af18ba2eb45443aa91a11e848aa6723?tpId37&tqId21237&tPage1&rp&ru/ta/huawei&qru/ta/huawei/question-ranking 题目描述 给定n个字符串,请对n个字符串按照字典序排列。 输入描述: 输入第一行…...
济南网站建设 伍际网络/哪里有免费的网站推广服务
可量化的软件项目质量考核指标说明关键字:软件项目质量考核有一个完整的指标体系,从可行易操作的角度出发,评价一个软件项目质量情况,可以从以下几个方面出发,获取比较客观的评价指标。指标内容说明如下。1 小组考核内…...
怎么做体育直播网站/前端性能优化
以下列出mysql函数的使用,并不完全,涉及到多少写多少。length(str):返回字符串(str)的字符长度。一个汉字算三个字符,一个数字或字母算一个字符。select length(测试); --6select length(123abc); --6char_length(str):…...
域名怎么制作网站/网站关键词优化案例
Cmake使用总结 Cmake中常见问题:## 标题 ## 1. 下载问题。经常需要从国外下载第三方包,由于网络问题可能会下载失败!建议开启VPN进行下载,或者从其他下载源下载后进行手动解压并放到指定路径下。 2. 路径问题。Cmake中往往需要填写路径&am…...
淘宝如何在其他网站做优惠/google关键词
随着课程的学习越来越深入,学期在不知不觉中已经过了三分之二。可是我自己仍觉得在课堂上没有学到什么知识,人家说大学生的学习效率最高的时候是在期末考试前的最后一天晚上,可能也适用于我现在的课程学习状态吧。下月中旬就要开始考四级了&a…...
wordpress进入后台显示500/洛阳网站seo
使用Chrome的开发者工具 怎样打开Chrome的开发者工具?【原文地址】http://www.cnblogs.com/QLeelulu/archive/2011/08/28/2156402.html你可以直接在页面上点击右键,然后选择审查元素:或者在Chrome的工具中找到:或者,你…...