“本文基于Android13源码,分析Input系统的Anr实现原理“

在文章之前,先提几个问题:

  • 如果在activity任意周期(onCreate,onResume等),同步执行耗时超过5s(ANR时间)的任务,期间不进行点击,那会触发ANR吗?
  • 如果在button点击的时候,在onClick回调同步执行耗时超过5s的任务。点击一次会触发ANR吗?点击2次呢,3次呢?

1、ANR 分类

首先看一下anr的分类:

  • Input ANR:按键或触摸事件在5s内没有相应,主要在activity、fragment中。
  • Service anr:前台service 响应时间是20s,后台service是200s。
  • Broadcast anr:前台广播是10s,后台广播是60s。
  • ContentProvider anr:publish执行未在10s内完成。
  • startForgoundService:应用调用startForegroundService,然后5s内未调用startForeground出现ANR或者Crash

有些小伙伴可能好奇,为啥没有Activity ANR的分类?Activity ANR准确的来说是——Input系统检测,触发activity 的anr。所以本文将通过input系统来讲述Android是如何触发activity的anr。

2、InputDispatcher

在了解Input Anr 原理之前,我们简单了解一下InputDispatcher是如何分发按键事件的。

Inputdispatcher中,在线程里面调用到dispatchOnce方法,该方法中主要做:

  • 通过dispatchOnceInnerLocked(),取出mInboundQueue 里面的 EventEntry事件
  • 通过enqueueDispatchEntryLocked(),生成事件DispatchEntry并加入connection的outbound队列。
  • 通过startDispatchCycleLocked(),从outboundQueue中取出事件DispatchEntry, 重新放入connection的waitQueue队列。同时通过inputPublisher.publishKeyEvent() 方法将按键事件分发给java层。
  • 通过processAnrsLocked(),判断是否需要触发ANR。

按键事件存储在3个queue中:

  1. InputDispatcher的mInboundQueue:存储的是从InputReader 送来的输入事件。
  2. Connection的outboundQueue:该队列是存储即将要发送给应用的输入事件。
  3. Connection的waitQueue:队列存储的是已经发给应用的事件,但是应用还未处理完成的。

2.1 dispatchOnce

dispatchOnce() 中主要就是调用如下的两个方法:

  • 事件分发:dispatchOnceInnerLocked()
  • 检查ANR:processAnrsLocked()
> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoid InputDispatcher::dispatchOnce() {nsecs_t nextWakeupTime = LONG_LONG_MAX; {...// 如果没有挂起的命令,则运行调度循环。调度循环可能会将命令排入队列,以便稍后运行。if (!haveCommandsLocked()) {dispatchOnceInnerLocked(&nextWakeupTime);}// 运行所有挂起的命令(如果有)。如果运行了任何命令,则强制下一次轮询立即唤醒。if (runCommandsLockedInterruptable()) {nextWakeupTime = LONG_LONG_MIN;}...// 我们可能必须早点醒来以检查应用程序是否正处于anrconst nsecs_t nextAnrCheck = processAnrsLocked();} // 等待回调、超时或唤醒。nsecs_t currentTime = now();int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);mLooper->pollOnce(timeoutMillis);
}

我们先简单回顾下事件分发过程

3、事件分发

3.1 dispatchOnceInnerLocked

该方法主要是:

  • 从mInboundQueue 中取出mPendingEvent

  • 通过mPendingEvent的type决定事件类型和分发方式。比如当前是key类型。

主要代码如下:

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoid InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {nsecs_t currentTime = now();...// 优化应用切换的延迟。本质上,当按下应用程序切换键(HOME)时,我们会开始一个短暂的超时。// 当它过期时,我们会抢占调度并删除所有其他挂起的事件。bool isAppSwitchDue = mAppSwitchDueTime <= currentTime;// 当前没有PendingEvent(即EventEntry),则取一个if (!mPendingEvent) {...//  mInboundQueue不为空 ,就从队列前面取一个PendingEventmPendingEvent = mInboundQueue.front();mInboundQueue.pop_front();traceInboundQueueLengthLocked();}...
}

3.2 enqueueDispatchEntryLocked

enqueueDispatchEntryLocked() 会创建一个新的DispatchEntry,然后将DispatchEntry 加入到connection#outboundQueue 中

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoid InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection,std::shared_ptr<EventEntry> eventEntry,const InputTarget& inputTarget,int32_t dispatchMode) {// 这是一个新事件。将新的调度条目排队到此连接的出站队列中。std::unique_ptr<DispatchEntry> dispatchEntry =createDispatchEntry(inputTarget, eventEntry, inputTargetFlags);...// 将生成的dispatchEntry 加入到 connection的outboundQueue 中connection->outboundQueue.push_back(dispatchEntry.release());traceOutboundQueueLength(*connection);
}

3.3 startDispatchCycleLocked

该方法主要通过connection 发布最终的事件,至此,InputDispatcher完成事件的发布,并且将发布的事件保存在connection的waitQueue中。

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoid InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection) {while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {// 从outboundQueue 队列中取出 DispatchEntryDispatchEntry* dispatchEntry = connection->outboundQueue.front();const std::chrono::nanoseconds timeout = getDispatchingTimeoutLocked(connection);// 设置超时时间dispatchEntry->timeoutTime = currentTime + timeout.count();// 发布事件status_t status;const EventEntry& eventEntry = *(dispatchEntry->eventEntry);...// 在等待队列上重新排队事件。connection->outboundQueue.erase(std::remove(connection->outboundQueue.begin(),connection->outboundQueue.end(),dispatchEntry));// 在waitQueue 尾部重新插入connection->waitQueue.push_back(dispatchEntry);if (connection->responsive) {// 插入事件对应的anr检查时间mAnrTracker.insert(dispatchEntry->timeoutTime,connection->inputChannel->getConnectionToken());}}
}

3.4 ANR超时时间

由 startDispatchCycleLocked() 方法,知道是通过getDispatchingTimeoutLocked 获取到超时时间

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp//  如果没有用于确定适当调度超时的焦点应用程序或暂停窗口,则默认输入调度超时。
const std::chrono::duration DEFAULT_INPUT_DISPATCHING_TIMEOUT = std::chrono::milliseconds(android::os::IInputConstants::UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS *HwTimeoutMultiplier());std::chrono::nanoseconds InputDispatcher::getDispatchingTimeoutLocked(const sp<Connection>& connection) {if (connection->monitor) {// 返回监控的超时时间return mMonitorDispatchingTimeout;}const sp<WindowInfoHandle> window =getWindowHandleLocked(connection->inputChannel->getConnectionToken());if (window != nullptr) {// 可以找到focused Windowreturn window->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);}// 获取默认的值return DEFAULT_INPUT_DISPATCHING_TIMEOUT;
}

WindowInfoHandle#getDispatchingTimeout 返回的值如下

> libs/gui/include/gui/WindowInfo.hclass WindowInfoHandle : public RefBase {inline std::chrono::nanoseconds getDispatchingTimeout(std::chrono::nanoseconds defaultValue) const {return mInfo.token ? std::chrono::nanoseconds(mInfo.dispatchingTimeout) : defaultValue;}
}struct WindowInfo : public Parcelable {std::chrono::nanoseconds dispatchingTimeout = std::chrono::seconds(5); // 5 秒
}

DEFAULT_INPUT_DISPATCHING_TIMEOUT 主要由UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS * HwTimeoutMultiplier() 计算得到

UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS的值如下

> android/os/IInputConstants.hclass IInputConstants : public ::android::IInterface {public:enum : int32_t { UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS = 5000 };....
};  // class IInputConstants
}

HwTimeoutMultiplier() 方法定义如下,即读ro.hw_timeout_multiplier 属性值,默认是1。

> system/libbase/include/android-base/properties.hstatic inline int HwTimeoutMultiplier() {return android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
}

3.5 调用栈

native层的事件分发调用栈如下

libs/input/InputTransport.cpp : InputPublisher::publishMotionEvent()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::startDispatchCycleLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::enqueueDispatchEntriesLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::prepareDispatchCycleLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::dispatchKeyLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::dispatchOnceInnerLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::dispatchOnce()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::start()

4、ANR触发

在dispatchOnce(),会调用processAnrsLocked 方法来决定是否需要触发anr

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppvoid InputDispatcher::dispatchOnce() {...// 我们可能必须早点醒来以检查应用程序是否正处于anrconst nsecs_t nextAnrCheck = processAnrsLocked();....
}

4.1 processAnrsLocked

该方法是用于检查队列中是否有太旧的事件,如果存在就触发ANR

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp// 检查是否有任何连接的等待队列具有太旧的事件。如果我们等待事件被确认的时间超过窗口超时,
// 请引发 ANR。返回我们下次应该醒来的时间。
nsecs_t InputDispatcher::processAnrsLocked() {const nsecs_t currentTime = now();nsecs_t nextAnrCheck = LONG_LONG_MAX; // 下一次检查anr的时间// 检查我们是否正在等待一个聚焦窗口出现。如果等待时间过长就报 ANRif (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {if (currentTime >= *mNoFocusedWindowTimeoutTime) {// 场景1: 触发noFocusedWindow的anrprocessNoFocusedWindowAnrLocked();mAwaitedFocusedApplication.reset();mNoFocusedWindowTimeoutTime = std::nullopt;return LONG_LONG_MIN;} else {// 请继续等待。我们将在mNoFocusedWindowTimeoutTime到来时放弃该事件。nextAnrCheck = *mNoFocusedWindowTimeoutTime;}}// 检查是否有任何连接 ANR 到期,mAnrTracker 中保存所有已分发事件(未被确认消费的事件)的超时时间nextAnrCheck = std::min(nextAnrCheck, mAnrTracker.firstTimeout());if (currentTime < nextAnrCheck) { // 最有可能的情况// 一切正常,在 nextAnrCheck 再检查一次return nextAnrCheck;}// 如果我们到达这里,则连接无响应。sp<Connection> connection = getConnectionLocked(mAnrTracker.firstToken());// 停止为此无响应的连接唤醒mAnrTracker.eraseToken(connection->inputChannel->getConnectionToken());// 场景2: 触发ANRonAnrLocked(connection);return LONG_LONG_MIN;
}

其中,mAnrTracker 存储已经成功分发给应用的事件。详情见startDispatchCycleLocked() 方法。

mNoFocusedWindowTimeoutTime 是在findFocusedWindowTargetsLocked() 方法中赋值的,在分发事件的时候会调用到findFocusedWindowTargetsLocked() :

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppInputEventInjectionResult InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime, const EventEntry& entry, std::vector<InputTarget>& inputTargets,nsecs_t* nextWakeupTime) {...// 兼容性行为:如果存在焦点应用程序但没有焦点窗口,则引发 ANR。只有当我们有重点事件要调度时,才开始计数。// 如果我们开始通过触摸(应用程序开关)与另一个应用程序交互,则 ANR 将被取消。// 如果将“无聚焦窗口 ANR”移动到策略中,则可以删除此代码。输入不知道应用是否应具有焦点窗口。if (focusedWindowHandle == nullptr && focusedApplicationHandle != nullptr) {if (!mNoFocusedWindowTimeoutTime.has_value()) {// 发现没有focusedWindow,就添加ANR定时器。std::chrono::nanoseconds timeout = focusedApplicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);mNoFocusedWindowTimeoutTime = currentTime + timeout.count();....return InputEventInjectionResult::PENDING;}}// 找到一个focusedwindow,就取消ANR定时器resetNoFocusedWindowTimeoutLocked();...
}void InputDispatcher::resetNoFocusedWindowTimeoutLocked() {// 取消ANR定时器mNoFocusedWindowTimeoutTime = std::nullopt;mAwaitedFocusedApplication.reset();
}

从上面的代码我们能小结出两个场景ANR的条件:

  • 有等待获取焦点的应用:当前时间超过Timeout,调用processNoFocusedWindowAnrLocked() 进一步确认
  • 存在window:当前时间超过事件响应的超时时间。调用onAnrLocked() 进一步确认。

4.2 processNoFocusedWindowAnrLocked

该方法触发anr的条件是:

  1. 当前关注的应用程序必须与我们等待的应用程序相同。
  2. 确保我们仍然没有聚焦窗口。

processNoFocusedWindowAnrLocked 最后也是调用到onAnrLocked。

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp//  如果没有聚焦窗口,请触发ANR。在触发 ANR 之前,请执行最终状态检查:
void InputDispatcher::processNoFocusedWindowAnrLocked() {std::shared_ptr<InputApplicationHandle> focusedApplication =getValueByKey(mFocusedApplicationHandlesByDisplay, mAwaitedApplicationDisplayId);if (focusedApplication == nullptr ||focusedApplication->getApplicationToken() !=mAwaitedFocusedApplication->getApplicationToken()) {// 出乎意料,因为当前焦点应用程序已被更改,我们应该重置 ANR 计时器return;}const sp<WindowInfoHandle>& focusedWindowHandle =getFocusedWindowHandleLocked(mAwaitedApplicationDisplayId);if (focusedWindowHandle != nullptr) {//我们现在有一个焦点window,不需要再触发ANRreturn;}onAnrLocked(mAwaitedFocusedApplication);
}

onAnrLocked 有两种实现:

  • 能找到当前focus的window
  • 找不到当前focus的window,但是可以找到当前前台应用。

我们先看情况1

4.3 onAnrLocked(connection)

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp//情况1: 能找到window的情况
void InputDispatcher::onAnrLocked(const sp<Connection>& connection) {// 由于我们允许策略延长超时,因此 waitQueue 可能已经再次正常运行。在这种情况下不要触发 ANRif (connection->waitQueue.empty()) {return;}// “最旧的条目”是首次发送到应用程序的条目。但是,该条目可能不是导致超时发生的条目。// 一种可能性是窗口超时已更改。这可能会导致较新的条目在已分派的条目之前超时。// 在这种情况下,最新条目会导致 ANR。但很有可能,该应用程序会线性处理事件。// 因此,提供有关最早条目的信息似乎是最有用的。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());sp<IBinder> connectionToken = connection->inputChannel->getConnectionToken();// 生成 reason 报告updateLastAnrStateLocked(getWindowHandleLocked(connectionToken), reason);processConnectionUnresponsiveLocked(*connection, std::move(reason));// 停止唤醒此连接上的事件,它已经没有响应cancelEventsForAnrLocked(connection);
}
// 捕获 ANR 时 InputDispatcher 状态的记录。
void InputDispatcher::updateLastAnrStateLocked(const std::string& windowLabel,const std::string& reason) {....dumpDispatchStateLocked(mLastAnrState);
}

4.3.1 dumpDispatchStateLocked

dumpDispatchStateLocked 函数主要打印当前window和事件队列信息。执行dumpsys input 命令,dumpDispatchStateLocked函数输出的内容如下:

Input Dispatcher State:....PendingEvent: <none> // 当前正在调度转储事件。InboundQueue: <empty> // Inbound 队列ReplacedKeys: <empty>Connections:317: channelName='cf1eda9 com.example.anrdemo/com.example.anrdemo.MainActivity (server)', windowName='cf1eda9 com.example.anrdemo/com.example.anrdemo.MainActivity (server)', status=NORMAL, monitor=false, responsive=trueOutboundQueue: <empty>WaitQueue: length=4MotionEvent(deviceId=9, source=0x00001002, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=22.8, yPrecision=10.8, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (700.0, 1633.9)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=0, age=4129ms, wait=4128msMotionEvent(deviceId=9, source=0x00001002, displayId=0, action=UP, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=22.8, yPrecision=10.8, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (700.0, 1633.9)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=1, age=4011ms, wait=4010ms....

从上面可以看到InboundQueue,OutboundQueue,WaitQueue 3个Queue的状态。其中WaitQueue的size为4,即两对点击事件在等待com.example.anrdemo消费。

4.3.2 processConnectionUnresponsiveLocked

在调用完updateLastAnrStateLocked 后,接着调用到processConnectionUnresponsiveLocked

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp// 该方法告诉策略连接已变得无响应,以便它可以启动 ANR。检查感兴趣的连接是监视器还是窗口,并将相应的命令条目添加到命令队列中。
void InputDispatcher::processConnectionUnresponsiveLocked(const Connection& connection,std::string reason) {const sp<IBinder>& connectionToken = connection.inputChannel->getConnectionToken();.... sendWindowUnresponsiveCommandLocked(connectionToken, pid, std::move(reason));
}void InputDispatcher::sendWindowUnresponsiveCommandLocked(const sp<IBinder>& token,std::optional<int32_t> pid,std::string reason) {auto command = [this, token, pid, reason = std::move(reason)]() REQUIRES(mLock) {scoped_unlock unlock(mLock);mPolicy->notifyWindowUnresponsive(token, pid, reason);};postCommandLocked(std::move(command));
}

sendWindowUnresponsiveCommandLocked 中将command添加到mCommandQueue队列后,最终调用到mPolicy的notifyWindowUnresponsive 。

通过InputDispatcher头文件可以知道mPolicy是InputDispatcherPolicyInterface 接口的实例。

> frameworks/native/services/inputflinger/dispatcher/InputDispatcher.hclass InputDispatcher : public android::InputDispatcherInterface {....
private:sp<InputDispatcherPolicyInterface> mPolicy;
}

NativeInputManager 类实现了InputDispatcherPolicyInterface接口

> frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cppclass NativeInputManager : public virtual RefBase,public virtual InputReaderPolicyInterface,public virtual InputDispatcherPolicyInterface,public virtual PointerControllerPolicyInterface {....
}

4.4 onAnrLocked(application)

//情况2: 找不到focus的window,但是找到当前获取input事件的应用。
void InputDispatcher::onAnrLocked(std::shared_ptr<InputApplicationHandle> application) {std::string reason =StringPrintf("%s does not have a focused window", application->getName().c_str());// 收集anr的window、reason信息updateLastAnrStateLocked(*application, reason);auto command = [this, application = std::move(application)]() REQUIRES(mLock) {scoped_unlock unlock(mLock);mPolicy->notifyNoFocusedWindowAnr(application);};// 将anr的命令添加到 mCommandQueue 中postCommandLocked(std::move(command));
}

同理,这里也调用到mPolicy(即NativeInputManager)的notifyNoFocusedWindowAnr 方法。

onAnrLocked 方法最后会分别调用到NativeInputManager的notifyWindowUnresponsive()notifyNoFocusedWindowAnr()

4.5 notifyWindowUnresponsive

该方法用于通知窗口无响应

> frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cppvoid NativeInputManager::notifyWindowUnresponsive(const sp<IBinder>& token,std::optional<int32_t> pid,const std::string& reason) {....jobject tokenObj = javaObjectForIBinder(env, token);ScopedLocalRef<jstring> reasonObj(env, env->NewStringUTF(reason.c_str()));// 重点:这里调用到Java层 InputManagerService的notifyWindowUnresponsive方法env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyWindowUnresponsive, tokenObj,pid.value_or(0), pid.has_value(), reasonObj.get());
}

gServiceClassInfo 是一个结构体

> frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cppstatic struct {jclass clazz;jmethodID notifyWindowUnresponsive; // 对应java层的方法....
} gServiceClassInfo;

对应的clazz初始化如下

> frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cppint register_android_server_InputManager(JNIEnv* env) {// Callbacksjclass clazz;FIND_CLASS(clazz, "com/android/server/input/InputManagerService");gServiceClassInfo.clazz = reinterpret_cast<jclass>(env->NewGlobalRef(clazz));....
}

这样,anr的消息就抛到了java层的InputManagerService#notifyWindowUnresponsive()

4.6 notifyNoFocusedWindowAnr

该方法用于通知无焦点ANR

> frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cppvoid NativeInputManager::notifyNoFocusedWindowAnr(const std::shared_ptr<InputApplicationHandle>& inputApplicationHandle) {JNIEnv* env = jniEnv();ScopedLocalFrame localFrame(env);jobject inputApplicationHandleObj =getInputApplicationHandleObjLocalRef(env, inputApplicationHandle);env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifyNoFocusedWindowAnr,inputApplicationHandleObj);checkAndClearExceptionFromCallback(env, "notifyNoFocusedWindowAnr");
}

同理,该方法最后通过JNI调用到Java层 InputManagerService#notifyNoFocusedWindowAnr()

4.7 调用栈

能找到window的connection 对应的调用栈

services/core/jni/com_android_server_input_InputManagerService.cpp : NativeInputManager::notifyWindowUnresponsive()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::sendWindowUnresponsiveCommandLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::processConnectionUnresponsiveLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::onAnrLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::processAnrsLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::dispatchOnce()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::start()

没有找到window,但是能找到当前应用 对应的调用栈

services/core/jni/com_android_server_input_InputManagerService.cpp : NativeInputManager::notifyNoFocusedWindowAnr()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::onAnrLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::processAnrsLocked()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::dispatchOnce()
services/inputflinger/dispatcher/InputDispatcher.cpp : InputDispatcher::start()

5、ANR对话框显示流程

5.1 InputManagerService

上面提到ANR分两种场景:

  • 能找到window的connection,常见提示:"%s is not responding. Waited %d ms for %s"
  • 没有找到window,但是能找到当前应用,常见异常信息:"Application does not have a focused window"

这里就分开讨论一下

5.1.1 notifyWindowUnresponsive

InputManagerService#notifyWindowUnresponsive() 方法实现如下,很简单,只调用mWindowManagerCallbacks 做转发

> frameworks/base/services/core/java/com/android/server/input/InputManagerService.javapublic class InputManagerService {// Native callbackprivate void notifyWindowUnresponsive(IBinder token, int pid, boolean isPidValid, String reason) {mWindowManagerCallbacks.notifyWindowUnresponsive(token,isPidValid ? OptionalInt.of(pid) : OptionalInt.empty(), reason);}
}

在InputManagerCallback 中,将事件传给了WindowManagerService#mAnrController。

> frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.javafinal class InputManagerCallback implements InputManagerService.WindowManagerCallbacks {private final WindowManagerService mService;@Overridepublic void notifyWindowUnresponsive(@NonNull IBinder token, @NonNull OptionalInt pid,@NonNull String reason) {mService.mAnrController.notifyWindowUnresponsive(token, pid, reason);}
}

mAnrController即AnrController的实例,AnrController#notifyWindowUnresponsive() 实现如下

> frameworks/base/services/core/java/com/android/server/wm/AnrController.java// 通知由其输入令牌标识的窗口无响应。 @return 如果窗口由给定的输入令牌标识并且请求已处理,则返回 true,否则返回 false。
private boolean notifyWindowUnresponsive(@NonNull IBinder inputToken, String reason) {final int pid;final boolean aboveSystem;final ActivityRecord activity;synchronized (mService.mGlobalLock) {InputTarget target = mService.getInputTargetFromToken(inputToken);WindowState windowState = target.getWindowState();pid = target.getPid();// 如果输入令牌属于窗口,则归咎于Activity。如果目标是嵌入式的,那么我们就会归咎 pid。activity = (windowState.mInputChannelToken == inputToken)? windowState.mActivityRecord : null;aboveSystem = isWindowAboveSystem(windowState);// 调用WindowManagerService#saveANRStateLocked 保存ANR相关信息dumpAnrStateLocked(activity, windowState, reason);}// 这里的activity是ActivityRecord类的实例if (activity != null) {// 情况1: 能找到当前window对应的activityRecordactivity.inputDispatchingTimedOut(reason, pid);} else {// 情况2: 找不到,直接调用mAmInternal的inputDispatchingTimedOutmService.mAmInternal.inputDispatchingTimedOut(pid, aboveSystem, reason);}return true;}

无论上面的activity是否为null,最终都会调用到mAmInternal.inputDispatchingTimedOut() 方法,只是传的参数不一样。

5.1.2 notifyNoFocusedWindowAnr

实现如下,只是作为转发给mWindowManagerCallbacks

> frameworks/base/services/core/java/com/android/server/input/InputManagerService.java// Native callback.private void notifyNoFocusedWindowAnr(InputApplicationHandle inputApplicationHandle) {mWindowManagerCallbacks.notifyNoFocusedWindowAnr(inputApplicationHandle);}

mWindowManagerCallbacks 是一个WindowManagerCallbacks接口,对应实现类是InputManagerCallback

> frameworks/base/services/core/java/com/android/server/wm/InputManagerCallback.javafinal class InputManagerCallback implements InputManagerService.WindowManagerCallbacks {private final WindowManagerService mService;// 通知窗口管理器有关由于没有焦点窗口而没有响应的应用程序。@Overridepublic void notifyNoFocusedWindowAnr(@NonNull InputApplicationHandle applicationHandle) {mService.mAnrController.notifyAppUnresponsive(applicationHandle, "Application does not have a focused window");}
}

mAnrController 是AnrController 的实例,notifyAppUnresponsive() 实现如下:

> frameworks/base/services/core/java/com/android/server/wm/AnrController.javavoid notifyAppUnresponsive(InputApplicationHandle applicationHandle, String reason) {preDumpIfLockTooSlow();final ActivityRecord activity;synchronized (mService.mGlobalLock) {// 获取activityactivity = ActivityRecord.forTokenLocked(applicationHandle.token);// 保存ANR 信息dumpAnrStateLocked(activity, null /* windowState */, reason);mUnresponsiveAppByDisplay.put(activity.getDisplayId(), activity);}activity.inputDispatchingTimedOut(reason, INVALID_PID);}

最后也会调用到ActivityRecord#inputDispatchingTimedOut() 方法。

5.2 ActivityRecord

inputDispatchingTimedOut 实现如下

> frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java// 当输入分派到与应用程序窗口容器关联的窗口超时时调用。public boolean inputDispatchingTimedOut(String reason, int windowPid) {ActivityRecord anrActivity;boolean blameActivityProcess;synchronized (mAtmService.mGlobalLock) {// 找到anr的activityanrActivity = getWaitingHistoryRecordLocked();...}if (blameActivityProcess) {return mAtmService.mAmInternal.inputDispatchingTimedOut(anrApp.mOwner,anrActivity.shortComponentName, anrActivity.info.applicationInfo,shortComponentName, app, false, reason);}....}

5.3 ActivityManagerService

inputDispatchingTimedOut 方法最后调用到ActivityManagerInternal的inputDispatchingTimedOut,ActivityManagerInternal 是一个抽象类,对应实现是LocalService

> frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.javapublic final class LocalService extends ActivityManagerInternal {@Overridepublic boolean inputDispatchingTimedOut(....) {return ActivityManagerService.this.inputDispatchingTimedOut(....);}
}

接着来到了ActivityManagerService的inputDispatchingTimedOut

> frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java// 处理输入调度超时。boolean inputDispatchingTimedOut(ProcessRecord proc, ... String reason) {if (reason == null) {annotation = "Input dispatching timed out";} else {annotation = "Input dispatching timed out (" + reason + ")";}....mAnrHelper.appNotResponding(proc, activityShortComponentName, aInfo,parentShortComponentName, parentProcess, aboveSystem, annotation);}

5.4 AnrHelper

AnrHelper.appNotResponding方法实现如下

> frameworks/base/services/core/java/com/android/server/am/AnrHelper.javavoid appNotResponding(com.android.server.am.ProcessRecord anrProcess, String activityShortComponentName,ApplicationInfo aInfo, String parentShortComponentName,WindowProcessController parentProcess, boolean aboveSystem, String annotation) {final int incomingPid = anrProcess.mPid;synchronized (mAnrRecords) {....// 将anr信息添加到list中mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName ....));}// 启动anr检查线程startAnrConsumerIfNeeded();}private void startAnrConsumerIfNeeded() {if (mRunning.compareAndSet(false, true)) {new AnrConsumerThread().start();}}

AnrConsumerThread 定义如下,该线程主要是遍历mAnrRecords,然后调用AnrRecord的appNotResponding方法。

> frameworks/base/services/core/java/com/android/server/am/AnrHelper.javaprivate class AnrConsumerThread extends Thread {.... @Overridepublic void run() {AnrRecord r;while ((r = next()) != null) {....// 是否要求仅转储自身final boolean onlyDumpSelf = reportLatency > EXPIRED_REPORT_TIME_MS;r.appNotResponding(onlyDumpSelf);.....}}}

AnrRecord.appNotResponding() 实现如下

> frameworks/base/services/core/java/com/android/server/am/AnrHelper.javaprivate static class AnrRecord {final com.android.server.am.ProcessRecord mApp;void appNotResponding(boolean onlyDumpSelf) {mApp.mErrorState.appNotResponding(mActivityShortComponentName, mAppInfo,mParentShortComponentName, mParentProcess, mAboveSystem, mAnnotation,onlyDumpSelf);}
}

mErrorState 是 ProcessErrorStateRecord 类的实例。

5.5 ProcessErrorStateRecord

ProcessErrorStateRecord.appNotResponding() 方法很长,主要做

  • 将 ANR 记录到主日志中。
  • 转储堆栈信息到跟踪文件中
  • 发出显示anr对话框的消息
> frameworks/base/services/core/java/com/android/server/am/ProcessErrorStateRecord.javavoid appNotResponding(String activityShortComponentName, ApplicationInfo aInfo ...) {....synchronized (mService) {// 如果是后台anr,就直接kill掉app,并打印原因if (isSilentAnr() && !mApp.isDebugging()) {mApp.killLocked("bg anr", ApplicationExitInfo.REASON_ANR, true);return;}synchronized (mProcLock) {// 设置app的notResponding状态,并查找errorReportReceivermakeAppNotRespondingLSP(activityShortComponentName,annotation != null ? "ANR " + annotation : "ANR", info.toString());mDialogController.setAnrController(anrController);}if (mService.mUiHandler != null) {// 调出臭名昭著的 App Not Responding 对话框Message msg = Message.obtain();msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;msg.obj = new AppNotRespondingDialog.Data(mApp, aInfo, aboveSystem);mService.mUiHandler.sendMessageDelayed(msg, anrDialogDelayMs);}}
}

mUiHandler 是Activity Manager Service中的handler,其实现如下

> frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.javafinal class UiHandler extends Handler {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {.....case SHOW_NOT_RESPONDING_UI_MSG: {// 处理显示ANR对话框的消息mAppErrors.handleShowAnrUi(msg);} break;}}
}

5.6 AppErrors

handleShowAnrUi对应实现如下,主要用于判断是否需要显示ANR对话框

> frameworks/base/services/core/java/com/android/server/am/AppErrors.javavoid handleShowAnrUi(Message msg) {final ProcessErrorStateRecord errState = proc.mErrorState;synchronized (mProcLock) {if (errState.getDialogController().hasAnrDialogs()) {// 已经显示ANR对话框,就直接returnMetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_APP_ANR,AppNotRespondingDialog.ALREADY_SHOWING);return;}// 满足弹出ANR对话框的条件if (mService.mAtmInternal.canShowErrorDialogs() || showBackground) {....// 调用controler 显示对话框errState.getDialogController().showAnrDialogs(data);} ...}
}

ProcessErrorStateRecord.getDialogController() 方法返回ErrorDialogController 对象

5.7 ErrorDialogController

ErrorDialogController 主要就是控制对话框的显示跟隐藏。

> frameworks/base/services/core/java/com/android/server/am/ErrorDialogController.javavoid showAnrDialogs(AppNotRespondingDialog.Data data) {List<Context> contexts = getDisplayContexts(mApp.mErrorState.isSilentAnr() /* lastUsedOnly */);mAnrDialogs = new ArrayList<>();for (int i = contexts.size() - 1; i >= 0; i--) {final Context c = contexts.get(i);// 创建新的ANR 对话框,AppNotRespondingDialog 即为我们见到的anr对话框mAnrDialogs.add(new AppNotRespondingDialog(mService, c, data));}// 显示dialogscheduleForAllDialogs(mAnrDialogs, Dialog::show);
}void scheduleForAllDialogs(List<? extends BaseErrorDialog> dialogs,Consumer<BaseErrorDialog> c) {mService.mUiHandler.post(() -> {if (dialogs != null) {forAllDialogs(dialogs, c);}});}
// 遍历 dialog列表,调用show方法
void forAllDialogs(List<? extends BaseErrorDialog> dialogs, Consumer<BaseErrorDialog> c) {for (int i = dialogs.size() - 1; i >= 0; i--) {c.accept(dialogs.get(i));}
}

5.8 AppNotRespondingDialog

ANR对话框的实现如下

> services/core/java/com/android/server/am/AppNotRespondingDialog.javafinal class AppNotRespondingDialog extends BaseErrorDialog implements View.OnClickListener {....@Overridepublic void onClick(View v) {switch (v.getId()) {case com.android.internal.R.id.aerr_report:// 点击了等待并且上报,该按钮只有在有ErrorReportReceiver的时候才显示mHandler.obtainMessage(WAIT_AND_REPORT).sendToTarget();break;case com.android.internal.R.id.aerr_close:// 将app杀死mHandler.obtainMessage(FORCE_CLOSE).sendToTarget();break;case com.android.internal.R.id.aerr_wait:// 继续等待mHandler.obtainMessage(WAIT).sendToTarget();break;default:break;}}private final Handler mHandler = new Handler() {public void handleMessage(Message msg) {switch (msg.what) {case FORCE_CLOSE:// 调用ActivityManagerService的killAppAtUsersRequest方法,将app kill掉mService.killAppAtUsersRequest(mProc);break;case WAIT_AND_REPORT:case WAIT:// 继续等待应用synchronized (mService) {ProcessRecord app = mProc;final ProcessErrorStateRecord errState = app.mErrorState;if (msg.what == WAIT_AND_REPORT) {appErrorIntent = mService.mAppErrors.createAppErrorIntentLOSP(app,System.currentTimeMillis(), null);}synchronized (mService.mProcLock) {// 清除anr的标识位errState.setNotResponding(false);// 退出对话框errState.getDialogController().clearAnrDialogs();}// 重新计算service anr的时间mService.mServices.scheduleServiceTimeoutLocked(app);// If the app remains unresponsive, show the dialog again after a delay.mService.mInternal.rescheduleAnrDialog(mData);}break;}if (appErrorIntent != null) {try {getContext().startActivity(appErrorIntent);} catch (ActivityNotFoundException e) {Slog.w(TAG, "bug report receiver dissappeared", e);}}dismiss();}};
}

5.9 调用栈

找到window场景,Java层的调用堆栈如下所示

services/core/java/com/android/server/am/AppNotRespondingDialog.java : show()
services/core/java/com/android/server/am/ErrorDialogController.java : showAnrDialogs()
services/core/java/com/android/server/am/AppErrors.java : handleShowAnrUi()
services/core/java/com/android/server/am/ActivityManagerService.java : UiHandler.SHOW_NOT_RESPONDING_UI_MSG
services/core/java/com/android/server/am/ProcessErrorStateRecord.java : appNotResponding()
services/core/java/com/android/server/am/AnrHelper.java : AnrRecord.appNotResponding()
services/core/java/com/android/server/am/AnrHelper.java : AnrConsumerThread.run()
services/core/java/com/android/server/am/AnrHelper.java : startAnrConsumerIfNeeded()
services/core/java/com/android/server/am/AnrHelper.java : appNotResponding()
services/core/java/com/android/server/am/ActivityManagerService.java : inputDispatchingTimedOut()
services/core/java/com/android/server/am/ActivityManagerService.java : LocalService.inputDispatchingTimedOut()
services/core/java/com/android/server/wm/ActivityRecord.java : inputDispatchingTimedOut()
services/core/java/com/android/server/wm/AnrController.java : notifyWindowUnresponsive()
services/core/java/com/android/server/wm/InputManagerCallback.java : notifyWindowUnresponsive()
services/core/java/com/android/server/input/InputManagerService.java : notifyWindowUnresponsive()

没有找到connection,但是找到对应应用,Java层的调用堆栈如下所示。

可以看到两者的调用栈区别并不大,主要是在InputManagerService中调用栈不一样。

services/core/java/com/android/server/am/AppNotRespondingDialog.java : show()
services/core/java/com/android/server/am/ErrorDialogController.java : showAnrDialogs()
services/core/java/com/android/server/am/AppErrors.java : handleShowAnrUi()
services/core/java/com/android/server/am/ActivityManagerService.java : UiHandler.SHOW_NOT_RESPONDING_UI_MSG
services/core/java/com/android/server/am/ProcessErrorStateRecord.java : appNotResponding()
services/core/java/com/android/server/am/AnrHelper.java : AnrRecord.appNotResponding()
services/core/java/com/android/server/am/AnrHelper.java : AnrConsumerThread.run()
services/core/java/com/android/server/am/AnrHelper.java : startAnrConsumerIfNeeded()
services/core/java/com/android/server/am/AnrHelper.java : appNotResponding()
services/core/java/com/android/server/am/ActivityManagerService.java : inputDispatchingTimedOut()
services/core/java/com/android/server/am/ActivityManagerService.java : LocalService.inputDispatchingTimedOut()
services/core/java/com/android/server/wm/ActivityRecord.java : inputDispatchingTimedOut()
services/core/java/com/android/server/wm/AnrController.java : notifyAppUnresponsive()
services/core/java/com/android/server/wm/InputManagerCallback.java : notifyNoFocusedWindowAnr()
services/core/java/com/android/server/input/InputManagerService.java : notifyNoFocusedWindowAnr()

5.10 cancelEventsForAnrLocked

cancelEventsForAnrLocked方法用于停止唤醒此连接上的事件,这些事件已经没有响应。

void InputDispatcher::cancelEventsForAnrLocked(const sp<Connection>& connection) {// 我们不会在这里中断任何连接,即使策略希望我们中止调度。如果策略决定关闭应用,// 我们将通过 unregisterInputChannel 获取通道删除事件,并以这种方式清理连接。// 当连接阻塞时,我们已经没有向连接发送新的指针,但重点事件将继续堆积。ALOGW("Canceling events for %s because it is unresponsive",connection->inputChannel->getName().c_str());if (connection->status == Connection::Status::NORMAL) {CancelationOptions options(CancelationOptions::CANCEL_ALL_EVENTS,"application not responding");synthesizeCancelationEventsForConnectionLocked(connection, options);}
}

6、总结

6.1 问题解答

先回答之前的两个问题

问题1:如果在activity任意周期(onCreate,onResume等),同步执行耗时超过5s(ANR时间)的任务,期间不进行点击,那会触发ANR吗?

答:不会。

原因:ANR的条件是waitQueue 不为空,在activity启动过程,没有触发按键事件的分发,也就 自然不会调用ANR检查流程,即processAnrsLocked()

问题2:如果在button点击的时候,在onClick回调同步执行耗时超过5s的任务。点击一次会触发ANR吗?点击2次呢,3次呢?

答:点击一次不会触发ANR,在上个任务结束之前,再次点击会导致ANR。

原因:在第一次点击的时候,在processAnrsLocked()检查ANR过程中,waitQueue虽然不为空,但是还没到超时时间。故不会触发ANR。

在第二次分发按键结束的时候,在processAnrsLocked()检查ANR过程中,发现waitQueue 不为空,并且已经有事件超时了,那么就会触发ANR的逻辑

6.2 ANR分类

当发生ANR的时候,System log 会出现如下信息,tag是 ActivityManager

"Input dispatching timed out (" + reason + ")"

其中reason取值如下:

  • 能找到focusedWindow:"%s is not responding. Waited %d ms for %s"
  • 不能找到focusedWindow,但是找到focused应用:Application does not have a focused window

在Android13 之前,这种分类的异常log是

Waiting because no window has focus but %s may eventually add a window when it finishes starting up. Will wait for %d ms
  • 其他类型:reason为null。

6.3 ANR 条件

  • 能找到window:waitQueue 不为空,并且当前时间 > 最早分发事件的超时时间。
  • 找不到window,但是找到focused应用:当前时间 > 等待FocusedWindow的时间。

6.4 修改超时时间

这个必须是厂家或者有root权限才能修改。

  • 通过修改 UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS 的值来改变timeOut值。
  • 通过修改ro.hw_timeout_multiplier 属性值(倍数,int值)来修改,需要root权限。

后记

最后,感谢大家能有耐心看完,这个过程会有点长,也希望读者能有所收获,共勉!

参考文献:

  • Input系统—ANR原理分析

【Android】带你细看Android input系统中ANR的机制相关推荐

  1. Android平台基于asmack实现XMPP协议中的PubSub机制

    Android平台基于asmack实现XMPP协议中的PubSub机制 本文主要介绍,在Android平台上基于asmack包,实现的PubSub机制,在PubSub中最重要的概念是节点Node,几乎 ...

  2. Android Studio 源码移植到系统中

    参考文章:https://blog.csdn.net/weixin_44008788/article/details/120990207 主要是添加 Android.mk文件,指定src.res.An ...

  3. android 获取蓝牙设备id_安卓蓝牙系统中如何获取蓝牙音乐的音频跟踪会话ID

    原标题:安卓蓝牙系统中如何获取蓝牙音乐的音频跟踪会话ID 蓝牙音乐AudioTrack Session ID的获取 当今这个音视频无处不在的时代,音频跟踪会话ID(AudioTrack Session ...

  4. 计算机关闭自带杀毒,Win10专业版系统中关闭自带杀毒软件操作方法

    相信大家对于电脑非常不陌生吧,那你知道Win10 1909系统中如何关闭自带的杀毒软件吗?是由于什么原因导致的呢?不知道了吧,其实系统设置非常简单,大家只需要按照小编下述所说的方法操作就可以了,下面小 ...

  5. Windows® CE 系统中的同步机制

    看到篇好文章,呵呵,独乐乐,不如众乐乐 本文转自http://blog.csdn.net/thl789/archive/2006/01/17/582246.aspx ,转载请注明出处 摘要 ... 1 ...

  6. WINCE 系统中的同步机制

    摘要 Windows® CE 是微软系列嵌入式平台所采用的操作系统内核.本文讨论了 WinCE 进程/线程之间的同步机制,给出了它们的典型应用场景.这些同步机制包括临界区.互斥体.信号量.事件.互锁函 ...

  7. android 带弧形背景,[Android日常]绘制弧形渐变背景

    最近要修改用户空间头部信息显示,参考了好多APP的用户空间,都有一个弧形的背景,看着挺漂亮的.实现这种效果,有两种实现方式:1.作图:2.通过代码进行绘制.今天就讲讲如何通过canvas进行绘制. 一 ...

  8. android 带弧形背景,Android 弧形ViewPager 和弧形HeaderView(升级版)

    封面.png 前段时间写了一篇项目总结的文章,总结了项目中使用的弧形View 和弧形ViewPager 效果,采用的是自定义View 的方法,然后绘制弧形采用的是二阶贝塞尔曲线,具体的思路和详情请看文 ...

  9. android bsp学习_Android BSP成长计划随笔之虚拟设备搭建和input系统

    由于工作关系,对Android关注将从FWK(Framework)转向BSP,也就是Linux Kernel.在工作的5年中,曾经数次研究过kernel,但一直没有合适的机会或者说推动力去深入研究.这 ...

最新文章

  1. A+B Problem III
  2. 【0729作业】随机生成20个手机号码
  3. 大数据学习——:wq不能退出vi编辑器
  4. xml操作之创建xml节点
  5. 主板19针接口_【新品上市】D4双通道还能组RAID!华南B365D4主板6/7/8/9代全兼容!...
  6. 浏览器打开出现证书错误_SSL证书=安全?小心,别错漏了TA…
  7. 平板电脑什么牌子好点_什么平板电脑充电柜好?
  8. 微软推出的Pylance,随着VS Code的更新,性能又前进了一步
  9. SAP Spartacus page-slot.component.html
  10. java借口案例实现_java实现接口的典型案例
  11. Python实现用户登录
  12. hihoCoder1223 不等式
  13. 新站结合熊掌号的实际操作 实现当天收录
  14. look与look like
  15. linux下编译C++文件基本命令
  16. 机器视觉,运动控制,C#联合雷赛运动控制卡,C#联合凌华控制 卡源码
  17. UDP测试工具(ace版本)
  18. 浏览器扩展应用安装AXURE插件
  19. FPS游戏的方框透视+自瞄原理
  20. [4G5G基础学习]:L3 RRC层概述与总体架构、ASN.1消息、无线承载SRB, DRB、终端三种状态、MIB, SIB,NAS消息类型

热门文章

  1. c++计算圆柱体表面积
  2. python通过ip获取地理位置等ip信息
  3. ZYNQ之FPGA 片内RAM读写测试实验
  4. SocketInputStream.socketRead0 导致线程hangs的解决方案
  5. 浅谈linux - 内核时间的处理
  6. Java AOP自定义注解
  7. 微信小程序 Page pages/Index/Index has not been registered yet.问题解决
  8. java语句翻译_java基础语句翻译
  9. 搜狗多账户cookies最新接口可用推送工具免费使用【2020】
  10. 小丸子学MongoDB系列之——部署MongoDB副本集