记录一个因为dumpsys导致应用出现ANR的问题

本文深入分析了一次应用程序无响应(ANR)的情况,通过主线程和渲染线程的状态追踪,定位到阻塞源头为渲染线程中的dprintf函数,并详细解释了阻塞原因及解决思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们先看一下这个ANR的主线程状态:

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7569ab08 self=0x77afe14c00
  | sysTid=3188 nice=0 cgrp=default sched=0/0 handle=0x78359a6550
  | state=S schedstat=( 4907584566 6966621388 17409 ) utm=341 stm=149 core=0 HZ=100
  | stack=0x7fd97db000-0x7fd97dd000 stackSize=8MB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/3188/stack)
  native: #00 pc 000000000001f06c  /system/lib64/libc.so (syscall+28)
  native: #01 pc 00000000000221d0  /system/lib64/libc.so (__futex_wait_ex(void volatile*, bool, int, bool, timespec const*)+140)
  native: #02 pc 0000000000080f4c  /system/lib64/libc.so (pthread_cond_wait+60)
  native: #03 pc 000000000047f1fc  /system/lib64/libhwui.so (android::uirenderer::renderthread::DrawFrameTask::postAndWait()+260)
  native: #04 pc 000000000047f0c8  /system/lib64/libhwui.so (android::uirenderer::renderthread::DrawFrameTask::drawFrame()+44)
  at android.view.ThreadedRenderer.nSyncAndDrawFrame(Native method)
  at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:823)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:3312)
  at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3116)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2481)
  at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1457)
  at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7202)
  at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)
  at android.view.Choreographer.doCallbacks(Choreographer.java:761)
  at android.view.Choreographer.doFrame(Choreographer.java:696)
  at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)
  at android.os.Handler.handleCallback(Handler.java:873)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:193)
  at android.app.ActivityThread.main(ActivityThread.java:6734)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:506)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

从主线程的堆栈我们可以看出,应用block在了renderthread的postAndWait方法,我们接着看一下这个方法的实现:

void DrawFrameTask::postAndWait() {
    AutoMutex _lock(mLock);
    mRenderThread->queue().post([this]() { run(); });
    mSignal.wait(mLock);
}

从这里我们可以看出是mSignal这个在等待锁释放,进一步搜索发现mSignal是在unblockUiThread方法中释放的

void DrawFrameTask::unblockUiThread() {
    AutoMutex _lock(mLock);
    mSignal.signal();
}

那么unblockUiThread方法是什么时候调用的呢?搜索代码发现该方法在DrawFrameTask的run方法中调用

void DrawFrameTask::run() {
    ATRACE_NAME("DrawFrame");

    bool canUnblockUiThread;
    bool canDrawThisFrame;
    {
        TreeInfo info(TreeInfo::MODE_FULL, *mContext);
        canUnblockUiThread = syncFrameState(info);
        canDrawThisFrame = info.out.canDrawThisFrame;

        if (mFrameCompleteCallback) {
            mContext->addFrameCompleteListener(std::move(mFrameCompleteCallback));
            mFrameCompleteCallback = nullptr;
        }
    }

    // Grab a copy of everything we need
    CanvasContext* context = mContext;
    std::function<void(int64_t)> callback = std::move(mFrameCallback);
    mFrameCallback = nullptr;

    // From this point on anything in "this" is *UNSAFE TO ACCESS*
    if (canUnblockUiThread) {
        unblockUiThread();
    }

    // Even if we aren't drawing this vsync pulse the next frame number will still be accurate
    if (CC_UNLIKELY(callback)) {
        context->enqueueFrameWork([callback, frameNr = context->getFrameNumber()]() {
            callback(frameNr);
        });
    }

    if (CC_LIKELY(canDrawThisFrame)) {
        context->draw();
    } else {
        // wait on fences so tasks don't overlap next frame
        context->waitOnFences();
    }

    if (!canUnblockUiThread) {
        unblockUiThread();
    }
}

根据postAndWait方法我们可以看到run方法实际是在renderthread中的消息队列执行的,这里的消息队列可以理解为和Android Java层中的Handler那个消息队列差不多。因此我们可以得出结论主线程被DrawFrameTask::postAndWait方法block,是因为上一次postAndWait放入到renderthread消息队列的run方法并未执行,所以下一步我们需要找出renderthread这个线程的堆栈信息,看看这个线程在干什么

"RenderThread" daemon prio=7 tid=19 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x13280ed8 self=0x778f662c00
  | sysTid=3558 nice=0 cgrp=default sched=0/0 handle=0x778e87f4f0
  | state=R schedstat=( 36485428771 60660326144 33388 ) utm=1153 stm=2495 core=2 HZ=100
  | stack=0x778e784000-0x778e786000 stackSize=1009KB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/3558/stack)
  native: #00 pc 000000000006f29c  /system/lib64/libc.so (write+8)
  native: #01 pc 000000000006c80c  /system/lib64/libc.so (__dwrite+16)
  native: #02 pc 000000000007d94c  /system/lib64/libc.so (fflush+180)
  native: #03 pc 000000000006c7c4  /system/lib64/libc.so (vdprintf+176)
  native: #04 pc 000000000007b7bc  /system/lib64/libc.so (dprintf+112)
  native: #05 pc 000000000012fc64  /system/lib64/libhwui.so (android::uirenderer::JankTracker::dumpFrames(int)+224)
  native: #06 pc 0000000000112f90  /system/lib64/libhwui.so (_ZNSt3__120__packaged_task_funcIZN7android10uirenderer12renderthread11RenderProxy15dumpProfileInfoEiiE4$_27NS_9allocatorIS5_EEFvvEEclEv$d1b19a60f4b1bb5f6a5927e3e6deb93d+84)
  native: #07 pc 000000000048290c  /system/lib64/libhwui.so (std::__1::packaged_task<void ()>::operator()()+88)
  native: #08 pc 00000000004366c4  /system/lib64/libhwui.so (android::uirenderer::WorkQueue::process()+168)
  native: #09 pc 0000000000114ee8  /system/lib64/libhwui.so (android::uirenderer::renderthread::RenderThread::threadLoop()+240)
  native: #10 pc 000000000000fa68  /system/lib64/libutils.so (android::Thread::_threadLoop(void*)+280)
  native: #11 pc 0000000000081a08  /system/lib64/libc.so (__pthread_start(void*)+36)
  native: #12 pc 00000000000234cc  /system/lib64/libc.so (__start_thread+68)
  (no managed stack frames)

从堆栈里面,我们可以很容易看到renderthread线程正在执行一个dumpFrames方法,而且block在了dprintf这个函数上。

void JankTracker::dumpFrames(int fd) {
    dprintf(fd, "\n\n---PROFILEDATA---\n");
    for (size_t i = 0; i < static_cast<size_t>(FrameInfoIndex::NumIndexes); i++) {
        dprintf(fd, "%s", FrameInfoNames[i].c_str());
        dprintf(fd, ",");
    }
    for (size_t i = 0; i < mFrames.size(); i++) {
        FrameInfo& frame = mFrames[i];
        if (frame[FrameInfoIndex::SyncStart] == 0) {
            continue;
        }
        dprintf(fd, "\n");
        for (int i = 0; i < static_cast<int>(FrameInfoIndex::NumIndexes); i++) {
            dprintf(fd, "%" PRId64 ",", frame[i]);
        }
    }
    dprintf(fd, "\n---PROFILEDATA---\n\n");
}

查阅资料得知,dprintf是一个往管道写入数据的方法。而写管道卡住可能有如下原因:
1、管道的read被关闭
2、管道的write写入太多数据,read太慢
3、整个系统的I/O出现了重大问题
因此我们需要看一下这个dumpFrames方法的管道是谁创建的,最终我们查阅源码的调用栈发现,这个管道是在AMS里面创建的

final void dumpGraphicsHardwareUsage(FileDescriptor fd,
            PrintWriter pw, String[] args) {
        ...

        for (int i = procs.size() - 1 ; i >= 0 ; i--) {
            ProcessRecord r = procs.get(i);
            if (r.thread != null) {
                pw.println("\n** Graphics info for pid " + r.pid + " [" + r.processName + "] **");
                pw.flush();
                try {
                    TransferPipe tp = new TransferPipe();
                    try {
                        r.thread.dumpGfxInfo(tp.getWriteFd(), args);
                        tp.go(fd);
                    } finally {
                        tp.kill();
                    }
                } catch (IOException e) {
                    pw.println("Failure while dumping the app: " + r);
                    pw.flush();
                } catch (RemoteException e) {
                    pw.println("Got a RemoteException while dumping the app " + r);
                    pw.flush();
                }
            }
        }
    }

可以看到这里实际是用户执行了dumpsys命令以后,AMS为每个Android进程创建了一个TransferPipe线程以及对应的管道,然后去获取对应的显示信息。我们再来看看管道的创建:

protected TransferPipe(String bufferPrefix, String threadName) throws IOException {
        mThread = new Thread(this, threadName);
        mFds = ParcelFileDescriptor.createPipe();
        mBufferPrefix = bufferPrefix;
    }
public static ParcelFileDescriptor[] createPipe() throws IOException {
        try {
            final FileDescriptor[] fds = Os.pipe();
            return new ParcelFileDescriptor[] {
                    new ParcelFileDescriptor(fds[0]),
                    new ParcelFileDescriptor(fds[1]) };
        } catch (ErrnoException e) {
            throw e.rethrowAsIOException();
        }
    }

从上面创建管道的代码中,我们可以看到这是一个阻塞的管道,也就是说如果write写满了,会直接阻塞write。然后我们再结合AMS的代码以及测试提供的dumpsys文件,我们可以看出其他进程的显示信息均正常打印,而我们这个ANR 应用的显示信息却没有,说明是ANR应用的显示信息太多了,导致占满了管道,进而阻塞了renderthread,进而阻塞了主线程,最终导致了ANR的发生。

<think>好的,我现在需要帮助用户解决关于测试应用ANR增量特性的代码实现和调试方法的问题。用户提到了几个具体的API,比如Utils.hasIncrementalFeature、ConfigUtils.uploadConfigForPushedAtomWithUid、StatsLog.EventMetricData,还希望了解如何统计ANR_OCCURRED事件以及验证增量特性支持。首先,我需要理清用户的需求,确保理解正确。 首先,用户想实现的是在Android设备上统计ANR事件的发生次数,并且支持增量特性。增量特性可能指的是只上传变化的数据,而不是每次全量上传,这样可以节省资源和流量。接下来,用户提到的几个API需要逐一分析它们的用途和用法。 Utils.hasIncrementalFeature可能用于检测设备是否支持增量特性。这通常涉及检查系统版本或特定功能是否存在。例如,Android可能在某个版本后引入了增量上报的支持,或者需要特定的权限或配置。 ConfigUtils.uploadConfigForPushedAtomWithUid可能用于配置上报参数,比如设置要上报的事件类型(这里应该是ANR_OCCURRED),以及相关的用户ID或应用ID。这个API可能需要传入配置对象,指定事件类型、上报频率等。 StatsLog.EventMetricData应该用于记录和上报事件数据。StatsLog是Android框架中用于打点统计的类,EventMetricData可能是一个数据结构,包含事件的具体信息,如时间戳、事件类型、附加参数等。 接下来,用户需要代码实现的示例,以及如何验证这些功能的调试方法。可能需要编写一个检测ANR的监听器,当ANR发生时触发事件记录,并通过上述API进行上报。同时,验证部分可能需要检查日志,或者使用adb命令触发ANR事件,观察统计结果是否正确。 我需要确保代码示例正确使用这些API,并符合Android的最佳实践。比如,监听ANR可能需要注册ContentObserver或者监听特定的系统广播,但根据Android版本不同,权限和方式可能有所变化。另外,使用StatsLog需要适当的权限,可能在AndroidManifest.xml中声明。 调试方法可能包括使用adb命令模拟ANR,例如发送SIGSTOP信号给主线程,然后观察应用是否正确地捕获并上报事件。同时,查看Logcat输出,确认相关日志是否正确生成,以及上报是否成功。 还需要考虑用户提到的项目地址,可能是一个现有的库或示例代码,用户可能希望集成或参考该项目的实现。因此,在回答中可以建议用户参考该项目,特别是关于ANR检测和日志收集的部分。 此外,用户的问题涉及到增量特性,可能需要处理配置的动态更新,比如当配置变化时,如何重新上传配置,确保上报的数据符合最新的设置。因此,代码示例中可能需要展示如何动态配置上报参数。 最后,需要提醒用户注意权限问题,以及不同Android版本的兼容性,特别是涉及到底层API的使用时,可能需要在代码中进行版本检查,避免在低版本设备上调用不支持的API导致崩溃。 总结步骤: 1. 检测增量特性支持:使用Utils.hasIncrementalFeature检查设备是否支持。 2. 配置上报参数:使用ConfigUtils.uploadConfigForPushedAtomWithUid设置上报ANR事件。 3. 监听ANR事件:注册监听器,当ANR发生时收集数据。 4. 记录和上报事件:使用StatsLog.EventMetricData打包数据并上报。 5. 调试方法:使用adb触发ANR,检查日志和上报结果。 需要确保每个步骤的代码示例正确,并解释关键部分的作用。同时,提供验证方法,帮助用户确认功能是否正常工作。</think>针对Android应用ANR增量特性的代码实现及验证,需结合事件统计API与配置管理进行系统化处理。以下分步骤说明实现方案: ### 一、增量特性检测与配置 1. **检测设备支持性** 使用`Utils.hasIncrementalFeature()`判断设备是否支持增量上报: ```java if (Utils.hasIncrementalFeature(context)) { // 执行增量特性相关操作 } ``` 2. **动态配置上报参数** 通过`ConfigUtils.uploadConfigForPushedAtomWithUid()`设置ANR事件上报策略: ```java ConfigUtils.uploadConfigForPushedAtomWithUid( context, StatsLog.ANR_OCCURRED, // 事件类型常量 uid, // 应用UID new ConfigUtils.UploadConfig() .setSamplingRate(1.0f) // 采样率 .setMaxUploadDelay(5000) // 最大延迟 ); ``` ### 二、ANR事件监控实现 1. **注册ANR监听器** 通过`ActivityManager`监听ANR事件: ```java ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); am.addOnANRListener((packageName, pid, processName, reason) -> { handleAnrEvent(packageName, reason); }); ``` 2. **事件数据记录** 使用`StatsLog.EventMetricData`构建事件数据: ```java EventMetricData data = StatsLog.newEventMetricData() .setAtomId(StatsLog.ANR_OCCURRED) .setElapsedTimestampNanos(SystemClock.elapsedRealtimeNanos()) .putExtraValues( // 附加信息 StatsLog.createExtraValueInt(processId), StatsLog.createExtraValueString(errorType) ); ``` ### 三、增量上报与验证 1. **事件上报触发** 通过`StatsLog.write()`提交数据: ```java StatsLog.write(data.build()); ``` 2. **调试验证方法** - **触发模拟ANR** ```shell adb shell "kill -3 `pidof com.example.app`" # 发送SIGQUIT信号 ``` - **查看事件日志** ```shell adb logcat | grep 'ANR_OCCURRED' ``` - **验证增量上传** 通过`dumpsys stats --metadata`检查配置状态[^1] ### 四、关键API说明表 | API名称 | 作用说明 | 参数示例 | |-----------------------------------|----------------------------------|-----------------------------------| | Utils.hasIncrementalFeature() | 检测增量特性支持 | Context对象 | | ConfigUtils.uploadConfigFor...() | 配置事件上报策略 | UID、采样率、延迟阈值 | | StatsLog.EventMetricData | 构建符合StatsD协议的事件数据结构 | 时间戳、原子事件ID、附加字段 |
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值