timeview未就绪_android input anr分析
input 系统
input系统涉及的源码主要有
(libinputflinger.so)frameworks/native/services/inputflinger/
- InputDispatcher.cpp
- InputReader.cpp
- InputManager.cpp
- EventHub.cpp
- InputListener.cpp
(libinput.so) frameworks/native/libs/input/
- InputTransport.cpp
- Input.cpp
- InputDevice.cpp
- Keyboard.cpp
- KeyCharacterMap.cpp
- IInputFlinger.cpp
(system_server)frameworks/base/services/core/
- java/com/android/server/input/InputManagerService.java
- jni/com_android_server_input_InputManagerService.cpp
如果需要调试input系统,应该gdb app_process,并在Android设备上attach system_server
input系统的主要类图如下:
以上类图主要关注: - InputReader负责从输入设备(EventHub)获取事件,并将该事件通知给QueuedInputListener,并最终通知到InputDispatcher - InputDispatcher负责将输入事件分发到正确的窗口上,并会处理ANR问题 - InputManager是InputReader和InputDispatcher的枢纽,类似MVC中到Controller作用,并对外提供InputManagerService
InputDispatcher实现
要想分析input ANR产生的原理需要先了解InputDispatcher的实现。
InputDispatcher的实现主要涉及3个Queue: 1. InboundQueue: 这个队列里面存储的是从InputReader送来到输入事件 2. OutboundQueue:这个队列里面存储的是即将要发送给应用的输入事件 3. WaitQueue:这个队列里面存储的是已经发给应用的事件,但应用还未处理完成
InputDispatcher内部维护了一个线程,负责不断从InboundQueue读取事件,获取到焦点窗口后,将事件放入OutboundQueue,并分发给应用后放入WaitQueue。WaitQueue是否有元素,元素的“生成时间”与焦点窗口是否应该触发ANR联系紧密。
InputDispatcher的主要流程如下:
流程图中看,正常的时序如下: 1. 在无等待事件时,才会消耗InboundQueue 2. 只有在窗口就绪的情况下才会将事件从InboundQueue(实际上已经缓存到mPendingEvent)移到OutboundQueue 3. OutboundQueue中的事件会进入WaitQueue,并等待应用处理完成后从WaitQueue移除
发生ANR的时序如下: 1. 窗口未就绪的判断条件是当前时间大于WaitQueue队头500ms 2. 窗口未就绪的话,当前事件会转为wait状态,并设置waitCause, waitTimeoutTime(waitTimeoutTime=currentTime+5s)等 3. 窗口未就绪且已经处于wait状态,则会检查当前时间是否大于waitTimeoutTime,如果是,则触发ANR
因此,发生ANR的条件是:WaitQueue有元素,且mPendingEvent等待(加入OutboundQueue)超时
以上过程,主要涉及InputDispatcher.cpp中的如下函数:
dispatchOnce: 循环入口
dispatchOnceInnerLocked:消费InboundQueue
dispatchMotionLocked:分发触摸事件
findTouchedWindowTargetsLocked/findFocusedWindowTargetsLocked:寻找焦点窗口
checkWindowReadyForMoreInputLocked:检查窗口是否就绪
handleTargetsNotReadyLocked:串口未就绪处理逻辑
onANRLocked:触发ANR
dumpsys input
在了解了原理后,可以借助dumpsys input来分析一些问题。
执行dumpsys input后,看到的信息结构如下:
INPUT MANAGER (dumpsys input)
Event Hub State:
......
Input Reader State:
......
Input Dispatcher State:
......
Input Dispatcher State at time of last ANR:
......
这里主要看下Input Dispatcher State。 dispatcher等dump由dumpDispatchStateLocked函数打印。主要结构如下:
Input Dispatcher State:
DispatchEnabled: 1
DispatchFrozen: 0
FocusedApplication: name='AppWindowToken{3f0c92ab token=Token{1ea37cfa ActivityRecord{235b7325 u0 com.android.launcher3/.Launcher t607}}}', dispatchingTimeout=5000.000ms
FocusedWindow: name='Window{37886f95 u0 com.android.launcher3/com.android.launcher3.Launcher}'
TouchStatesByDisplay:
0: down=false, split=false, deviceId=6, source=0x00002002
Windows:
Windows:
......
MonitoringChannels:
......
RecentQueue: length=10:
......
PendingEvent:
InboundQueue:
ReplacedKeys:
Connections:
......
8: channelName='37886f95 com.android.launcher3/com.android.launcher3.Launcher (server)', windowName='Window{37886f95 u0 com.android.launcher3/com.android.launcher3.Launcher}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue:
......
AppSwitch: not pending
Configuration:
KeyRepeatDelay: 50.0ms
KeyRepeatTimeout: 500.0ms
以上可以看到InboundQueue,OutboundQueue,WaitQueue 3个Queue的状态,以及PendingEvent的值。InputDispatcher与应用之间维持的socket连接叫做Connection,用于通知事件和反馈事件处理完成。
从上面dump的信息看,焦点窗口是launcher3,没有未处理的输入事件。
ANR Demo 1 —— dispatchTouchEvent超时
因为dispatchTouchEvent这个函数处理结束后才会通过Connection通知InputDispatcher事件处理完成。因此,我们重载这个函数来模拟ANR:
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
Log.i(TAG, "handle event: "+ev)
Thread.sleep(100000)
return super.dispatchTouchEvent(ev)
}
函数执行一开始sleep 10s,来阻塞当前事件的处理。
程序运行后触屏点击一次Activity, 打印"handle event",并10s sleep,这时,执行adb shell dumpsys input,输出如下:
PendingEvent:
InboundQueue:
Connections:
......
11: channelName='14a0d447 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{14a0d447 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue: length=3
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (740.0, 75.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=0, age=1411.8ms, wait=1411.3ms
MotionEvent(deviceId=5, source=0x00001002, action=2, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (741.0, 80.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=1363.3ms, wait=1363.2ms
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (741.0, 80.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=1, age=1347.2ms, wait=1347.0ms
可以看到PendingEvent, InboundQueue和OutboundQueue都是空,而WaitQueue中有3个MotionEvent,分别对应action 0,2,1,也就是Down,Move,Up。
产生这样输出的原理是,在点击前,窗口没有在处理任何输入,因此对于down可以添加到WaitQueue。而对于Move和Up,由于是几乎同时产生和处理的,因此在判断窗口是否就绪时(当前时间是否大于WaitQueue队头500ms),发现窗口就绪,因此一并加入到了WaitQueue.
根据以上分析,此时,WaitQueue有元素,那么(500ms后)再次点击,往InqueueBound加入事件后,由于WaitQueue仍在处理,窗口未就绪,因此PendingEvent无法加入到WaitQueue。
如果5s后再次尝试添加PendingEvent时窗口仍未就绪,则会触发ANR。
再点击一次,看看dump输出:
PendingEvent:
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (716.0, 75.0)]), policyFlags=0x62000000, age=2067.0ms
InboundQueue: length=1
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (716.0, 75.0)]), policyFlags=0x62000000, age=1985.6ms
Connections:
......
11: channelName='14a0d447 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{14a0d447 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue: length=2
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (691.0, 62.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=0, age=3362.6ms, wait=3362.0ms
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (691.0, 62.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=1, age=3265.9ms, wait=3265.8ms
WaitQueue和之前的dump相同,因为应用还在sleep, dwon仍未处理完成。
但PendingEvent为down, InboundQueue为up(快速点击时不会有move),然后,5s后触发了ANR。
ANR Demo 2 —— Button onClick处理超时
以上通过dispatchTouchEvent结合原理分析了一次ANR的产生过程。然而,如果时在onButtonClicked中模拟ANR,则会有所不同。
fun onTestBtnClick(view: View){
Thread.sleep(100000)
}
第一次点击:
PendingEvent:
InboundQueue:
Connections:
......
11: channelName='3d4d7501 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{3d4d7501 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue:
PendingEvent, InboundQueue, OutboundQueue, WaitQueue均为空。
第二次点击:
PendingEvent:
InboundQueue:
Connections:
......
11: channelName='323faa65 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{323faa65 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue: length=2
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (701.0, 72.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=0, age=4015.0ms, wait=4014.3ms
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (701.0, 72.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=1, age=3950.4ms, wait=3950.2ms
PendingEvent, InboundQueue, OutboundQueue均为空。WaitQueue有此次点击的两个事件(down/up)。5s后并未发生ANR。
第三次点击:
PendingEvent:
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (723.0, 76.0)]), policyFlags=0x62000000, age=2377.9ms
InboundQueue: length=2
MotionEvent(deviceId=5, source=0x00001002, action=2, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (718.0, 74.0)]), policyFlags=0x62000000, age=2329.0ms
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (718.0, 74.0)]), policyFlags=0x62000000, age=2312.8ms
Connections:
......
11: channelName='323faa65 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{323faa65 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue: length=2
MotionEvent(deviceId=5, source=0x00001002, action=0, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (701.0, 72.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=0, age=4015.0ms, wait=4014.3ms
MotionEvent(deviceId=5, source=0x00001002, action=1, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (701.0, 72.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=1, age=3950.4ms, wait=3950.2ms
第三次点击5s后才出现ANR。为什么与dispatchTouchEvent不同,需要点击3次呢?
因为onButtonClick是在dispatchTouchEvent执行完成后转抛到主线程的一条消息。也就是当进入onButtonClick函数时,第一次点击的down/up事件已经执行完成,并从WaitQueue移除。第二次点击因为WaitQueue为空,认为窗口就绪,所以事件被加入到WaitQueue,但是应用主线程被阻塞,所以dispatchTouchEvent不能及时处理消息,事件就停留在了WaitQueue中。第三次点击时,窗口未就绪,事件被放入PendingEvent,5s超时后触发ANR。
鼠标和触屏差异
当用鼠标操作Demo2时,会发现第二次点击时就触发了ANR。这是因为鼠标事件多了一个action=7(ACTION_HOVER_MOVE),并且是在处理完onButtonClick后才会从WaitQueue移除。
不妨看下第一次点击后的dump:
PendingEvent:
InboundQueue:
Connections:
11: channelName='a40bbc6 com.cy.anr/com.cy.anr.MainActivity (server)', windowName='Window{a40bbc6 u0 com.cy.anr/com.cy.anr.MainActivity}', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue:
WaitQueue: length=1
MotionEvent(deviceId=6, source=0x00002002, action=7, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (754.3, 84.0)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=7, age=2587.3ms, wait=2586.6ms
可以看到WaitQueue length=1,其中的元素为MotionEvent action=7。
UI线程对Input事件的处理
那么是否只要确保主线程处理时间小于5s就可以不发生(input)ANR了呢?答案是否定的。
在进一步分析“何时应用产生ANR”之前,需要先了解下UI线程对Input事件的处理过程。
UI线程与InputDispatcher间的交互是通过一个名为InputChannel的类实现的,详细的通信过程如下:
如图所示,InputDispatcher通过publishKeyEvent将事件通过InputChannel(内部基于sokcetpair实现)“发送”到UI线程后,会“等待”UI线程处理完成。(实现上这是一个异步过程,并非同步等待)
上图未提及的了几个重要信息: 1. 如果收到应用发来的消息,则唤醒InputDispatcher的线程处理该消息,处理完该消息后,如果有未发送的事件(PendingEvent或InboundQueue有未处理事件),则会再次尝试发送。 2. consumeEvents的内部实现。consumeEvents内部循环读取socket recvBuf中的事件,直至buf为空,也就是说,在一次的主线程循环中,consumeEvents会处理多个InputDispatcher发来的event。 3. consumeEvents被nativePollOnce中调用,nativePollOnce在主线程每次获取下一条消息时都被执行(MessageQueue.next())
这些信息影响下列行为: 1. 在dispatchTouchEvent中sleep 4s,快速点击,会触发ANR 2. 在onButtonClicked中sleep 4s,快速点击按钮,并不会出发ANR
在具体分析这两个行为的ANR前,看下这两个函数的调用栈: 1. dispatchTouchEvent:
dispatchTouchEvent:30, MainActivity {com.cy.anr}
......
onInputEvent:6210, ViewRootImpl$WindowInputEventReceiver {android.view}
dispatchInputEvent:185, InputEventReceiver {android.view}
nativePollOnce:-1, MessageQueue {android.os}
next:143, MessageQueue {android.os}
loop:122, Looper {android.os}
main:5280, ActivityThread {android.app}
nativePollOnce隐藏了具体的native调用。如果去分析native代码,可以知道:InputEventReceiver.dispatchInputEvent被native的consumeEvents调用。onButtonClicked:
onTestBtnClick:23, MainActivity {com.cy.anr}
invoke:-1, Method {java.lang.reflect}
invoke:372, Method {java.lang.reflect}
onClick:4044, View$1 {android.view}
performClick:4809, View {android.view}
run:20006, View$PerformClick {android.view}
handleCallback:739, Handler {android.os}
dispatchMessage:95, Handler {android.os}
loop:135, Looper {android.os}
main:5280, ActivityThread {android.app}
可以看到,dispatchTouchEvent是在收到InputChannel消息后的那次主线程循环中,从consumeEvents一路调用过来的。而onButtonClicked则不是,onButtonClicked是识别完touch意图后转换为另一个“事件”,在consumeEvents调用链中加入到消息队列后调度的。
接下来具体分析上述两种行为。
ANR Demo 3 —— dispatchTouchEvent 4s sleep 并快速点击
从原理可知,dispatchTouchEvent发生在InputChannel消息处理的当次主线程循环中,因此sleep dispatchTouchEvent造成的阻塞,对WaitQueue而言是“同步”的。
假设每次touch的时间间隔为300ms,3次点击的示意图如下:
以上示意图中的时间为deliveryTime,即加入(准备加入)WaitQueue的时间。
分析如下: 1. 第1次点击,没有在处理事件,可以加入;第2次点击,距离WaitQueue.head小于500ms,可以加入WaitQueue; 2. 第3次点击,超过500ms,因此放入PendingEvent,等待下次循环重试 3. 4s后,Down+0处理完成,第3次点击的Down(Down+600)现在距离WaitQueue队头4600,窗口为就绪,仍然无法加入到WaitQueue,继续等待 4. 在第5000ms时线程唤醒,发现仍然无法加入到WaitQueue,触发ANR
ANR Demo 4 —— onButtonClicked 4s sleep 并快速点击
从原理可知,onButtonClicked发生在InputChannel消息处理的下次循环,因此sleep onButtonClicked造成的阻塞,对WaitQueue而言是“异步”的。
假设每次touch的时间间隔为300ms,4次点击的示意图如下:
分析如下: 1. 第一次点击,没有在处理事件,加入后被快速处理完成,转成了onButtonClicked事件,主线程开始处理onButtonCicked。因此示意图省略了第一次点击,直接从第2次点击开始。 2. 第2、3次顺利加入WaitQueue 3. 第4次,由于deliveryTime大于WaitQueue.head 500ms,所以加入PendingEvent 4. 4s后,onButtonClicked处理完成。主线程又开始处理InputChannel消息,之前分析过consumeEvents会处理多个InputChannel消息,所以WaitQueue被清空了 5. WaitQueue被清空后,等待中的Down/Up(第4次点击)被加入WaitQueue,并又立即被处理,最后清空了WaitQueue。 6. 极限条件下,第4次点击可能没被立即处理,到下次onButtonClicked处理完成就会被处理(不是所有onButtonClicked处理完成,因为nativePollOnce每次主线程循环都执行)
因此,对于这个Demo而言,不论点击速度多快,效果上都是,WaitQueue中,tail.deliveryTime - head.deliveryTime <= 500ms, PendingEvent和InboundQueue中缓冲了未处理的事件;当主线程处理完一次循环后,WaitQueue被消化完,未处理事件也可能被消化完。不论是否未处理事件是否被消化完,继续点击,会回到WaitQueue累计近500ms事件、PendingEvent和InboundQueue中缓冲了未处理的事件的状态,如此重复。
总结如果在dispatchTouchEvent, onTouchEvent等处于consumeEvents调用链中,执行耗时操作,即使未达到5s,也可能因为事件累计导致ANR。导致ANR的条件是在有PendingEvent时,WaitQueue中累积事件的总处理时长大于5s。
如果在onButtonClicked等非consumeEvents调用链中,执行耗时操作,一定是大于5s(且有PendingEvent)才会触发ANR。
timeview未就绪_android input anr分析相关推荐
- TDengine 单节点Cluster not ready( 群集未就绪) 异常问题分析及解决方案
TDengine单节点群集未就绪解决方案 问题表现 问题排查 问题解决 反思 问题表现 在开发中,创建单节点TDengine后,启动taosd服务后,经taos命令链接,jdbc链接正常,创建表空间, ...
- Android 系统(135)---Android anr 分析步骤总结
Android anr 分析步骤总结 前言:最近经手了比较多的anr问题,声明经手不是解决,只是从log上推断造成anr的原因,以此作为根据转交给对应的人来处理. 1. ANR简介 ANR全名Appl ...
- (五十二) Android anr 分析步骤总结
前言:最近经手了比较多的anr问题,声明经手不是解决,只是从log上推断造成anr的原因,以此作为根据转交给对应的人来处理. 1. ANR简介 ANR全名Application Not Respond ...
- 拉取 trace.txt 进行 anr 分析
一.ANR 介绍 ANR的全称是application not responding,意思就是程序未响应. 首先ANR的发生是有条件限制的,分为以下三点: 1.只有主线程才会产生ANR,主线程就是UI ...
- java.io.IOException: 设备未就绪
java.io.IOException: 设备未就绪.at java.io.WinNTFileSystem.canonicalize0(Native Method)at java.io.Win32Fi ...
- 从数据库导出Excel上线后出现IO异常:设备未就绪之解决方法
最近做项目遇到一个问题,就是利用JavaPOI导出Excel表格时,在自己电脑上的工程上面可以使用,但是项目一上线,这个功能就报错,错误如下: 设备未就绪异常,报错的语句是createNewFile( ...
- linux input子系统分析--子系统核心.事件处理层.事件传递过程
linux input子系统分析--子系统核心.事件处理层.事件传递过程 一. 输入子系统核心分析. 1.输入子系统核心对应与/drivers/input/input.c文件,这个也是作为一个模块注 ...
- linux input子系统分析--主要函数
linux input子系统分析--主要函数 一. 各种注册函数 因为分析一所讲的每种数据结构都代表一类对象,所以每种数据结构都会对应一个注册函数,他们都定义在子系统核心的input.c文件中.主要有 ...
- linux input子系统分析--概述与数据结构
linux input子系统分析--概述与数据结构 Input子系统处理输入事务,任何输入设备的驱动程序都可以通过Input输入子系统提供的接口注册到内核,利用子系统提供的功能来与用户空间交互.输入设 ...
最新文章
- 一份忧伤的大厂生存百科
- 利用MySQL数据库来处理中英文取首字母排序
- 一学即懂得计算机视觉
- #舍得Share#Flash Media Server4.5迅雷高速下载地址by lwxshow
- VS网站开发的发布部署的不同情况说明
- for-each 循环原理
- 用Python实现快速排序
- C# params的用法详解
- .NET Core 3.0 部署在docker上运行
- “约见”面试官系列之常见面试题之第八十五篇之css响应式(建议收藏)
- java数字类型_Java数据类型
- 中国移动全球通寻宝第四期攻略
- Maven学习总结(36)——Apache Maven 3.5.0抢鲜看
- php 增加数组下标_PHP数组排序更改下标KEY方法
- 一副对联,送给所有创业小公司
- Kubernetes 小白学习笔记(14)--k8s集群路线-kubernetes核心组件详解
- matlab 无法进行符号运算,无法使用syms 命令
- [DELPHI] 使用mod函数换行
- 分布式轻量级任务调度框架-XXL-JOB(最全面,附带本人实战)
- 杨焘鸣 杨涛鸣:怎样建立自己的人脉网络