卡顿、死锁、ANR原理,android面试自我介绍

伪代码如下

插桩前

fun method(){

run()

}

插桩后

fun method(){

input(1)

run()

output(1)

}

目前微信的Matrix 使用的卡顿监控方案就是字节码插桩,如下图所示

插桩需要注意的问题:

  1. 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。

  2. 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。

微信Matrix做了大量优化,整体包体积增加1%-2%,帧率下降2帧以内,对性能影响整体可以接受,不过依然只会在灰度包使用。

再来说说ANR~

三、ANR 原理


ANR 的类型和触发ANR的流程

3.1 哪些场景会造成ANR呢

  • Service Timeout:比如前台服务在20s内未执行完成,后台服务是10s;

  • BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s

  • ContentProvider Timeout:内容提供者,在publish过超时10s;

  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。

相关超时定义可以参考ActivityManagerService

// How long we allow a receiver to run before giving up on it.

static final int BROADCAST_FG_TIMEOUT = 10*1000;

static final int BROADCAST_BG_TIMEOUT = 60*1000;

// How long we wait until we timeout on key dispatching.

static final int KEY_DISPATCHING_TIMEOUT = 5*1000;

3.2 ANR触发流程

来简单分析下源码,ANR触发流程其实可以比喻成埋炸弹拆炸弹的过程,

以后台Service为例

3.2.1 埋炸弹

Context.startService

调用链如下:

AMS.startService

ActiveServices.startService

ActiveServices.realStartServiceLocked

ActiveServices.realStartServiceLocked

private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {

//1、这里会发送delay消息(SERVICE_TIMEOUT_MSG)

bumpServiceExecutingLocked(r, execInFg, “create”);

try {

//2、通知AMS创建服务

app.thread.scheduleCreateService(r, r.serviceInfo,

mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),

app.repProcState);

}

}

注释1的bumpServiceExecutingLocked内部调用scheduleServiceTimeoutLocked

void scheduleServiceTimeoutLocked(ProcessRecord proc) {

Message msg = mAm.mHandler.obtainMessage(

ActivityManagerService.SERVICE_TIMEOUT_MSG);

msg.obj = proc;

// 发送deley消息,前台服务是20s,后台服务是10s

mAm.mHandler.sendMessageDelayed(msg,

proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);

}

注释2通知AMS启动服务之前,注释1处发送Handler延时消息,埋下炸弹,如果10s内(前台服务是20s)没人来拆炸弹,炸弹就会爆炸,即ActiveServices#serviceTimeout方法会被调用

3.2.2 拆炸弹

启动一个Service,先要经过AMS管理,然后AMS会通知应用进程执行Service的生命周期, ActivityThreadhandleCreateService方法会被调用

-> ActivityThread#handleCreateService

private void handleCreateService(CreateServiceData data) {

try {

Application app = packageInfo.makeApplication(false, mInstrumentation);

service.attach(context, this, data.info.name, data.token, app,

ActivityManager.getService());

//1、service onCreate调用

service.onCreate();

mServices.put(data.token, service);

try {

//2、拆炸弹在这里

ActivityManager.getService().serviceDoneExecuting(

data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);

} catch (RemoteException e) {

throw e.rethrowFromSystemServer();

}

}

}

注释1,ServiceonCreate方法被调用,

注释2,调用AMS的serviceDoneExecuting方法,最终会调用到ActiveServices. serviceDoneExecutingLocked

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,

boolean finishing) {

//移除delay消息

mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);

}

可以看到,onCreate方法调用完之后,就会移除delay消息,炸弹被拆除。

3.2.3 引爆炸弹

假设Service的onCreate执行超过10s,那么炸弹就会引爆,也就是

ActiveServices#serviceTimeout方法会被调用

void serviceTimeout(ProcessRecord proc) {

if (anrMessage != null) {

mAm.mAppErrors.appNotResponding(proc, null, null, false, anrMessage);

}

}

所有ANR,最终都会调用AppErrorsappNotResponding方法

AppErrors #appNotResponding

final void appNotResponding(ProcessRecord app, ActivityRecord activity,

ActivityRecord parent, boolean aboveSystem, final String annotation) {

//1、写入event log

// Log the ANR to the event log.

EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,

app.processName, app.info.flags, annotation);

//2、收集需要的log,anr、cpu等,StringBuilder凭借

// Log the ANR to the 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”);

}

ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);

// 3、dump堆栈信息,包括java堆栈和native堆栈,保存到文件中

// For background ANRs, don’t pass the ProcessCpuTracker to

// avoid spending 1/2 second collecting stats to rank lastPids.

File tracesFile = ActivityManagerService.dumpStackTraces(

true, firstPids,

(isSilentANR) ? null : processCpuTracker,

(isSilentANR) ? null : lastPids,

nativePids);

String cpuInfo = null;

//4、输出ANR 日志

Slog.e(TAG, info.toString());

if (tracesFile == null) {

// 5、没有抓到tracesFile,发一个SIGNAL_QUIT信号

// There is no trace file, so dump (only) the alleged culprit’s threads to the log

Process.sendSignal(app.pid, Process.SIGNAL_QUIT);

}

StatsLog.write(StatsLog.ANR_OCCURRED, …)

// 6、输出到drapbox

mService.addErrorToDropBox(“anr”, app, app.processName, activity, parent, annotation, cpuInfo, tracesFile, null);

synchronized (mService) {

mService.mBatteryStatsService.noteProcessAnr(app.processName, app.uid);

//7、后台ANR,直接杀进程

if (isSilentANR) {

app.kill(“bg anr”, true);

return;

}

//8、错误报告

// Set the app’s notResponding state, and look up the errorReportReceiver

makeAppNotRespondingLocked(app,

activity != null ? activity.shortComponentName : null,

annotation != null ? "ANR " + annotation : “ANR”,

info.toString());

//9、弹出ANR dialog,会调用handleShowAnrUi方法

// Bring up the infamous App Not Responding dialog

Message msg = Message.obtain();

msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;

msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);

mService.mUiHandler.sendMessage(msg);

}

}

主要流程如下:

1、写入event log

2、写入 main log

3、生成tracesFile

4、输出ANR logcat(控制台可以看到)

5、如果没有获取到tracesFile,会发一个SIGNAL_QUIT信号,这里看注释是会触发收集线程堆栈信息流程,写入traceFile

6、输出到drapbox

7、后台ANR,直接杀进程

8、错误报告

9、弹出ANR dialog,会调用 AppErrors#handleShowAnrUi方法。

ANR触发流程小结

ANR触发流程,可以比喻为埋炸弹和拆炸弹的过程,

以启动Service为例,Service的onCreate方法调用之前会使用Handler发送延时10s的消息,Service 的onCreate方法执行完,会把这个延时消息移除掉。

假如Service的onCreate方法耗时超过10s,延时消息就会被正常处理,也就是触发ANR,会收集cpu、堆栈等信息,弹ANR Dialog。

service、broadcast、provider 的ANR原理都是埋定时炸弹和拆炸弹原理,

但是input的超时检测机制稍微有点不同,需要等收到下一次input事件,才会去检测上一次input事件是否超时,input事件里埋的炸弹是普通炸弹,需要通过扫雷来排查。

四、ANR 分析方法


上面已经分析了ANR触发流程,最终会把发生ANR时的线程堆栈、cpu等信息保存起来,我们一般都是分析 /data/anr/traces.txt 文件

4.1 模拟死锁导致ANR

private fun testAnr(){

val lock1 = Object()

val lock2 = Object()

//子线程持有锁1,想要竞争锁2

thread {

synchronized(lock1){

Thread.sleep(100)

synchronized(lock2){

Log.d(TAG, “testAnr: getLock2”)

}

}

}

//主线程持有锁2,想要竞争锁1

synchronized(lock2){

Thread.sleep(100)

synchronized(lock1){

Log.d(TAG, “testAnr: getLock1”)

}

}

}

触发ANR之后,一般我们会拉取anr日志: adb pull /data/traces.txt(文件名可能是anr_xxx.txt)

4.2 分析ANR 文件

首先看主线程,搜索 main

ANR日志中有很多信息,可以看到,主线程id是1(tid=1),在等待一个锁,这个锁一直被id为22的程持有,那么看下22号线程的堆栈

id为22的线程是Blocked状态,正在等待一个锁,这个锁被id为1的线程持有,同时这个22号线程还持有一个锁,这个锁是主线程想要的。

通过ANR日志,可以很清楚分析出这个ANR是死锁导致的,并且有具体堆栈信息。

上面只是举例一种死锁导致ANR的情况,实际项目中,可能有很多情况会导致ANR,例如内存不足、CPU被抢占、系统服务没有及时响应等等。

如果是线上问题,怎么样才能拿到ANR日志呢?

五、ANR 监控


前面已经分析了ANR触发流程,以及常规的线下分析方法,看起来还是有点繁琐的,需要pull出anr日志,然后分析线程堆栈等信息。对于线上ANR,如何搭建一个完善的ANR监控系统呢?

下面将介绍ANR监控的方式

5.1 抓取系统traces.txt 上传

1、当监控线程发现主线程卡死时,主动向系统发送SIGNAL_QUIT信号。

2、等待/data/anr/traces.txt文件生成。

3、文件生成以后进行上报。

看起来好像可行,不过有以下两个问题:

1、traces.txt 里面包含所有线程的信息,上传之后需要人工过滤分析

2、很多高版本系统需要root权限才能读取 /data/anr这个目录

既然这个方案存在问题,那么可还有其它办法?

5.2 ANRWatchDog

ANRWatchDog 是一个自动检测ANR的开源库

5.2.1 ANRWatchDog 原理

其源码只有两个类,核心是ANRWatchDog这个类,继承自Thread,它的run 方法如下,看注释处

public void run() {

setName(“|ANR-WatchDog|”);

long interval = _timeoutInterval;

// 1、开启循环

while (!isInterrupted()) {

boolean needPost = _tick == 0;

_tick += interval;

if (needPost) {

// 2、往UI线程post 一个Runnable,将_tick 赋值为0,将 _reported 赋值为false

_uiHandler.post(_ticker);

}

try {

// 3、线程睡眠5s

Thread.sleep(interval);

} catch (InterruptedException e) {

_interruptionListener.onInterrupted(e);

return ;

}

// If the main thread has not handled _ticker, it is blocked. ANR.

// 4、线程睡眠5s之后,检查 _tick 和 _reported 标志,正常情况下_tick 已经被主线程改为0,_reported改为false,如果不是,说明 2 的主线程Runnable一直没有被执行,主线程卡住了

if (_tick != 0 && !_reported) {

if (_namePrefix != null) {

// 5、判断发生ANR了,那就获取堆栈信息,回调onAppNotResponding方法

error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);

} else {

error = ANRError.NewMainOnly(_tick);

}

_anrListener.onAppNotResponding(error);

interval = _timeoutInterval;

_reported = true;

}

}

}

ANRWatchDog 的原理是比较简单的,概括为以下几个步骤

  1. 开启一个线程,死循环,循环中睡眠5s

  2. 往UI线程post 一个Runnable,将_tick 赋值为0,将 _reported 赋值为false

  3. 线程睡眠5s之后检查_tick和_reported字段是否被修改

  4. 如果_tick和_reported没有被修改,说明给主线程post的Runnable一直没有被执行,也就说明主线程卡顿至少5s**(只能说至少,这里存在5s内的误差)**。

  5. 将线程堆栈信息输出

其中涉及到并发的一个知识点,关于 volatile 关键字的使用,面试中的常客, volatile的特点是:保证可见性,禁止指令重排,适合在一个线程写,其它线程读的情况。

面试中一般会展开问JMM,工作内存,主内存等,以及为什么要有工作内存,能不能所有字段都用 volatile 关键字修饰等问题。

回到ANRWatchDog本身,细心的同学可能会发现一个问题,使用ANRWatchDog有时候会捕获不到ANR,是什么原因呢?

5.2.2 ANRWatchDog 缺点

ANRWatchDog 会出现漏检测的情况,看图

ANRWatchDog漏检测

如上图这种情况,红色表示卡顿,

  1. 假设主线程卡顿了2s之后,ANRWatchDog这时候刚开始一轮循环,将_tick 赋值为5,并往主线程post一个任务,把_tick修改为0

  2. 主线程过了3s之后不卡顿了,将_tick赋值为0

  3. 等到ANRWatchDog睡眠5s之后,发现_tick的值是0,判断为没有发生ANR。而实际上,主线程中间是卡顿了5s,ANRWatchDog误差是在5s之内的(5s是默认的,线程的睡眠时长)

针对这个问题,可以做一下优化。

5.3 ANRMonitor

ANRWatchDog 漏检测的问题,根本原因是因为线程睡眠5s,不知道前一秒主线程是否已经出现卡顿了,如果改成每间隔1秒检测一次,就可以把误差降低到1s内

接下来通过改造ANRWatchDog ,来做一下优化,命名为ANRMonitor

我们想让子线程间隔1s执行一次任务,可以通过 HandlerThread来实现

流程如下:

核心的Runnable代码

@Volatile

var mainHandlerRunEnd = true

//子线程会间隔1s调用一次这个Runnable

private val mThreadRunnable = Runnable {

blockTime++

//1、标志位 mainHandlerRunEnd 没有被主线程修改,说明有卡顿

if (!mainHandlerRunEnd && !isDebugger()) {

logw(TAG, “mThreadRunnable: main thread may be block at least $blockTime s”)

}

//2、卡顿超过5s,触发ANR流程,打印堆栈

if (blockTime >= 5) {

if (!mainHandlerRunEnd && !isDebugger() && !mHadReport) {

mHadReport = true

//5s了,主线程还没更新这个标志,ANR

loge(TAG, "ANR->main thread may be block at least $blockTime s ")

loge(TAG, getMainThreadStack())

//todo 回调出去,这里可以按需把其它线程的堆栈也输出

//todo debug环境可以开一个新进程,弹出堆栈信息

}

}

//3、如果上一秒没有卡顿,那么重置标志位,然后让主线程去修改这个标志位

if (mainHandlerRunEnd) {

mainHandlerRunEnd = false

mMainHandler.post {

mainHandlerRunEnd = true

}

}

//子线程间隔1s调用一次mThreadRunnable

sendDelayThreadMessage()

}

  1. 子线程每隔1s会执行一次mThreadRunnable,检测标志位 mainHandlerRunEnd 是否被修改

  2. 假如mainHandlerRunEnd如期被主线程修改为true,那么重置mainHandlerRunEnd标志位为false,然后继续执行步骤1

  3. 假如mainHandlerRunEnd没有被修改true,说明有卡顿,累计卡顿5s就触发ANR流程

在监控到ANR的时候,除了获取主线程堆栈,还有cpu、内存占用等信息也是比较重要的,demo中省略了这部分内容。

5.3.1 测试ANR

5.3.2 ANR检测结果

logcat打印所示

主线程卡顿超过5s,会打堆栈信息,如果是卡顿1-5s内,会有warning的log 提示,线下可以做成弹窗或者toast提示,

看到这里,大家应该能想到,线下也可以用这种方法检测卡顿,定位到耗时的代码。

此方案可以结合ProcessLifecycleOwner,应用在前台才开启检测,进入后台则停止检测。

六、死锁监控


在发生ANR的时候,有时候只有主线程堆栈信息可能还不够,例如发生死锁的情况,需要知道当前线程在等待哪个锁,以及这个锁被哪个线程持有,然后把发生死锁的线程堆栈信息都收集到。

流程如下:

  1. 获取当前blocked状态的线程

  2. 获取该线程想要竞争的锁

  3. 获取该锁被哪个线程持有

  4. 通过关系链,判断死锁的线程,输出堆栈信息

在Java层并没有相关API可以实现死锁监控,可以从Native层入手。

6.1 获取当前blocked状态的线程

这个比较简单,一个for循环就搞定,不过我们要的线程id是native层的线程id,Thread 内部有一个native线程地址的字段叫 nativePeer,通过反射可以获取到。

Thread[] threads = getAllThreads();

for (Thread thread : threads) {

if (thread.getState() == Thread.State.BLOCKED) {

long threadAddress = (long) ReflectUtil.getField(thread, “nativePeer”);

// 找不到地址,或者线程已经挂了,此时获取到的可能是0和-1

if (threadAddress <= 0) {

continue;

}

…后续

}

}

有了native层线程地址,还需要找到native层相关函数

6.2 获取当前线程想要竞争的锁

从ART 源码可以找到这个函数 androidxref.com/8.0.0_r4/xr…

函数:Monitor::GetContendedMonitor

从源码和源码的解释可以看出,这个函数是用来获取当前线程等待的Monitor。

顺便说说Monitor以及Java对象结构

Monitor

Monitor是一种并发控制机制,提供多线程环境下的互斥和同步,以支持安全的并发访问。

Monitor由以下3个元素组成:

  1. 临界区:例如synchronize修饰的代码块

  2. 条件变量:用来维护因不满足条件而阻塞的线程队列

  3. Monitor对象,维护Monitor的入口、临界区互斥量(即锁)、临界区和条件变量,以及条件变量上的阻塞和唤醒

Java的Class对象

Java的Class对象包括三部分组成:

  1. 对象头:MarkWord和对象指针

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

本文在开源项目GitHub中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

目前已经更新的部分资料,需要的自己取:



r。

顺便说说Monitor以及Java对象结构

Monitor

Monitor是一种并发控制机制,提供多线程环境下的互斥和同步,以支持安全的并发访问。

Monitor由以下3个元素组成:

  1. 临界区:例如synchronize修饰的代码块

  2. 条件变量:用来维护因不满足条件而阻塞的线程队列

  3. Monitor对象,维护Monitor的入口、临界区互斥量(即锁)、临界区和条件变量,以及条件变量上的阻塞和唤醒

Java的Class对象

Java的Class对象包括三部分组成:

  1. 对象头:MarkWord和对象指针

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-D5ElzB7A-1710902271644)]
[外链图片转存中…(img-pWz5DoyX-1710902271644)]
[外链图片转存中…(img-GuGW3vQA-1710902271645)]
[外链图片转存中…(img-Di6cnd1H-1710902271645)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-ErwhDzxU-1710902271645)]

最后

本文在开源项目GitHub中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

目前已经更新的部分资料,需要的自己取:

[外链图片转存中…(img-7zZnjslZ-1710902271646)]
[外链图片转存中…(img-xs9iDOCN-1710902271646)]
[外链图片转存中…(img-h0MPsl23-1710902271647)]

  • 22
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值