Android ANR的设计原理

吕通
• 阅读 881

ANR的设计原理

定时等待问题

先来看个小故事

老师给我布置了个作业,要求我10分钟内完成,他说10分钟后再来检查。

10分钟后,老师来检查,发现我作业没完成,就把我的名字写在黑板上,来警示其他人。

10分钟后,老师来检查,发现我作业写完了,就接着布置下一个作业了。

但是,这里有个问题,假如我5分钟就写完了作业,是不是可以主动去告诉老师,而不是让他再多等5分钟呢?

当然可以!

这样就可以提前结束本次等待过程,大大节省时间从而提高效率。

上述过程就简单的模拟了ANR的实现原理,更术语的说法如下。

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的检测过程。

上述的道理也很容易理解,A和B一定不能在同一个线程,因为如果是同一个线程,B如果陷入死循环,那么C永远都执行不到了,还检测个毛。

如果B执行完了,只去通知A清除标记F,而不清除C可以吗,也可以!但是这个时候C还会继续等待,等到T时间后,去检测F,F肯定是不在的,就检测了个寂寞,还不如直接取消。就像上述例子我提前去告诉老师一样,B提前去告诉A结束C。

所以,我们可以将ANR更精炼的总结为: 埋雷、拆雷和爆雷三个步骤

了解了基本道理,我们就可以通过代码来验证下,我们来看下四大组件中的Service的ANR检测逻辑。

Service的ANR源码分析

埋雷的过程

我们通过context.startService(intent)来启动service最终都会调用到ContextImpl里面去,最终通过AMS来发起一次跨进程通信,最终调用到system_server进程中去启动service,这里不再废话,直接列出流程。

  • 1 A进程中调用 context.startService(intent)
  • 2 最终调用到system_server进程的AMSstartService()中。
  • 3 最后会调用到system_server进程的ActiveServicerealStartServiceLocked()中。

我们就来看这个函数: ActiveService.realStartServiceLocked(),这里只贴出核心部分:

private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {
    // 核心函数: 开启ANR检测,也是埋雷和爆雷的地方
    bumpServiceExecutingLocked(r, execInFg, "create");
    try {
        // 核心函数: 启动服务,也是拆雷的地方
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
                app.getReportedProcState());
    } catch (DeadObjectException e) {
        throw e;
    } finally {
    }
}

核心逻辑有两个: 1 开启ANR检测; 2 启动服务。我们先来看ANR检测函数bumpServiceExecutingLocked():

private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    //...
    // 将ServiceRecord添加到ProcessRecord的executingServices里面去
    r.app.executingServices.add(r);
    // 开始进行ANR检测
    scheduleServiceTimeoutLocked(r.app);
    //...
}

上述代码只贴出核心部分,r.app是一个ProcessRecord,表示当前服务所属的进程,r.app.excutingServices表示当前进程正在执行的服务的集合,如下:

final class ServiceRecord extends Binder implements ComponentName.WithComponentName {
    // 当前服务所属的进程
    ProcessRecord app;
}

class ProcessRecord implements WindowProcessListener {
    // 正在执行的服务的集合
    final ArraySet<ServiceRecord> executingServices = new ArraySet<>();
}

也就是说,现在我们已经把要启动的Service,添加到进程的executingServices里面了,等价于添加了Flag了。

接着我们看进行ANR检测的方法 scheduleServiceTimeoutLocked,也就是爆雷的过程

爆雷的过程

以下代码位于ActiveService中,这里只贴出核心部分。

void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    // 如果没有正在执行的服务 或者 进程已经不再了,就返回
    if (proc.executingServices.size() == 0 || proc.thread == null) {
        return;
    }
    // 构建ANR消息,记住这个Flag: SERVICE_TIMEOUT_MSG
    Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    // 发射delay消息,如果是前台服务,delay时间就是SERVICE_TIMEOUT,否则delay时间就是SERVICE_BACKGROUND_TIMEOUT
    mAm.mHandler.sendMessageDelayed(msg,proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}

// 前台服务ANR的时间是20s
static final int SERVICE_TIMEOUT = 20*1000;
// 后台服务ANR的时间是200s
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;

这里可以看到,通过mAM.mHandlerpost一个消息,如果是前台服务,则检测时间是20s,如果是后台服务,检测时间是200s,那么我们就来看下这个mAm.mHandler里面被执行时候的逻辑吧。

mAm就是ActivityManagerService,以下代码位于ActivityManagerService中,这里只贴出核心部分。

final class MainHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            // case到这个Flag了
            case SERVICE_TIMEOUT_MSG: {
                // 进行检测,最后调到了ActiveService.serviceTimeout()
                mServices.serviceTimeout((ProcessRecord)msg.obj);
            } break;
        }
    }
}

我们跟着主线代码ActiveService.serviceTimeout()

void serviceTimeout(ProcessRecord proc) {
    // anr的消息
    String anrMessage = null;
    synchronized(mAm) {
        // 如果是debug引起的anr,无视
        if (proc.isDebugging()) {
            return;
        }
        // 如果进程已经没有要执行的服务 或者 进程不在了,就无视
        if (proc.executingServices.size() == 0 || proc.thread == null) {
            return;
        }
        // 记录当前时间
        final long now = SystemClock.uptimeMillis();
        // 计算服务最早的开始时间,如果小于这个时间,就是发生了ANR
        final long maxTime =  now - (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
        // 记录超时的服务
        ServiceRecord timeout = null;
        // 如果没有发生ANR,则记录下一条服务的开始时间
        long nextTime = 0;
        // 这里就从前面保存的executingServices列表中开始倒序比较了,还记得我们前面的: r.app.executingServices.add(r)吗
        // 遍历寻找发生ANR的Service
        for (int i=proc.executingServices.size()-1; i>=0; i--) {
            ServiceRecord sr = proc.executingServices.valueAt(i);
            // 如果小于最晚开始时间,则发生了ANR
            if (sr.executingStart < maxTime) {
                // 记录超时的服务
                timeout = sr;
                break;
            }
            // 如果没有发生ANR,就保存下一条服务的开始时间
            if (sr.executingStart > nextTime) {
                nextTime = sr.executingStart;
            }
        }
        // 分支1: 如果发生了ANR,并且进程还在,就提示ANR消息
        if (timeout != null && mAm.mProcessList.mLruProcesses.contains(proc)) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new FastPrintWriter(sw, false, 1024);
            pw.println(timeout);
            timeout.dump(pw, "    ");
            pw.close();
            // 构建ANR消息
            anrMessage = "executing service " + timeout.shortInstanceName;
        } else {
            // 分支2: 如果没发生ANR,就进行下一轮观测
            Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);
            msg.obj = proc;
            // 下一轮观测的时间就是 下一条服务的启动时间 + 服务的超时时间
            mAm.mHandler.sendMessageAtTime(msg, proc.execServicesFg
                    ? (nextTime+SERVICE_TIMEOUT) : (nextTime + SERVICE_BACKGROUND_TIMEOUT));
        }
    }
    
    // 如果anrMessage不为null,也就是发生了ANR消息,就交给系统处理(开启了ANR消息提示就会弹出提示框)
    if (anrMessage != null) {
        mAm.mAnrHelper.appNotResponding(proc, anrMessage);
    }
}

这块代码的逻辑是: 遍历正在执行的服务列表,查找发生了ANR的服务,如果找到了,就构建ANR消息并交给系统处理,否则就找到最小的下一条服务的开始执行时间,然后重新计算时间并进行ANR检测。

那么,ANR时间是怎么判断的呢?我们先看下它的相关计算:

// 记录当前时间
final long now = SystemClock.uptimeMillis();
// 计算服务最晚的开始时间,如果小于这个时间,就是发生了ANR
final long maxTime =  now - (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
if (sr.executingStart < maxTime) {
  // 发生了ANR
}

接着,我们来逐条讲解,我们假设本服务是前台服务:

// 首先,获取当前时间
final long now = SystemClock.uptimeMillis();
// (我们假设是前台服务),假设服务的开始时间是start,那么如果发生了ANR,就满足: now - start > SERVICE_TIMEOUT
// 也就是: now - SERVICE_TIMEOUT > start
final long maxTime =  now - SERVICE_TIMEOUT;
// 也就是: start < maxTime
if (sr.executingStart < maxTime) {
    // 发生了ANR
}

看下面的图更直接:

Android ANR的设计原理

接着,我们来看下拆雷的过程

拆雷的过程

我们先回顾下入口函数: ActiveService.realStartServiceLocked()

private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {
    // 核心函数: 开启ANR检测,也是埋雷和爆雷的地方
    bumpServiceExecutingLocked(r, execInFg, "create");
    try {
        // 核心函数: 启动服务,也是拆雷的地方
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
                app.getReportedProcState());
    } catch (DeadObjectException e) {
        throw e;
    } finally {
    }
}

其中,app.threadIApplicationThread接口,它的实现是ApplicationThread,是ActivityThread的一个内部类。代码如下所示:

private class ApplicationThread extends IApplicationThread.Stub {
    public final void scheduleCreateService(IBinder token,ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
        updateProcessState(processState, false);
        // 创建数据
        CreateServiceData s = new CreateServiceData();
        s.token = token;
        s.info = info;
        s.compatInfo = compatInfo;
        // 通过handler发射出去,那我们只需要跟这个 H.CREATE_SERVICE 就可以了
        sendMessage(H.CREATE_SERVICE, s);
    }
}

我们跟着H.CREATE_SERVICE,发现它的处理在我们的老朋友ActivityThreadH中,如下:

class H extends Handler {
    case CREATE_SERVICE:
        // 又是调用了handleXXXXYYYY系列函数
        handleCreateService((CreateServiceData)msg.obj);
        break;
}

我们点进去:

private void handleCreateService(CreateServiceData data) {
    // 暂停GC的处理
    unscheduleGcIdler();
    LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo, data.compatInfo);
    Service service = null;
    try {
        
        // 通过反射创建Service(记得当初Activity也是这么干的)
        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        Application app = packageInfo.makeApplication(false, mInstrumentation);
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        service = packageInfo.getAppFactory().instantiateService(cl, data.info.name, data.intent);
        context.getResources().addLoaders(app.getResources().getLoaders().toArray(new ResourcesLoader[0]));
        context.setOuterContext(service);
        // 调用service.attach(这里保存了context)
        service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService());
        // 回调onCreate()函数
        service.onCreate();
        mServices.put(data.token, service);
        try {
            // 核心函数,也就是拆雷的地方!
            ActivityManager.getService().serviceDoneExecuting( data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    } catch (Exception e) {
           //...
    }
}

上述逻辑: 通过反射创建服务;回调onCreate();拆雷。我们看拆雷的核心函数serviceDoneExecuting,位于ActivityManagerService中,这里只展示核心函数。

void serviceDoneExecutingLocked(ServiceRecord r, int type, int startId, int res) {
    boolean inDestroying = mDestroyingServices.contains(r);
    if (r != null) {
        // ...
        
        // 拆雷的核心函数
        serviceDoneExecutingLocked(r, inDestroying, inDestroying);
    } else {
        // ...
    }
}

紧跟着:

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing) {
    r.executeNesting--;
    if (r.executeNesting <= 0) {
        if (r.app != null) {
            // 重置前台服务标记
            r.app.execServicesFg = false;
            // 从executingServices中移除
            r.app.executingServices中移除.remove(r);
            if (r.app.executingServices.size() == 0) {
                // 如果没有正在执行的服务了,也就没必要再进行ANR检测了,就直接移除,也就是拆雷。
                mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
            } else if (r.executeFg) {
                // 处理其他逻辑
            }
        }
    }
}

这就是拆雷的过程,这里有个问题,如果r.app.executingServices.size() == 0不满足呢,就不移除了吗?没错!不移除也没有影响的,因为既然跑到了这里,说明本个服务已经执行完毕了,即使这个检测不移除,等它被执行到了,也检测不到本个服务的ANR,也就是爆雷阶段的分支2,会检测下一轮ANR信息。

当然,移除是最好的,但是为什么不移除呢?这里我也不懂,可能是因为post消息的时候,传递的object参数是r.app,是所有服务共享的进程,而不是单个服务独有的信息,从而导致不能移除,因为一旦移除,就导致所有检测的Message都被移除。如下:

// 检测的时候
Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc; // 这里传递的是进程proc,所有的service都用的它
mAm.mHandler.sendMessageDelayed(msg, proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);

// 移除的时候

// 如果这么干了,那么如果service1跑完了,就会导致service2的检测逻辑也会被移除,
// 因为service2检测用的msg.obj跟service1一样都是进程proc,所以不能移除,只能等没有服务执行了才全部移除。
mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);

如果能改成如下,会更好:

// 检测的时候
Message msg = mAm.mHandler.obtainMessage(ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = serviceRecord.xxx; // 传递本服务的独立信息
mAm.mHandler.sendMessageDelayed(msg, proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);

// 移除的时候
// 直接使用服务独立的信息,不影响其他服务
mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, serviceRecord.xxx);

也可能是我理解的不对!有理解的小伙伴可以在评论区指点迷津。

总结

ANR的检测信息很简单,这里再重复下:

  • 1 将要执行的service添加到系统进程的executingServices中。
  • 2 开启检测逻辑,检测将在指定时间后执行,具体时间决定与是前台服务还是后台服务。
  • 3 一旦服务被执行完,就会尝试移除检测逻辑。
  • 4 如果检测逻辑没被移除,就会被执行,然后去检测哪个服务发生了ANR
  • 5 如果发生了ANR,就将构建ANR信息提供给系统,否则就检测并执行下一轮ANR检测。
点赞
收藏
评论区
推荐文章
Easter79 Easter79
3年前
type
typeofpython作业作业练习:要想检查文本是否属于回文需要忽略其中的标点、空格与大小写。例如,“Risetovote,sir.”是一段回文文本,但是我们现有的程序不会这么认为。你可以改进上面的程序以使它能够识别这段回文吗?\\注:\\本文中用的python版本为3.70,编译
Aimerl0 Aimerl0
4年前
网络渗透测试实验一
写在前面现在信安专业老师上课的考核方式也是与时俱进,要求大家都有自己的博客,然后作业啥的都推到博客上,就不用交纸质档或者电子档的作业了,十分省事且与时俱进,好评网络渗透测试实验一:网络扫描与网络侦察实验目的理解网络扫描、网络侦察的作用;通过搭建网络渗透测试平台,了解并熟悉常用搜索引擎、扫描工具的应用,通过信息收集为下一步渗透工作打下基础。系统环境
TKE 用户故事 - 作业帮 PB 级低成本日志检索服务
作者吕亚霖,2019年加入作业帮,作业帮架构研发负责人,在作业帮期间主导了云原生架构演进、推动实施容器化改造、服务治理、GO微服务框架、DevOps的落地实践。莫仁鹏,2020年加入作业帮,作业帮高级架构师,在作业帮期间,推动了作业帮云原生架构演进,负责作业帮服务治理体系的设计和落地、服务感知体系建设以及自研mesh、MQproxy研发工作。摘要日志是服务
Stella981 Stella981
3年前
ScalaMP
1、前言        这个项目是一次课程作业,老师要求写一个并行计算框架,本人本身对openmp比较熟,加上又是scala的爱好者,所以想了许久,终于想到了用scala来实现一个类似openmp的一个简单的并行计算框架。项目github地址:ScalaMp(https://www.oschina.net/action/GoT
Stella981 Stella981
3年前
Flink 专题
CheckPoint1\.checkpoint保留策略默认情况下,checkpoint不会被保留,取消程序时即会删除他们,但是可以通过配置保留定期检查点,根据配置当作业失败或者取消的时候,不会自动清除这些保留的检查点。java:CheckpointConfi
Wesley13 Wesley13
3年前
ABAQUS 分析出现网格错误解决办法
我是从mimics的inp文件格式,已经进行了体网格划分,在3matic和abaqus中检查网格质量均为通过,但是分析作业提交后仍然出现了下列的错误:Thevolumeof2elementsiszero,small,ornegative.Checkcoordinatesornodenumbering,ormodify
Wesley13 Wesley13
3年前
2019 OO第一单元总结(表达式求导)
一.基于度量的程序结构分析1\.第一次作业  这次作业是我上手的第一个java程序,使用了4个类来实现功能。多项式采用两个arraylist来存,系数和幂指数一一对应。1privateArrayList<BigIntegercoefs;2privateArrayList<BigIntegerdegre
Stella981 Stella981
3年前
Python课 #02号作业
为了记录我的Python课,将我的作业发上来,欢迎各位大佬评鉴。如果你有什么更好的想法,请在下方评论或联系我。谢谢!作业一:由行转逆列描述编写程序实现如下功能:‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪
Wesley13 Wesley13
3年前
24岁的天空
    24岁,现在的我心里比较乱。    我13年毕业于西安一所高校,毕业后就去了合肥的一家软件企业转行做了软件开发,14年10月来的北京。我小的时候就属于那种很听话的孩子,该写的作业基本都能按时完成,该玩的时候去玩。所以从上小学开始一直到大学毕业(小学二三年级除外)我的成绩基本都比较不错的。虽然初中高中时候由于生活的原因,家人很少在我身
Wesley13 Wesley13
3年前
56、数据库设计(铁路购票系统)作业评改
实验目的:1、根据需求完成数据库设计建模,熟练使用ER模型;2、在数据库设计方案基础上实现为数据库。    作业要求(模拟火车票购票系统):1、以ER模型展示你的设计方案,要求包含完整的设计,有实体名称、实体属性、主键,并在图中体现实体间的关系;注:ER模型,可以手绘后拍照,也可以直接在WORD中绘制,还可以使用V
广电行业如何上云?来抄作业!
当代打工人最幸福的时刻,莫过于下班回家,打开手机在各个APP上冲浪,享受属于自己的快乐时光。随着互联网时代到来,人们的娱乐方式悄然转变,线上娱乐成为主流。面对日趋增长的线上娱乐需求,媒体内容采集、制作、分发等全流程也面临着新的挑战。国家广电总局印发《广播电视和网络视听“十四五”科技发展规划》指出,充分发挥科技的引领作用,大力促进云计算、大数据、物联网等新一代