点击上方“何俊林”,马上关注,每天早上8:50准时推送

真爱,请置顶或星标

本文转载自公号玉刚说,原创作者htkeepmoving,原文链接:https://www.jianshu.com/p/ad1a84b6ec69

ANR概述

首先,ANR(Application Not responding)是指应用程序未响应,Android系统对于一些事件需要在一定的时间范围内完成,如果超过预定时间能未能得到有效响应或者响应时间过长,都会造成ANR。ANR由消息处理机制保证,Android在系统层实现了一套精密的机制来发现ANR,核心原理是消息调度和超时处理。

其次,ANR机制主体实现在系统层。所有与ANR相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,同时,系统进程设计了不同的超时限制来跟踪消息的处理。 一旦应用程序处理消息不当,超时限制就起作用了,它收集一些系统状态,譬如CPU/IO使用情况、进程函数调用栈,并且报告用户有进程无响应了(ANR对话框)。

然后,ANR问题本质是一个性能问题。ANR机制实际上对应用程序主线程的限制,要求主线程在限定的时间内处理完一些最常见的操作(启动服务、处理广播、处理输入), 如果处理超时,则认为主线程已经失去了响应其他操作的能力。主线程中的耗时操作,譬如密集CPU运算、大量IO、复杂界面布局等,都会降低应用程序的响应能力。

哪些场景会造成ANR?

  1. 发生ANR时会调用AppNotRespondingDialog.show()方法弹出对话框提示用户,该对话框的依次调用关系如下图所示:

  1. AppErrors.appNotResponding(),该方法是最终弹出ANR对话框的唯一入口,调用该方法的场景才会有ANR提示,也可以认为在主线程中执行无论再耗时的任务,只要最终不调用该方法,都不会有ANR提示,也不会有ANR相关日志及报告;通过调用关系可以看出哪些场景会导致ANR,有以下四种场景:

(1)Service Timeout:Service在特定的时间内无法处理完成
(2)BroadcastQueue Timeout:BroadcastReceiver在特定时间内无法处理完成
(3)ContentProvider Timeout:内容提供者执行超时
(4)inputDispatching Timeout: 按键或触摸事件在特定时间内无响应。

ANR机制

ANR机制可以分为两部分:
ANR监测机制:Android对于不同的ANR类型(Broadcast, Service, InputEvent)都有一套监测机制。
ANR报告机制:在监测到ANR以后,需要显示ANR对话框、输出日志(发生ANR时的进程函数调用栈、CPU使用情况等)。

整个ANR机制的代码也是横跨了Android的几个层:

  • App层:应用主线程的处理逻辑;

  • Framework层:ANR机制的核心,主要有AMS、BroadcastQueue、ActiveServices、InputmanagerService、InputMonitor、InputChannel、ProcessCpuTracker等;

  • Native层:InputDispatcher.cpp;

Provider超时机制遇到的比较少,暂不做分析;Broadcast目前主要想说两个知识点:

第一:无论是普通广播还是有序广播,最终广播接受者的onreceive都是串行执行的,可以通过Demo进行验证;

第二:通过Demo以及框架添加相关日志,都验证了普通广播也会有ANR监测机制,ANR机制以及问题分析文章认为只有串行广播才有ANR监测机制,后续再会专门讲解Broadcast发送及接收流程,同时也会补充Broadcast ANR监测机制;本文主要以Servie处理超时、输入事件分发超时为例探讨ANR监测机制。

Service超时监测机制

Service运行在应用程序的主线程,如果Service的执行时间超过20秒,则会引发ANR。

当发生Service ANR时,一般可以先排查一下在Service的生命周期函数中(onCreate(), onStartCommand()等)有没有做耗时的操作,譬如复杂的运算、IO操作等。 如果应用程序的代码逻辑查不出问题,就需要深入检查当前系统的状态:CPU的使用情况、系统服务的状态等,判断当时发生ANR进程是否受到系统运行异常的影响。

如何检测Service超时呢?Android是通过设置定时消息实现的。定时消息是由AMS的消息队列处理的(system_server的ActivityManager线程)。 AMS有Service运行的上下文信息,所以在AMS中设置一套超时检测机制也是合情合理的。
我们先抛出两个问题

  • 问题一:Service启动流程?

  • 问题一:如何监测Service超时?

主要通过以上两个问题来说明Service监测机制,在知道Service启动流程之后,通过Service启动流程可以更容易分析Service超时监测机制。

  1. Service启动流程如下图所示:

(1)ActiveServices.realStartServiceLocked()在通过app.thread的scheduleCreateService()来创建Service对象并调用Service.onCreate()后,接着又调用sendServiceArgsLocked()方法来调用Service的其他方法,如onStartCommand。以上两步均是进程间通信,应用与AMS之间跨进程通信可以参考应用进程与系统进程通信
(2)以上只是列出Service启动流程的关键步骤,具体每个方法主要做哪些工作还需要查看具体的代码,暂时先忽略这些,感兴趣的可以参考Android开发艺术探索等其他相关资料

  1. Service超时监测机制

Service超时监测机制可以从Service启动流程中找到。
(1)ActiveServices.realStartServiceLocked()主要工作有

    private final void realStartServiceLocked(ServiceRecord r,ProcessRecord app, boolean execInFg) throws RemoteException {...// 主要是为了设置ANR超时,可以看出在正式启动Service之前开始ANR监测;bumpServiceExecutingLocked(r, execInFg, "create");// 启动过程调用scheduleCreateService方法,最终会调用Service.onCreate方法;app.thread.scheduleCreateService(r, r.serviceInfo,// 绑定过程中,这个方法中会调用app.thread.scheduleBindService方法requestServiceBindingsLocked(r, execInFg);// 调动Service的其他方法,如onStartCommand,也是IPC通讯sendServiceArgsLocked(r, execInFg, true);}

(2)bumpServiceExecutingLocked()会调用scheduleServiceTimeoutLocked()方法

    void scheduleServiceTimeoutLocked(ProcessRecord proc) {if (proc.executingServices.size() == 0 || proc.thread == null) {return;}Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);msg.obj = proc;// 在serviceDoneExecutingLocked中会remove该SERVICE_TIMEOUT_MSG消息,// 当超时后仍没有remove SERVICE_TIMEOUT_MSG消息,则执行ActiveServices. serviceTimeout()方法;mAm.mHandler.sendMessageDelayed(msg,proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);// 前台进程中执行Service,SERVICE_TIMEOUT=20s;后台进程中执行Service,SERVICE_BACKGROUND_TIMEOUT=200s}

(3)如果在指定的时间内还没有serviceDoneExecutingLocked()方法将消息remove掉,就会调用ActiveServices. serviceTimeout()方法

void serviceTimeout(ProcessRecord proc) {...final long maxTime =  now -(proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);...// 寻找运行超时的Servicefor (int i=proc.executingServices.size()-1; i>=0; i--) {ServiceRecord sr = proc.executingServices.valueAt(i);if (sr.executingStart < maxTime) {timeout = sr;break;}...}...// 判断执行Service超时的进程是否在最近运行进程列表,如果不在,则忽略这个ANRif (timeout != null && mAm.mLruProcesses.contains(proc)) {anrMessage = "executing service " + timeout.shortName;}...if (anrMessage != null) {// 当存在timeout的service,则执行appNotResponding,报告ANRmAm.appNotResponding(proc, null, null, false, anrMessage);}
}

(4)Service onCreate超时监测整体流程如下图

在onCreate生命周期开始执行前,启动超时监测,如果在指定的时间onCreate没有执行完毕(该该方法中执行耗时任务),就会调用ActiveServices.serviceTimeout()方法报告ANR;如果在指定的时间内onCreate执行完毕,那么就会调用ActivityManagerService.serviceDoneExecutingLocked()方法移除SERVICE_TIMEOUT_MSG消息,说明Service.onCreate方法没有发生ANR,Service是由AMS调度,利用Handler和Looper,设计了一个TIMEOUT消息交由AMS线程来处理,整个超时机制的实现都是在Java层;以上就是Service超时监测的整体流程。

输入事件超时监测

应用程序可以接收输入事件(按键、触屏、轨迹球等),当5秒内没有处理完毕时,则会引发ANR。

这里先把问题抛出来了:
输入事件经历了一些什么工序才能被派发到应用的界面?
如何检测到输入时间处理超时?

1. Android输入系统简介

Android输入系统总体流程与参与者如下图所示。

简单来说,内核将原始事件写入到设备节点中,InputReader在其线程循环中不断地从EventHub中抽取原始输入事件,进行加工处理后将加工所得的事件放入InputDispatcher的派发发队列中。InputDispatcher则在其线程循环中将派发队列中的事件取出,查找合适的窗口,将事件写入到窗口的事件接收管道中。窗口事件接收线程的Looper从管道中将事件取出,交由窗口事件处理函数进行事件响应。关键流程有:原始输入事件的读取与加工;输入事件的派发;输入事件的发送、接收与反馈。其中输入事件派发是指InputDispatcher不断的从派发队列取出事件、寻找合适的窗口进行发送的过程,输入事件的发送是InputDispatcher通过Connection对象将事件发送给窗口的过程。

InputDispatcher与窗口之间的跨进程通信主要通过InputChannel来完成。在InputDispatcher与窗口通过InputChannel建立连接之后,就可以进行事件的发送、接收与反馈;输入事件的发送和接收主要流程如图所示:

其中,将输入事件注入派发队列后,会唤醒派发线程,派发线程循环由InputDispatcher.dispatchOnce函数完成;InputDispatcher将事件以InputMessage写入InputChannel之后,窗口端的looper被唤醒,进而执行NativeInputReceiver::handleEvent()开始输入事件的接收,从InputEventReceiver开始输入事件被派发到用户界面;以上只是输入事件的大致流程,更详细的流程可以参考相关资料;在了解输入系统的大致流程之后,我们来分析输入事件的超时监测机制。

2. 输入事件超时监测

按键事件超时监测整体流程如下图所示

(1)InputDispatcher::dispatchOnceInnerLocked():
根据事件类型选择不同事件的处理方法:InputDispatcher::dispatchKeyLocked()或者InputDispatcher::dispatchMotionLocked(),我们以按键事件超时监测为例进行说明;
(2)findFocusedWindowTargetsLocked()方法会调用checkWindowReadyForMoreInputLocked();该方法检查窗口是否有能力再接收新的输入事件;可能会有一系列的场景阻碍事件的继续派发,相关场景有:
场景1: 窗口处于paused状态,不能处理输入事件
“Waiting because the [targetType] window is paused.”

场景2: 窗口还未向InputDispatcher注册,无法将事件派发到窗口
“Waiting because the [targetType] window’s input channel is not registered with the input dispatcher. The window may be in the process of being removed.”

场景3: 窗口和InputDispatcher的连接已经中断,即InputChannel不能正常工作
“Waiting because the [targetType] window’s input connection is [status]. The window may be in the process of being removed.”

场景4: InputChannel已经饱和,不能再处理新的事件
“Waiting because the [targetType] window’s input channel is full. Outbound queue length: %d. Wait queue length: %d.”

场景5: 对于按键类型(KeyEvent)的输入事件,需要等待上一个事件处理完毕
“Waiting to send key event because the [targetType] window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: %d. Wait queue length: %d.”

场景6: 对于触摸类型(TouchEvent)的输入事件,可以立即派发到当前的窗口,因为TouchEvent都是发生在用户当前可见的窗口。但有一种情况, 如果当前应用由于队列有太多的输入事件等待派发,导致发生了ANR,那TouchEvent事件就需要排队等待派发。
“Waiting to send non-key event because the %s window has not finished processing certain input events that were delivered to it over %0.1fms ago. Wait queue length: %d. Wait queue head age: %0.1fms.”

以上这些场景就是我们常在日志中看到的ANR原因的打印。

(3)其中事件分发5s限制定义在InputDispatcher.cpp;InputDispatcher::handleTargetsNotReadyLocked()方法中如果事件5s之内还没有分发完毕,则调用InputDispatcher::onANRLocked()提示用户应用发生ANR;

//默认分发超时间为5s

const nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL;
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,const EventEntry* entry,const sp<InputApplicationHandle>& applicationHandle,const sp<InputWindowHandle>& windowHandle,nsecs_t* nextWakeupTime, const char* reason) {// 1.如果当前没有聚焦窗口,也没有聚焦的应用if (applicationHandle == NULL && windowHandle == NULL) {...} else {// 2.有聚焦窗口或者有聚焦的应用if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {// 获取等待的时间值if (windowHandle != NULL) {// 存在聚焦窗口,DEFAULT_INPUT_DISPATCHING_TIMEOUT事件为5stimeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);} else if (applicationHandle != NULL) {// 存在聚焦应用,则获取聚焦应用的分发超时时间timeout = applicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);} else {// 默认的分发超时时间为5stimeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;}}}// 如果当前时间大于输入目标等待超时时间,即当超时5s时进入ANR处理流程// currentTime 就是系统的当前时间,mInputTargetWaitTimeoutTime 是一个全局变量,if (currentTime >= mInputTargetWaitTimeoutTime) {// 调用ANR处理流程onANRLocked(currentTime, applicationHandle, windowHandle,entry->eventTime, mInputTargetWaitStartTime, reason);// 返回需要等待处理return INPUT_EVENT_INJECTION_PENDING;}
}

(4)当应用主线程被卡住的事件,再点击该应用其它组件也是无响应,因为事件派发是串行的,上一个事件不处理完毕,不会处理下一个事件。
(5)Activity.onCreate执行耗时操作,不管用户如何操作都不会发生ANR,因为输入事件相关监听机制还没有建立起来;InputChannel通道还没有建立
这时是不会响应输入事件,InputDispatcher还不能事件发送到应用窗口,ANR监听机制也还没有建立,所以此时是不会报告ANR的。
(6)输入事件由InputDispatcher调度,待处理的输入事件都会进入队列中等待,设计了一个等待超时的判断,超时机制的实现在Native层。
以上就是输入事件ANR监测机制;具体逻辑请参考相关源码;

ANR报告机制

无论哪种类型的ANR发生以后,最终都会调用 AppErrors.appNotResponding() 方法,所谓“殊途同归”。这个方法的职能就是向用户或开发者报告ANR发生了。 最终的表现形式是:弹出一个对话框,告诉用户当前某个程序无响应;输入一大堆与ANR相关的日志,便于开发者解决问题。

final void appNotResponding(ProcessRecord app, ActivityRecord activity,ActivityRecord parent, boolean aboveSystem, final String annotation) {...if (ActivityManagerService.MONITOR_CPU_USAGE) {// 1. 更新CPU使用信息。ANR的第一次CPU信息采样,采样数据会保存在mProcessStats这个变量中mService.updateCpuStatsNow();}// 记录ANR到EventLog中EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,app.processName, app.info.flags, annotation);// 输出ANR到main log.StringBuilder info = new StringBuilder();info.setLength(0);info.append("ANR in ").append(app.processName);if (activity != null && activity.shortComponentName != null) {info.append(" (").append(activity.shortComponentName).append(")");}info.append("\n");info.append("PID: ").append(app.pid).append("\n");if (annotation != null) {info.append("Reason: ").append(annotation).append("\n");}if (parent != null && parent != activity) {info.append("Parent: ").append(parent.shortComponentName).append("\n");}// 3. 打印调用栈。具体实现由dumpStackTraces()函数完成File tracesFile = ActivityManagerService.dumpStackTraces(true, firstPids,(isSilentANR) ? null : processCpuTracker,(isSilentANR) ? null : lastPids,nativePids);String cpuInfo = null;// MONITOR_CPU_USAGE默认为trueif (ActivityManagerService.MONITOR_CPU_USAGE) {// 4. 更新CPU使用信息。ANR的第二次CPU使用信息采样。两次采样的数据分别对应ANR发生前后的CPU使用情况mService.updateCpuStatsNow();synchronized (mService.mProcessCpuTracker) {// 输出ANR发生前一段时间内各个进程的CPU使用情况cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);}// 输出CPU负载info.append(processCpuTracker.printCurrentLoad());info.append(cpuInfo);}// 输出ANR发生后一段时间内各个进程的CPU使用率info.append(processCpuTracker.printCurrentState(anrTime));//会打印发生ANR的原因,如输入事件导致ANR的不同场景Slog.e(TAG, info.toString());if (tracesFile == null) {// There is no trace file, so dump (only) the alleged culprit's threads to the log// 发送signal 3(SIGNAL_QUIT)来dump栈信息Process.sendSignal(app.pid, Process.SIGNAL_QUIT);}// 将anr信息同时输出到DropBoxmService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,cpuInfo, tracesFile, null);// Bring up the infamous App Not Responding dialog// 5. 显示ANR对话框。抛出SHOW_NOT_RESPONDING_MSG消息,// AMS.MainHandler会处理这条消息,显示AppNotRespondingDialog对话框提示用户发生ANRMessage msg = Message.obtain();HashMap<String, Object> map = new HashMap<String, Object>();msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;msg.obj = map;msg.arg1 = aboveSystem ? 1 : 0;map.put("app", app);if (activity != null) {map.put("activity", activity);}mService.mUiHandler.sendMessage(msg);}
}

除了主体逻辑,发生ANR时还会输出各种类别的日志:
event log:通过检索”am_anr”关键字,可以找到发生ANR的应用
main log:通过检索”ANR in “关键字,可以找到ANR的信息,日志的上下文会包含CPU的使用情况
dropbox:通过检索”anr”类型,可以找到ANR的信息
traces:发生ANR时,各进程的函数调用栈信息

至此ANR相关报告已经完成,后续需要分析ANR问题,分析ANR往往是从main log中的CPU使用情况和traces中的函数调用栈开始。所以,更新CPU的使用信息updateCpuStatsNow()方法和打印函数栈dumpStackTraces()方法,是系统报告ANR问题关键所在,具体分析ANR问题请参考相关资料。

总结

  1. ANR的监测机制:首先分析Service和输入事件大致工作流程,然后从Service,InputEvent两种不同的ANR监测机制的源码实现开始,分析了Android如何发现各类ANR。在启动服务、输入事件分发时,植入超时检测,用于发现ANR。

  2. ANR的报告机制:分析Android如何输出ANR日志。当ANR被发现后,两个很重要的日志输出是:CPU使用情况和进程的函数调用栈,这两类日志是我们解决ANR问题的利器。

  3. 监测ANR的核心原理是消息调度和超时处理。

  4. 只有被ANR监测的场景才会有ANR报告以及ANR提示框。

参考资料
  • ANR机制以及问题分析

  • 理解Android ANR的触发原理

  • 深入理解Android卷三(Android输入系统)

  • Android开发艺术探索

  • Android源码

最后,感谢本文内容所参考文章的作者。

 推荐阅读

AI视频变脸,你竟然可以看见女明星们出现在这个网站

我敢打赌,你对ConcurrentHashMap不了解?

推荐8个超人气公号给大家

我想加入阿里,我该怎么做?

工作5年,为什么我越混越差?

喜欢就点击“在看”吧!

看完这篇 Android ANR 分析,就可以和面试官装逼了!相关推荐

  1. 看完这篇 JVM 垃圾回收,和面试官扯皮没问题了

    本文来源:码海 前言 Java 相比 C/C++ 最显著的特点便是引入了自动垃圾回收 (下文统一用 GC 指代自动垃圾回收),它解决了 C/C++ 最令人头疼的内存管理问题,让程序员专注于程序本身,不 ...

  2. 看完这篇文章,我再也不怕面试官问「垃圾回收」了...

    前言 Java 相比 C/C++ 最显著的特点便是引入了自动垃圾回收 (下文统一用 GC 指代自动垃圾回收),它解决了 C/C++ 最令人头疼的内存管理问题,让程序员专注于程序本身,不用关心内存回收这 ...

  3. 看完这篇Exception 和 Error,和面试官扯皮就没问题了

    来自:Java建设者 在 Java 中的基本理念是 结构不佳的代码不能运行,发现错误的理想时期是在编译期间,因为你不用运行程序,只是凭借着对 Java 基本理念的理解就能发现问题.但是编译期并不能找出 ...

  4. 看完这篇JVM内存管理机制,面试再也不慌了!

    /   今日科技快讯   / 近日,美国新冠肺炎确诊病例已破300万例,众多美企深受疫情打击.然而,特朗普政府当前正全力推进一项针对华为.中兴等中企产品的采购禁令,又让一众美企措手不及.7月10日,代 ...

  5. 看完这篇java单利模式文章,面试的时候再也不怕了

    来源:https://blog.csdn.net/weixin_44170221/article/details/106365623 哇塞,被称为Java中最简单的设计模式--单例设计模式.这都可以有 ...

  6. 【系统架构设计师】软考高级职称,一次通过,倾尽所有,看完这篇就够了,论软件架构设计的重要性、本篇论文“未通过考试”,供分析参考

    [系统架构设计师]软考高级职称,一次通过,倾尽所有,看完这篇就够了,学习方法和技巧这里全都有. 论软件架构设计的重要性.本篇论文未通过考试(不合格),供分析参考. 目录 摘要 正文 结尾 摘要 201 ...

  7. 红米k30 android版本,红米K30系列机型众多,傻傻分不清楚?看完这篇你就懂了

    红米K30系列机型众多,傻傻分不清楚?看完这篇你就懂了 2020-10-01 10:51:57 18点赞 33收藏 11评论 红米K30系列 不可否认的是,目前的手机厂商喜欢一次性发布几款新机型,以不 ...

  8. 看完这篇分析,还不懂分布式事物,请给我差评

    看完这篇分析,还不懂分布式事物,请给我差评 咖啡拿铁 架构师小秘圈 今天 作者:咖啡拿铁,现就职于美团点评,后端研发 来自:公众号咖啡拿铁(ID:code_3092860495) 0 题记 又或者在网 ...

  9. JVM难学?那是因为你没认真看完这篇文章

    JVM难学?那是因为你没认真看完这篇文章 一:虚拟机内存图解 JAVA程序运行与虚拟机之上,运行时需要内存空间.虚拟机执行JAVA程序的过程中会把它管理的内存划分为不同的数据区域方便管理. 虚拟机管理 ...

最新文章

  1. 第八章 路由协议原理
  2. 为什么UART串口通信要16倍过采样数据
  3. python compile正则_Python 正则表达式:compile,match
  4. java圆柱的底面积体积,计算圆柱的底面积和体积
  5. python 单链表查找元素_如何在python中一次找到链表的中间元素?
  6. respond java 使用_java – 使用android问题的HttpResponse:执行总...
  7. u盘pe无人值守linux,从U盘无人值守安装linux操作系统(纯实践笔记
  8. java 一个list根据另一个list进行排序_java – 基于另一个List对List进行排序
  9. android命令行 gles,Android利用OpenGLES绘制天空盒实例教程
  10. h700通话糊 索尼wi_索尼随身听变种!火爆日本的异形智能 500元最强索尼降噪神器来了...
  11. myisam为什么比innodb查询快_mysql存储引擎之MyISAM 和 InnoDB的比较
  12. 天书般的ICTCLAS分词系统代码(二)
  13. 服务器的登录日志文件,windows云服务器登录日志文件
  14. 34 模板方法模式(模板方法设计模式)详解
  15. android 播放器 samba,(发烧屋)教你如何解决蓝光机 KODI无法打开局域网SMB共享的问题/安卓播放器/硬盘播放器...
  16. mysql order by注入_sql注入之order by注入
  17. ssms管理linux数据库,使用 SSMS 管理 Linux 上的 SQL Server - SQL Server | Microsoft Docs
  18. 前端中的A、B、C端解释
  19. UE4 自定义按键事件(踩坑记录)
  20. iOS开发者账号过期续费

热门文章

  1. happens-before简介
  2. 求三角形面积(Python)
  3. 即使一个人,也要好好生活
  4. 【ES6】最简单的对象数组去重的方法
  5. 墨天轮2022年度数据库获奖名单
  6. 分治算法——分金块、求残缺棋盘问题
  7. 唐骏、学历门和魔术表演
  8. 阴什么圆什么的成语(阴什么圆什么四字成语大全)
  9. ChartJs多图表重影问题解决
  10. 魔法大锅炉-Eric Raymond