ANR 觸發(fā)、監(jiān)控、分析 一網(wǎng)打盡

平時看博客或者學知識,學到的東西比較零散,沒有獨立的知識模塊概念,而且學了之后很容易忘。于是我建立了一個自己的筆記倉庫 (一個我長期維護的筆記倉庫,感興趣的可以點個star~你的star是我寫作的巨大大大大的動力),將平時學到的東西都歸類然后放里面,需要的時候呢也方便復習。

僅做學習和記錄,方案非原創(chuàng)。

1. ANR是什么

ANR全稱是Applicatipon No Response,Android設計ANR的用意,是系統(tǒng)通過與之交互的組件以及用戶交互進行超時監(jiān)控,用來判斷應用進程是否存在卡死或響應過慢的問題,通俗來說就是很多系統(tǒng)中看門狗(watchdog)的設計思想。

2. 導致ANR的原因

耗時操作導致ANR,并不一定是app的問題,實際上,有很大的概率是系統(tǒng)原因?qū)е碌腁NR。下面簡單分析一下哪些操作是應用層導致的ANR,哪些是系統(tǒng)導致的ANR。

應用層導致ANR:

  • 函數(shù)阻塞:如死循環(huán)、主線程IO、處理大數(shù)據(jù)
  • 鎖出錯:主線程等待子線程的鎖
  • 內(nèi)存緊張:系統(tǒng)分配給一個應用的內(nèi)存是有上限的,長期處于內(nèi)存緊張,會導致頻繁內(nèi)存交換,進而導致應用的一些操作超時

系統(tǒng)導致ANR:

  • CPU被搶占:一般來說,前臺在玩游戲,可能會導致你的后臺廣播被搶占
  • 系統(tǒng)服務無法及時響應:比如獲取系統(tǒng)聯(lián)系人等,系統(tǒng)的服務都是Binder機制,服務能力也是有限的,有可能系統(tǒng)服務長時間不響應導致ANR
  • 其他應用占用大量內(nèi)存

3. 線下拿到ANR日志

  • adb pull /data/anr/
  • adb bugreport

缺陷:

  • 只能線下,用戶反饋時,無法獲取ANR日志
  • 可能沒有堆棧信息

4. ANR場景

  • Service Timeout:比如前臺服務在20s內(nèi)未執(zhí)行完成,后臺服務Timeout時間是前臺服務的10倍,200s;
  • BroadcastQueue Timeout:比如前臺廣播在10s內(nèi)未執(zhí)行完成,后臺60s
  • ContentProvider Timeout:內(nèi)容提供者,在publish過超時10s;
  • InputDispatching Timeout: 輸入事件分發(fā)超時5s,包括按鍵和觸摸事件。
//ActiveServices.java
// How long we wait for a service to finish executing.
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
// How long the startForegroundService() grace period is to get around to
// calling startForeground() before we ANR + stop it.
static final int SERVICE_START_FOREGROUND_TIMEOUT = 10*1000;

//ActivityManagerService.java
// 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;

5. ANR觸發(fā)流程

ANR觸發(fā)流程大致可分為2種,一種是Service、Broadcast、Provider觸發(fā)ANR,另外一種是Input觸發(fā)ANR。

5.1 Service、Broadcast、Provider觸發(fā)ANR

大體流程可分為3個步驟:

  1. 埋定時炸彈
  2. 拆炸彈
  3. 引爆炸彈

下面舉個startService的例子,詳細說說這3個步驟:

1.埋定時炸彈

在Activity中調(diào)用startService后,調(diào)用鏈:ContextImpl.startService()->ContextImpl.startServiceCommon()->ActivityManagerService.startService()->ActiveServices.startServiceLocked()->ActiveServices.startServiceInnerLocked()->ActiveServices.bringUpServiceLocked()->ActiveServices.realStartServiceLocked()

//com.android.server.am.ActiveServices.java
private final void realStartServiceLocked(ServiceRecord r,
        ProcessRecord app, boolean execInFg) throws RemoteException {
    ......
    //發(fā)個延遲消息給AMS的Handler
    bumpServiceExecutingLocked(r, execInFg, "create");

    ......
    try {
        //IPC通知app進程啟動Service,執(zhí)行handleCreateService
        app.thread.scheduleCreateService(r, r.serviceInfo,
                mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
                app.getReportedProcState());
    } catch (DeadObjectException e) {
    } finally {
    }
}

private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    scheduleServiceTimeoutLocked(r.app);
    .....
}

final ActivityManagerService mAm;

// How long we wait for a service to finish executing.
static final int SERVICE_TIMEOUT = 20*1000;

// How long we wait for a service to finish executing.
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;

void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    //mAm是AMS,mHandler是AMS里面的一個Handler
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    //發(fā)個延遲消息給AMS里面的一個Handler
    mAm.mHandler.sendMessageDelayed(msg,
            proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}

在startService流程中,在通知app進程啟動Service之前,會進行預埋一個炸彈,也就是延遲發(fā)送一個消息給AMS的mHandler。當AMS的這個Handler收到SERVICE_TIMEOUT_MSG這個消息時,就認為Service超時了,觸發(fā)ANR。也就是說,特定時間內(nèi),沒人來拆這個炸彈,這個炸彈就會爆炸。

2. 拆炸彈

在AMS校驗通過后,app這邊可以啟動Service,于是來到了ApplicationThread的scheduleCreateService方法,該方法是運行在binder線程里面的,所以得切到主線程去執(zhí)行,也就是ActivityThread的handleCreateService方法:

//android.app.ActivityThread.java
@UnsupportedAppUsage
private void handleCreateService(CreateServiceData data) {
    ......
    Service service = null;
    try {
        //1. 初始化Service
        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);
        ......
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());
        //2. Service執(zhí)行onCreate,啟動完成
        service.onCreate();
        mServices.put(data.token, service);
        try {
            //3. Service啟動完成,需要通知AMS
            ActivityManager.getService().serviceDoneExecuting(
                    data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
        } catch (RemoteException e) {
        }
    } catch (Exception e) {
    }
}

在app進程這邊啟動完Service之后,需要IPC通信告知AMS我這邊已經(jīng)啟動完成了。AMS.serviceDoneExecuting()->ActiveServices.serviceDoneExecutingLocked()

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
        boolean finishing) {
    ......
    mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
    ......
}

很清晰,就是把之前延遲發(fā)送的SERVICE_TIMEOUT_MSG消息給移除掉,也就是拆炸彈。只要在規(guī)定的時間內(nèi)把炸彈拆了,那就沒事,要是沒拆,炸彈就要爆炸,觸發(fā)ANR。

3. 引爆炸彈

之前延遲給AMS的handler發(fā)送了一個消息,mAm.mHandler.sendMessageDelayed(msg,proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);,下面我們來看一下這條消息的邏輯

//com.android.server.am.ActivityManagerService.java

final MainHandler mHandler;

final class MainHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        ......
        case SERVICE_TIMEOUT_MSG: {
            //這個mServices是ActiveServices
            mServices.serviceTimeout((ProcessRecord)msg.obj);
        } break;
        }
        ......
    }
    ......
}

//com.android.server.am.ActiveServices.java
void serviceTimeout(ProcessRecord proc) {
    String anrMessage = null;
    synchronized(mAm) {
        //計算是否有service超時
        final long now = SystemClock.uptimeMillis();
        final long maxTime =  now -
                (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
        ServiceRecord timeout = null;
        for (int i=proc.executingServices.size()-1; i>=0; i--) {
            ServiceRecord sr = proc.executingServices.valueAt(i);
            if (sr.executingStart < maxTime) {
                timeout = sr;
                break;
            }
        }
        if (timeout != null && mAm.mProcessList.mLruProcesses.contains(proc)) {
            anrMessage = "executing service " + timeout.shortInstanceName;
        }
    }

    if (anrMessage != null) {
        //有超時的Service,mAm是AMS,mAnrHelper是AnrHelper
        mAm.mAnrHelper.appNotResponding(proc, anrMessage);
    }
}

AMS這邊如果收到了SERVICE_TIMEOUT_MSG消息,也就是超時了,沒人來拆炸彈,那么它會讓ActiveServices確認一下是否有Service超時,有的話,再利用AnrHelper來觸發(fā)ANR。

void appNotResponding(ProcessRecord anrProcess, String activityShortComponentName,
        ApplicationInfo aInfo, String parentShortComponentName,
        WindowProcessController parentProcess, boolean aboveSystem, String annotation) {
    //添加AnrRecord到List里面
    synchronized (mAnrRecords) {
        mAnrRecords.add(new AnrRecord(anrProcess, activityShortComponentName, aInfo,
                parentShortComponentName, parentProcess, aboveSystem, annotation));
    }
    startAnrConsumerIfNeeded();
}
private void startAnrConsumerIfNeeded() {
    if (mRunning.compareAndSet(false, true)) {
        //開個子線程來處理
        new AnrConsumerThread().start();
    }
}

private class AnrConsumerThread extends Thread {
    @Override
    public void run() {
        AnrRecord r;
        while ((r = next()) != null) {
            ......
            //這里的r就是AnrRecord
            r.appNotResponding(onlyDumpSelf);
            ......
        }
    }
}
private static class AnrRecord {
    void appNotResponding(boolean onlyDumpSelf) {
        //mApp是ProcessRecord
        mApp.appNotResponding(mActivityShortComponentName, mAppInfo,
                mParentShortComponentName, mParentProcess, mAboveSystem, mAnnotation,
                onlyDumpSelf);
    }
}

開了個子線程,然后調(diào)用ProcessRecord的appNotResponding方法來處理ANR的流程(彈出app無響應彈窗、dump堆棧什么的),具體流程下面會細說。到這里,炸彈就完全引爆了,觸發(fā)了ANR。

5.2 Input觸發(fā)ANR

input的超時檢測機制跟Service、Broadcast、Provider截然不同,并非時間到了就一定被爆炸,而是處理后續(xù)上報事件的過程才會去檢測是否該爆炸,所以更像是掃雷的過程。

input超時機制為什么是掃雷,而非定時爆炸?由于對于input來說即便某次事件執(zhí)行時間超過Timeout時長,只要用戶后續(xù)沒有再生成輸入事件,則不會觸發(fā)ANR。這里的掃雷是指當前輸入系統(tǒng)中正在處理著某個耗時事件的前提下,后續(xù)的每一次input事件都會檢測前一個正在處理的事件是否超時(進入掃雷狀態(tài)),檢測當前的時間距離上次輸入事件分發(fā)時間點是否超過timeout時長。如果沒有超過,則會重置anr的Timeout,從而不會爆炸。

5.3 哪些路徑會引發(fā)ANR?

從埋下炸彈到拆炸彈之間的任何一個或多個路徑執(zhí)行慢都會導致ANR。這里以Service為例,如:

  • Service的生命周期的回調(diào)方法執(zhí)行慢
  • 主線程的消息隊列存在其他耗時消息讓Service回調(diào)方法遲遲得不到執(zhí)行
  • sp操作執(zhí)行慢
  • system_server進程的binder線程繁忙而導致沒有及時收到拆炸彈的指令

5.4 ANR dump主要流程

ANR流程基本是在system_server系統(tǒng)進程完成的,系統(tǒng)進程的行為我們很難監(jiān)控到,想要監(jiān)控這個事情就得從系統(tǒng)進程與應用進程溝通的邊界著手,看邊界上有沒有可以操作的地方。

不管是怎么發(fā)生的ANR,最后都會走到appNotResponding ,比如輸入超時的路徑

  1. ActivityManagerService#inputDispatchingTimedOut
  2. AnrHelper#appNotResponding
  3. AnrConsumerThread#run
  4. AnrRecord#appNotResponding
  5. ProcessRecord#appNotResponding

那我們直接分析這個appNotResponding 方法:

//com.android.server.am.ProcessRecord.java
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
        String parentShortComponentName, WindowProcessController parentProcess,
        boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
    ArrayList<Integer> firstPids = new ArrayList<>(5);
    SparseArray<Boolean> lastPids = new SparseArray<>(20);

    mWindowProcessController.appEarlyNotResponding(annotation, () -> kill("anr",
                ApplicationExitInfo.REASON_ANR, true));

    long anrTime = SystemClock.uptimeMillis();
    if (isMonitorCpuUsage()) {
        mService.updateCpuStatsNow();
    }

    final boolean isSilentAnr;
    synchronized (mService) {
        //注釋1
        // PowerManager.reboot() can block for a long time, so ignore ANRs while shutting down.
        //正在重啟
        if (mService.mAtmInternal.isShuttingDown()) {
            Slog.i(TAG, "During shutdown skipping ANR: " + this + " " + annotation);
            return;
        } else if (isNotResponding()) {
            //已經(jīng)處于ANR流程中
            Slog.i(TAG, "Skipping duplicate ANR: " + this + " " + annotation);
            return;
        } else if (isCrashing()) {
            //正在crash的狀態(tài)
            Slog.i(TAG, "Crashing app skipping ANR: " + this + " " + annotation);
            return;
        } else if (killedByAm) {
            //app已經(jīng)被killed
            Slog.i(TAG, "App already killed by AM skipping ANR: " + this + " " + annotation);
            return;
        } else if (killed) {
            //app已經(jīng)死亡了
            Slog.i(TAG, "Skipping died app ANR: " + this + " " + annotation);
            return;
        }

        // In case we come through here for the same app before completing
        // this one, mark as anring now so we will bail out.
        //做個標記
        setNotResponding(true);

        // Log the ANR to the event log.
        EventLog.writeEvent(EventLogTags.AM_ANR, userId, pid, processName, info.flags,
                annotation);

        // Dump thread traces as quickly as we can, starting with "interesting" processes.
        firstPids.add(pid);

        // Don't dump other PIDs if it's a background ANR or is requested to only dump self.
        //注釋2
        //沉默的anr : 這里表示后臺anr
        isSilentAnr = isSilentAnr();
        if (!isSilentAnr && !onlyDumpSelf) {
            int parentPid = pid;
            if (parentProcess != null && parentProcess.getPid() > 0) {
                parentPid = parentProcess.getPid();
            }
            if (parentPid != pid) firstPids.add(parentPid);

            if (MY_PID != pid && MY_PID != parentPid) firstPids.add(MY_PID);
                        
            //選擇需要dump的進程
            for (int i = getLruProcessList().size() - 1; i >= 0; i--) {
                ProcessRecord r = getLruProcessList().get(i);
                if (r != null && r.thread != null) {
                    int myPid = r.pid;
                    if (myPid > 0 && myPid != pid && myPid != parentPid && myPid != MY_PID) {
                        if (r.isPersistent()) {
                            firstPids.add(myPid);
                            if (DEBUG_ANR) Slog.i(TAG, "Adding persistent proc: " + r);
                        } else if (r.treatLikeActivity) {
                            firstPids.add(myPid);
                            if (DEBUG_ANR) Slog.i(TAG, "Adding likely IME: " + r);
                        } else {
                            lastPids.put(myPid, Boolean.TRUE);
                            if (DEBUG_ANR) Slog.i(TAG, "Adding ANR proc: " + r);
                        }
                    }
                }
            }
        }
    }

    ......

    int[] pids = nativeProcs == null ? null : Process.getPidsForCommands(nativeProcs);
    ArrayList<Integer> nativePids = null;

    if (pids != null) {
        nativePids = new ArrayList<>(pids.length);
        for (int i : pids) {
            nativePids.add(i);
        }
    }

    // For background ANRs, don't pass the ProcessCpuTracker to
    // avoid spending 1/2 second collecting stats to rank lastPids.
    StringWriter tracesFileException = new StringWriter();
    // To hold the start and end offset to the ANR trace file respectively.
    final long[] offsets = new long[2];
    //注釋4
    File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,
            isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids,
            nativePids, tracesFileException, offsets);
        ......
}

代碼比較長,我們一步一步來看。

注釋1處首先是針對幾種特殊情況:正在重啟、已經(jīng)處于ANR流程中、正在crash、app已經(jīng)被killed和app已經(jīng)死亡了,不用處理ANR,直接return。

注釋2處isSilentAnr是表示當前是否為一個后臺ANR,后臺ANR跟前臺ANR表現(xiàn)不同,前臺ANR會彈出無響應的Dialog,后臺ANR會直接殺死進程。什么是前臺ANR:發(fā)生ANR的進程對用戶來說有感知,就是前臺ANR,否則就是后臺ANR。

注釋3處,選擇需要dump的進程。發(fā)生ANR時,為了方便定位問題,會dump很多信息到Trace文件中。而Trace文件里包含著與ANR相關聯(lián)的進程的Trace信息,因為產(chǎn)生ANR的原因有可能是其他的進程搶占了太多資源,或者IPC到其他進程的時候卡住導致的。需要被dump的進程分為3類:

  • firstPids:firstPids是需要首先dump的重要進程,發(fā)生ANR的進程無論如何是一定要被dump的,也是首先被dump的,所以第一個被加到firstPids中。如果是SilentAnr(即后臺ANR),不用再加入任何其他的進程。如果不是,需要進一步添加其他的進程:如果發(fā)生ANR的進程不是system_server進程的話,需要添加system_server進程;接下來輪詢AMS維護的一個LRU的進程List,如果最近訪問的進程包含了persistent的進程,或者帶有 *BIND_TREAT_LIKE_ACTVITY* 標簽的進程,都添加到firstPids中。
  • extraPids:LRU進程List中的其他進程,都會首先添加到lastPids中,然后lastPids會進一步被選出最近CPU使用率高的進程,進一步組成extraPids;
  • nativePids:nativePids最為簡單,是一些固定的native的系統(tǒng)進程,定義在WatchDog.java中

注釋4處,拿到需要dump的所有進程的pid后,AMS開始按照firstPids、nativePids、extraPids的順序dump這些進程的堆棧。這里比較重要,我們需要跟進去看看具體做了什么。

public static Pair<Long, Long> dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids,
        ArrayList<Integer> nativePids, ArrayList<Integer> extraPids) {

    // 最多dump 20秒
    long remainingTime = 20 * 1000;

    // First collect all of the stacks of the most important pids.
    if (firstPids != null) {
        int num = firstPids.size();
        for (int i = 0; i < num; i++) {
            final int pid = firstPids.get(i);
            final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime);
            remainingTime -= timeTaken;
            if (remainingTime <= 0) {
                Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid
                        + "); deadline exceeded.");
                return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
            }
        }
    }
    ......
}

就是根據(jù)順序取出前面?zhèn)魅氲膄irstPids、nativePids 、extraPids 的pid,然后逐一去dump這些進程中所有的線程,當然這是一個非常重的操作,一個進程就有那么多線程,更別說這么多進程了。所以,這里規(guī)定了個最長dump時間為20秒,超過則及時返回,這樣可以確保ANR彈窗可以及時彈出(或者被kill掉)。接下來我們接著跟進dumpJavaTracesTombstoned。經(jīng)過一連串的邏輯:ActivityManagerService#dumpJavaTracesTombstoned() → Debug#dumpJavaBacktraceToFileTimeout() → android_os_Debug#android_os_Debug_dumpJavaBacktraceToFileTimeout() → android_os_Debug#dumpTraces() → debuggerd_client#dump_backtrace_to_file_timeout() → debuggerd_client#debuggerd_trigger_dump()。

bool debuggerd_trigger_dump(pid_t tid, DebuggerdDumpType dump_type, unsigned int timeout_ms, unique_fd output_fd) {
    //pid是從AMS那邊傳過來的,即需要dump堆棧的進程
        pid_t pid = tid;
    //......

    // Send the signal.
        //從android_os_Debug_dumpJavaBacktraceToFileTimeout過來的,dump_type為kDebuggerdJavaBacktrace
    const int signal = (dump_type == kDebuggerdJavaBacktrace) ? SIGQUIT : BIONIC_SIGNAL_DEBUGGER;
    sigval val = {.sival_int = (dump_type == kDebuggerdNativeBacktrace) ? 1 : 0};
        //sigqueue:在隊列中向指定進程發(fā)送一個信號和數(shù)據(jù),成功返回0
    if (sigqueue(pid, signal, val) != 0) {
      log_error(output_fd, errno, "failed to send signal to pid %d", pid);
      return false;
    }
    //......
    LOG(INFO) << TAG "done dumping process " << pid;
    return true;
}

注意,這里相當于是AMS進程間接給需要dump堆棧那個進程發(fā)送了一個SIGQUIT信號,那個進程收到SIGQUIT信號之后便開始dump。這里也就是前面所說的邊界。現(xiàn)在看起來是當一個進程發(fā)生ANR時,則會收到SIGQUIT信號。如果,我們能監(jiān)控到系統(tǒng)發(fā)送的SIGQUIT信號,也許就能感知到發(fā)生了ANR,達到監(jiān)控的目的。

關于進程信號的處理,這里簡單提一下:除Zygote進程外,每個進程都會創(chuàng)建一個SignalCatcher守護線程,用于捕獲SIGQUIT、SIGUSR1信號,并采取相應的行為。

//art/runtime/signal_catcher.cc
void* SignalCatcher::Run(void* arg) {
  SignalCatcher* signal_catcher = reinterpret_cast<SignalCatcher*>(arg);
  CHECK(signal_catcher != nullptr);
  Runtime* runtime = Runtime::Current();
  //檢查當前線程是否依附到Android Runtime
  CHECK(runtime->AttachCurrentThread("Signal Catcher", true, runtime->GetSystemThreadGroup(), !runtime->IsAotCompiler()));

  Thread* self = Thread::Current();
  DCHECK_NE(self->GetState(), kRunnable);
  {
    MutexLock mu(self, signal_catcher->lock_);
    signal_catcher->thread_ = self;
    signal_catcher->cond_.Broadcast(self);
  }

  SignalSet signals;
  signals.Add(SIGQUIT); //添加對信號SIGQUIT的處理
  signals.Add(SIGUSR1); //添加對信號SIGUSR1的處理
    
    //死循環(huán),不斷等待監(jiān)聽2個信號的dao'l
  while (true) {
    //等待信號到來,這是個阻塞操作
    int signal_number = signal_catcher->WaitForSignal(self, signals);
    //當信號捕獲需要停止時,則取消當前線程跟Android Runtime的關聯(lián)。
    if (signal_catcher->ShouldHalt()) {
      runtime->DetachCurrentThread();
      return nullptr;
    }
    switch (signal_number) {
    case SIGQUIT:
      signal_catcher->HandleSigQuit(); //輸出線程trace
      break;
    case SIGUSR1:
      signal_catcher->HandleSigUsr1(); //強制GC
      break;
    default:
      LOG(ERROR) << "Unexpected signal %d" << signal_number;
      break;
    }
  }
}

在SignalCatcher線程里面,死循環(huán),通過WaitForSignal監(jiān)聽SIGQUIT和SIGUSR1信號的到來,前面系統(tǒng)進程system_server進程發(fā)送的SIGQUIT信號也就是在這里被監(jiān)聽到,然后開始dump堆棧。

現(xiàn)在,我們整理一下整個ANR的流程:

  1. 系統(tǒng)監(jiān)控到app發(fā)生ANR后,收集了一些相關進程pid(包括發(fā)生ANR的進程),準備讓這些進程dump堆棧,從而生成ANR Trace文件
  2. 系統(tǒng)開始向這些進程發(fā)送SIGQUIT信號,進程收到SIGQUIT信號之后開始dump堆棧

整個過程的示意圖:

ANR流程示意圖

圖片轉(zhuǎn)自微信客戶端技術團隊

可以看到,一個進程發(fā)生ANR之后的整個流程,只有dump堆棧的行為會發(fā)生在發(fā)生ANR的進程中,其他過程全在系統(tǒng)進程進行處理的,我們無法感知。這個過程從收到SIGQUIT信號開始到使用socket寫Trace結(jié)束。然后繼續(xù)回到系統(tǒng)進程完成剩余的ANR流程,這2個邊界上我們可以做做文章。后面我們會詳細敘述。

6. ANR監(jiān)控

Android M(6.0) 版本之后,應用側(cè)無法直接通過監(jiān)聽 data/anr/trace 文件,監(jiān)控是否發(fā)生 ANR。目前了解到的能用的方案主要有下面2種:

6.1 WatchDog

開個子線程,不斷往主線程發(fā)送消息,并設置超時檢測,如果超時還沒執(zhí)行相應消息,則判定為可能發(fā)生ANR。需要進一步從系統(tǒng)服務獲取相關數(shù)據(jù)(可通過ActivityManagerService.getProcessesInErrorState()方法獲取進程的ANR信息),進一步判定是否真的發(fā)生了ANR。

這個方案對應的開源庫為ANR-WatchDog,源碼比較簡單,只有2個源文件。簡單解析一下核心代碼:


private final Handler _uiHandler = new Handler(Looper.getMainLooper());
private final int _timeoutInterval;
private volatile long _tick = 0;
private volatile boolean _reported = false;

private final Runnable _ticker = new Runnable() {
    @Override public void run() {
        _tick = 0;
        _reported = false;
    }
};

@Override
public void run() {
    setName("|ANR-WatchDog|");

    //_timeoutInterval為設定的超時時長
    long interval = _timeoutInterval;
    while (!isInterrupted()) {
        //_tick為標志,主線程執(zhí)行了下面發(fā)送的_ticker這個Runnable, 那么_tick就會被置為0
        boolean needPost = _tick == 0;
        //在子線程里面需要把標志改為非0,待會兒主線程執(zhí)行了才知道
        _tick += interval;
        if (needPost) {
            //發(fā)個消息給主線程
            _uiHandler.post(_ticker);
        }

        //子線程睡一段時間,起來的時候要是標志位_tick沒有被改成0,說明主線程太忙了,或者卡頓了,沒來得及執(zhí)行該消息
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e) {
            _interruptionListener.onInterrupted(e);
            return ;
        }

        // If the main thread has not handled _ticker, it is blocked. ANR.
        if (_tick != 0 && !_reported) {
            //noinspection ConstantConditions
            //排除debug的情況
            if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
                Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
                _reported = true;
                continue ;
            }

            //可以自定義一個Interceptor告訴watchDog,當前上下文環(huán)境是否可以進行上報
            interval = _anrInterceptor.intercept(_tick);
            if (interval > 0) {
                continue;
            }

            //上報線程堆棧
            final ANRError error;
            if (_namePrefix != null) {
                error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
            } else {
                error = ANRError.NewMainOnly(_tick);
            }
            //回調(diào)
            _anrListener.onAppNotResponding(error);
            interval = _timeoutInterval;
            _reported = true;
        }
    }
}

核心代碼非常簡潔,基本上就是上面方案的實現(xiàn)了。有一點需要補充的是,需要進一步從系統(tǒng)服務獲取相關數(shù)據(jù)(可通過ActivityManagerService.getProcessesInErrorState()方法獲取進程的ANR信息,具體實現(xiàn)方式下面會詳細說明),進一步判定是否真的發(fā)生了ANR??梢宰远x一個_anrInterceptor,在里面實現(xiàn)這些內(nèi)容。

6.2 監(jiān)控SIGQUIT信號

這種方案才是真正的監(jiān)控ANR,matrix、xCrash都在使用這種方案。已經(jīng)在國民應用微信等app上檢驗過,穩(wěn)定性和可靠性都能得到保證。

在文章上面的ANR流程分析中,我們找到了系統(tǒng)與發(fā)生ANR進程之間的邊界(即下圖中的1和2)。我們能否監(jiān)聽到系統(tǒng)發(fā)送給我們的SIGQUIT信號呢?答案當然是可行的。

ANR流程示意圖

這里需要一點預備知識,首先我們得知道什么是SIGQUIT信號,前面我們提到了除Zygote進程以外的其他進程都有個Signal Catcher線程在不斷地通過sigwait監(jiān)聽SIGQUIT信號,當收到SIGQUIT信號時開始dump線程堆棧。我們需要攔截或者監(jiān)聽SIGQUIT信號,首先需要了解信號處理的相關函數(shù),如kill、signal、sigaction、sigwait、pthread_sigmask等,本文就不詳細展開這些函數(shù)的具體使用了,如需詳細了解,推薦閱讀《UNIX環(huán)境高級編程》。

下面是我寫的監(jiān)控SIGQUIT信號demo的核心代碼,完整源碼在這里:

void signalHandler(int sig, siginfo_t *info, void *uc) {
    __android_log_print(ANDROID_LOG_DEBUG, "xfhy_anr", "我監(jiān)聽到SIGQUIT信號了,可能發(fā)生anr了");

    //在這里去dump主線程堆棧
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_xfhy_watchsignaldemo_MainActivity_startWatch(JNIEnv *env, jobject thiz) {
    sigset_t set, old_set;
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
        
    /*
     * 這里需要調(diào)用SIG_UNBLOCK,因為目標進程被Zogyte fork出來的時候,主線程繼承了
     * Zogyte的主線程的信號屏蔽關系,Zogyte主線程在初始化的時候,通過
     * pthread_sigmask SIG_BLOCK把SIGQUIT的信號給屏蔽了,因此我們需要在自己進程的主線程,
     * 設置pthread_sigmask SIG_UNBLOCK ,這會導致原來的SignalCatcher sigwait將失效,
     * 原因是SignalCatcher 線程會對SIGQUIT 信號處理
     */
    int r = pthread_sigmask(SIG_UNBLOCK, &set, &old_set);
    if (0 != r) {
        return false;
    }

    struct sigaction sa{};
    sa.sa_sigaction = signalHandler;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;

    return sigaction(SIGQUIT, &sa, nullptr) == 0;
}

Android默認把SIGQUIT設置成了BLOCKED,所以只會響應Signal Catcher線程的sigwait監(jiān)聽SIGQUIT信號,我們用sigaction監(jiān)聽的則收不到,所以這里還需要處理一下。我們通過pthread_sigmask或者sigprocmask把SIGQUIT設置為UNBLOCK,那么再次收到SIGQUIT時,就一定會進入到我們的signalHandler方法中。

除了上面這個之外,還需要注意的是:我們用sigaction搶了Signal Catcher線程的SIGQUIT信號,那Signal Catcher線程就收不到該信號了,那原本的系統(tǒng)dump堆棧的流程就沒了,這是不太合適的。所以我們需要將該信號重新發(fā)送出去,讓Signal Catcher線程接收到該信號。

int tid = getSignalCatcherThreadId(); //遍歷/proc/[pid]目錄,找到SignalCatcher線程的tid
tgkill(getpid(), tid, SIGQUIT);

以上,咱們得到了一個不改變系統(tǒng)行為的前提下,比較完善的監(jiān)控SIGQUIT信號的機制,雖然不是特別完美,但這是監(jiān)控ANR的基礎。接下來我們慢慢完善。

6.2.1 完善的ANR監(jiān)控方案

監(jiān)控到SIGQUIT信號并不等于就監(jiān)控到了ANR。

6.2.1.1 誤報

發(fā)生ANR的進程一定會收到SIGQUIT信號;但是收到SIGQUIT信號的進程并不一定發(fā)生了ANR。

可能是下面2種情況:

  1. 其他進程的ANR:發(fā)生ANR之后,發(fā)生ANR的進程并不是唯一需要dump堆棧的進程,系統(tǒng)會收集許多其他的進程進行dump,也就是說當一個應用發(fā)生ANR的時候,其他的應用也有可能收到SIGQUIT信號。所以,我們收到SIGQUIT信號,可能是其他進程發(fā)生了ANR,這個時候上報的話就屬于是誤報了。
  2. 非ANR發(fā)送SIGQUIT:發(fā)送SIGQUIT信號非常容易,系統(tǒng)和應用級app都能輕易發(fā)送SIGQUIT信號:java層調(diào)用android.os.Process.sendSignal方法;Native層調(diào)用kill或者tgkill方法。我們收到SIGQUIT信號時,可能并非是ANR流程發(fā)送的SIGQUIT信號,也會產(chǎn)生誤報。

如何解決上面2個誤報的問題?回到ANR流程開始的地方細看

//com.android.server.am.ProcessRecord.java
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
        String parentShortComponentName, WindowProcessController parentProcess,
        boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
    //......
    synchronized (mService) {
        //注意,如果是后臺ANR,直接就kill進程然后return了,并不會走到下面的makeAppNotRespondingLocked,當前進程也不會有NOT_RESPONDING這個flag
        if (isSilentAnr() && !isDebugging()) {
            kill("bg anr", ApplicationExitInfo.REASON_ANR, true);
            return;
        }

        // Set the app's notResponding state, and look up the errorReportReceiver
        makeAppNotRespondingLocked(activityShortComponentName,
                annotation != null ? "ANR " + annotation : "ANR", info.toString());

        // show ANR dialog ......
    }
}

private void makeAppNotRespondingLocked(String activity, String shortMsg, String longMsg) {
    setNotResponding(true);
    // mAppErrors can be null if the AMS is constructed with injector only. This will only
    // happen in tests.
    if (mService.mAppErrors != null) {
        notRespondingReport = mService.mAppErrors.generateProcessError(this,
                ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING,
                activity, shortMsg, longMsg, null);
    }
    startAppProblemLocked();
    getWindowProcessController().stopFreezingActivities();
}

void setNotResponding(boolean notResponding) {
    mNotResponding = notResponding;
    mWindowProcessController.setNotResponding(notResponding);
}

在ANR彈窗前,會執(zhí)行makeAppNotRespondingLocked方法,在這里會給發(fā)生ANR的進程標記一個NOT_RESPONDING的flag,這個flag可以通過ActivityManager來獲?。?/p>

private static boolean checkErrorState() {
    try {
        Application application = sApplication == null ? Matrix.with().getApplication() : sApplication;
        ActivityManager am = (ActivityManager) application.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
        if (procs == null) return false;
        for (ActivityManager.ProcessErrorStateInfo proc : procs) {
            if (proc.pid != android.os.Process.myPid()) continue;
            if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) continue;
            return true;
        }
        return false;
    } catch (Throwable t){
        MatrixLog.e(TAG,"[checkErrorState] error : %s", t.getMessage());
    }
    return false;
}

監(jiān)控到SIGQUIT后,我們在20秒內(nèi)(20秒是ANR dump的timeout時間)不斷輪詢自己是否有NOT_RESPONDING的flag,一旦發(fā)現(xiàn)有這個flag,那么馬上就可以認定發(fā)生了一次ANR。

ps: 你可能會想,有這么方便的方法,監(jiān)控SIGQUIT信號不是多余么?我直接搞個死循環(huán),不斷監(jiān)聽該flag,一旦發(fā)現(xiàn)不就監(jiān)控到ANR了么?可以是可以,但不優(yōu)雅,而且有缺陷(低效、耗電、不環(huán)保、無法解決下面提到的漏報問題)。

6.2.1.2 漏報

進程處于NOT_RESPONDING的狀態(tài)可以確認該進程發(fā)生了ANR。但是發(fā)生ANR的進程并不一定會被設置為NOT_RESPONDING狀態(tài)

下面2種是特殊情況:

  1. 后臺ANR(SilentAnr):如果ANR被標記為了后臺ANR(即SilentAnr),那么殺死進程后就會直接return,不會執(zhí)行到makeAppNotRespondingLocked,那么該進程就不會有NOT_RESPONDING這個flag。這意味著,后臺的ANR沒辦法捕捉到,但后臺ANR的量也挺大的,并且后臺ANR會直接殺死進程,對用戶的體驗也是非常負面的,這么大一部分ANR監(jiān)控不到,當然是無法接受的。
  2. 閃退ANR:想當一部分機型(如OPPO、VIVO兩家的高Android版本的機型)修改了ANR的流程,即使是發(fā)生在前臺的ANR,也并不會彈窗,而是直接殺死進程,即閃退。

基于上面2種情況,我們需要一種機制,在收到SIGQUIT信號后,需要非??焖俚膫刹槌鲎约菏欠褚呀?jīng)處于ANR的狀態(tài),進行快速的dump和上報。此時我們可以通過主線程釋放處于卡頓狀態(tài)來判斷,怎么快速的知道主線程是否卡住了?可以通過Looper的mMessage對象,該對象的when變量,表示的是當前正在處理的消息入隊的時間,我們可以通過when變量減去當前時間,得到的就是等待時間,如果等待時間過長,就說明主線程是處于卡住的狀態(tài)。這時候收到SIGQUIT信號基本上就可以認為的確發(fā)生了一次ANR:

private static boolean isMainThreadStuck(){
    try {
        MessageQueue mainQueue = Looper.getMainLooper().getQueue();
        Field field = mainQueue.getClass().getDeclaredField("mMessages");
        field.setAccessible(true);
        final Message mMessage = (Message) field.get(mainQueue);
        if (mMessage != null) {
            long when = mMessage.getWhen();
            if(when == 0) {
                return false;
            }
            long time = when - SystemClock.uptimeMillis();
            long timeThreshold = BACKGROUND_MSG_THRESHOLD;
            if (foreground) {
                timeThreshold = FOREGROUND_MSG_THRESHOLD;
            }
            return time < timeThreshold;
        }
    } catch (Exception e){
        return false;
    }
    return false;
}

通過上面幾種機制來綜合判斷收到SIGQUIT信號后,是否真的發(fā)生了一次ANR,最大程度地減少誤報和漏報。

6.2.1.3 獲取ANR Trace

回到上面的ANR流程示意圖,Signal Catcher線程寫Trace也是一個邊界,它是通過socket的write方法來寫trace的。那我們可以直接hook這里的write,就能直接拿到系統(tǒng)dump的ANR Trace內(nèi)容。這個內(nèi)容非常全面,包括了所有線程的各種狀態(tài)、鎖和堆棧(包括native堆棧),對于我們排查問題十分有用,尤其是一些native問題和死鎖等問題。native hook采用PLT Hook方案,穩(wěn)得很,這種方案已經(jīng)在微信上驗證了其穩(wěn)定性。

int (*original_connect)(int __fd, const struct sockaddr* __addr, socklen_t __addr_length);
int my_connect(int __fd, const struct sockaddr* __addr, socklen_t __addr_length) {
    if (strcmp(__addr->sa_data, "/dev/socket/tombstoned_java_trace") == 0) {
        isTraceWrite = true;
        signalCatcherTid = gettid();
    }
    return original_connect(__fd, __addr, __addr_length);
}

int (*original_open)(const char *pathname, int flags, mode_t mode);
int my_open(const char *pathname, int flags, mode_t mode) {
    if (strcmp(pathname, "/data/anr/traces.txt") == 0) {
        isTraceWrite = true;
        signalCatcherTid = gettid();
    }
    return original_open(pathname, flags, mode);
}

ssize_t (*original_write)(int fd, const void* const __pass_object_size0 buf, size_t count);
ssize_t my_write(int fd, const void* const buf, size_t count) {
    if(isTraceWrite && signalCatcherTid == gettid()) {
        isTraceWrite = false;
        signalCatcherTid = 0;
        char *content = (char *) buf;
        printAnrTrace(content);
    }
    return original_write(fd, buf, count);
}

void hookAnrTraceWrite() {
    int apiLevel = getApiLevel();
    if (apiLevel < 19) {
        return;
    }
    if (apiLevel >= 27) {
        plt_hook("libcutils.so", "connect", (void *) my_connect, (void **) (&original_connect));
    } else {
        plt_hook("libart.so", "open", (void *) my_open, (void **) (&original_open));
    }

    if (apiLevel >= 30 || apiLevel == 25 || apiLevel ==24) {
        plt_hook("libc.so", "write", (void *) my_write, (void **) (&original_write));
    } else if (apiLevel == 29) {
        plt_hook("libbase.so", "write", (void *) my_write, (void **) (&original_write));
    } else {
        plt_hook("libart.so", "write", (void *) my_write, (void **) (&original_write));
    }
}

有幾點需要注意:

  1. 只Hook ANR流程:有些情況下,基礎庫中的connect/open/write方法可能調(diào)用的比較頻繁,我們需要把hook的影響降到最低。所以我們只會在接收到SIGQUIT信號后(重新發(fā)送SIGQUIT信號給Signal Catcher前)進行hook,ANR流程結(jié)束后再unhook。
  2. 只處理Signal Catcher線程open/connect后的第一次write:除了Signal Catcher線程中的dump trace的流程,其他地方調(diào)用的write方法我們并不關心,并不需要處理。
  3. Hook點因API Level而不同:需要hook的write方法在不同的Android版本中,所在so庫也不同,需分別處理。

到此,matrix監(jiān)控SIGQUIT信號從而監(jiān)控ANR的方案的核心邏輯已全部呈現(xiàn),更多詳細源碼請移步matrix倉庫

總結(jié)一下,該方案通過去監(jiān)聽SIGQUIT信號,從而感知當前進程可能發(fā)生了ANR,需配合當前進程是否處于NOT_RESPONDING狀態(tài)以及主線程是否卡頓來進行甄別,以免誤判。注冊監(jiān)聽SIGQUIT信號之后,系統(tǒng)原來的Signal Catcher線程就監(jiān)聽不到這個信號了,需要把該信號轉(zhuǎn)發(fā)出去,讓它接收到,以免影響。當前進程的Signal Catcher線程要dump堆棧的時候,會通過socket的write向system server進程進行傳輸dump好的數(shù)據(jù),我們可以hook這個write,從而拿到系統(tǒng)dump好的ANR Trace內(nèi)容,相當于我們并沒有影響系統(tǒng)的任何流程,還拿到了想要拿到的東西。這個方案完全是在系統(tǒng)的正常dump anr trace的過程中獲取信息,所以能拿到的東西更加全面,但是系統(tǒng)的dump過程其實是對性能影響比較大的,時間也比較久。

7. ANR分析

監(jiān)控固然重要,更重要的是分析是什么原因?qū)е碌腁NR,然后修復好。

7.1 trace文件分析

拿到trace文件,詳細分析下:

----- pid 7761 at 2022-11-02 07:02:26 -----
Cmd line: com.xfhy.watchsignaldemo
Build fingerprint: 'HUAWEI/LYA-AL00/HWLYA:10/HUAWEILYA-AL00/10.1.0.163C00:user/release-keys'
ABI: 'arm64'
Build type: optimized
Zygote loaded classes=11918 post zygote classes=729
Dumping registered class loaders
#0 dalvik.system.PathClassLoader: [], parent #1
#1 java.lang.BootClassLoader: [], no parent
#2 dalvik.system.PathClassLoader: [/system/app/FeatureFramework/FeatureFramework.apk], no parent
#3 dalvik.system.PathClassLoader: [/data/app/com.xfhy.watchsignaldemo-4tkKMWojrpHAf-Q3iecaHQ==/base.apk:/data/app/com.xfhy.watchsignaldemo-4tkKMWojrpHAf-Q3iecaHQ==/base.apk!classes2.dex:/data/app/com.xfhy.watchsignaldemo-4tkKMWojrpHAf-Q3iecaHQ==/base.apk!classes4.dex:/data/app/com.xfhy.watchsignaldemo-4tkKMWojrpHAf-Q3iecaHQ==/base.apk!classes3.dex], parent #1
Done dumping class loaders
Intern table: 44132 strong; 436 weak
JNI: CheckJNI is off; globals=681 (plus 67 weak)
Libraries: /data/app/com.xfhy.watchsignaldemo-4tkKMWojrpHAf-Q3iecaHQ==/lib/arm64/libwatchsignaldemo.so libandroid.so libcompiler_rt.so libhitrace_jni.so libhiview_jni.so libhwapsimpl_jni.so libiAwareSdk_jni.so libimonitor_jni.so libjavacore.so libjavacrypto.so libjnigraphics.so libmedia_jni.so libopenjdk.so libsoundpool.so libwebviewchromium_loader.so (15)
//已分配堆內(nèi)存大小26M,其中2442kb醫(yī)用,總分配74512個對象
Heap: 90% free, 2442KB/26MB; 74512 objects

Total number of allocations 120222 //進程創(chuàng)建到現(xiàn)在一共創(chuàng)建了多少對象
Total bytes allocated 10MB         //進程創(chuàng)建到現(xiàn)在一共申請了多少內(nèi)存
Total bytes freed 8173KB           //進程創(chuàng)建到現(xiàn)在一共釋放了多少內(nèi)存
Free memory 23MB                   //不擴展堆的情況下可用的內(nèi)存
Free memory until GC 23MB          //GC前的可用內(nèi)存
Free memory until OOME 381MB       //OOM之前的可用內(nèi)存,這個值很小的話,說明已經(jīng)處于內(nèi)存緊張狀態(tài),app可能是占用了過多的內(nèi)存
Total memory 26MB                  //當前總內(nèi)存(已用+可用)
Max memory 384MB                   //進程最多能申請的內(nèi)存

.....//省略GC相關信息


//當前進程共17個線程
DALVIK THREADS (17):

//Signal Catcher線程調(diào)用棧
"Signal Catcher" daemon prio=5 tid=4 Runnable
  | group="system" sCount=0 dsCount=0 flags=0 obj=0x18c84570 self=0x7252417800
  | sysTid=7772 nice=0 cgrp=default sched=0/0 handle=0x725354ad50
  | state=R schedstat=( 16273959 1085938 5 ) utm=0 stm=1 core=4 HZ=100
  | stack=0x7253454000-0x7253456000 stackSize=991KB
  | held mutexes= "mutator lock"(shared held)
  native: #00 pc 000000000042f8e8  /apex/com.android.runtime/lib64/libart.so (art::DumpNativeStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, int, BacktraceMap*, char const*, art::ArtMethod*, void*, bool)+140)
  native: #01 pc 0000000000523590  /apex/com.android.runtime/lib64/libart.so (art::Thread::DumpStack(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool, BacktraceMap*, bool) const+508)
  native: #02 pc 000000000053e75c  /apex/com.android.runtime/lib64/libart.so (art::DumpCheckpoint::Run(art::Thread*)+844)
  native: #03 pc 000000000053735c  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::RunCheckpoint(art::Closure*, art::Closure*)+504)
  native: #04 pc 0000000000536744  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::Dump(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, bool)+1048)
  native: #05 pc 0000000000536228  /apex/com.android.runtime/lib64/libart.so (art::ThreadList::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char>>&)+884)
  native: #06 pc 00000000004ee4d8  /apex/com.android.runtime/lib64/libart.so (art::Runtime::DumpForSigQuit(std::__1::basic_ostream<char, std::__1::char_traits<char>>&)+196)
  native: #07 pc 000000000050250c  /apex/com.android.runtime/lib64/libart.so (art::SignalCatcher::HandleSigQuit()+1356)
  native: #08 pc 0000000000501558  /apex/com.android.runtime/lib64/libart.so (art::SignalCatcher::Run(void*)+268)
  native: #09 pc 00000000000cf7c0  /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+36)
  native: #10 pc 00000000000721a8  /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
  (no managed stack frames)

"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x73907540 self=0x725f010800
  | sysTid=7761 nice=-10 cgrp=default sched=1073741825/2 handle=0x72e60080d0
  | state=S schedstat=( 281909898 5919799 311 ) utm=20 stm=7 core=4 HZ=100
  | stack=0x7fca180000-0x7fca182000 stackSize=8192KB
  | held mutexes=
  at java.lang.Thread.sleep(Native method)
  - sleeping on <0x00f895d9> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:443)
  - locked <0x00f895d9> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:359)
  at android.os.SystemClock.sleep(SystemClock.java:131)
  at com.xfhy.watchsignaldemo.MainActivity.makeAnr(MainActivity.kt:35)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7317)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1219)
  at android.view.View.performClickInternal(View.java:7291)
  at android.view.View.access$3600(View.java:838)
  at android.view.View$PerformClick.run(View.java:28247)
  at android.os.Handler.handleCallback(Handler.java:900)
  at android.os.Handler.dispatchMessage(Handler.java:103)
  at android.os.Looper.loop(Looper.java:219)
  at android.app.ActivityThread.main(ActivityThread.java:8668)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)

  ... //此處省略剩余的N個線程

trace參數(shù)詳細解讀:

"Signal Catcher" daemon prio=5 tid=4 Runnable
  | group="system" sCount=0 dsCount=0 flags=0 obj=0x18c84570 self=0x7252417800
  | sysTid=7772 nice=0 cgrp=default sched=0/0 handle=0x725354ad50
  | state=R schedstat=( 16273959 1085938 5 ) utm=0 stm=1 core=4 HZ=100
  | stack=0x7253454000-0x7253456000 stackSize=991KB
  | held mutexes= "mutator lock"(shared held)

第1行:

"Signal Catcher" daemon prio=5 tid=4 Runnable

  • "Signal Catcher" daemon : 線程名,有daemon表示守護線程
  • prio:線程優(yōu)先級
  • tid:線程內(nèi)部id
  • 線程狀態(tài):Runnable
ANR線程狀態(tài)對照表

ps: 一般來說:main線程處于BLOCK、WAITING、TIMEWAITING狀態(tài),基本上是函數(shù)阻塞導致的ANR,如果main線程無異常,則應該排查CPU負載和內(nèi)存環(huán)境。

第2行:

| group="system" sCount=0 dsCount=0 flags=0 obj=0x18c84570 self=0x7252417800

  • group:線程所屬的線程組
  • sCount:線程掛起次數(shù)
  • dsCount:用于調(diào)試的線程掛起次數(shù)
  • obj:當前線程關聯(lián)的Java線程對象
  • self:當前線程地址

第3行:

| sysTid=7772 nice=0 cgrp=default sched=0/0 handle=0x725354ad50

  • sysTid:線程真正意義上的tid
  • nice:調(diào)度優(yōu)先級,值越小則優(yōu)先級越高
  • cgrp:進程所屬的進程調(diào)度組
  • sched:調(diào)度策略
  • handle:函數(shù)處理地址

第4行:

| state=R schedstat=( 16273959 1085938 5 ) utm=0 stm=1 core=4 HZ=100

  • state:線程狀態(tài)
  • schedstat:CPU調(diào)度時間統(tǒng)計(schedstat括號中的3個數(shù)字依次是Running、Runable、Switch,Running時間:CPU運行的時間,單位ns,Runable時間:RQ隊列的等待時間,單位ns,Switch次數(shù):CPU調(diào)度切換次數(shù))
  • utm/stm:用戶態(tài)/內(nèi)核態(tài)的CPU時間
  • core:該線程的最后運行所在核
  • HZ:時鐘頻率

第5行:

| stack=0x7253454000-0x7253456000 stackSize=991KB

  • stack:線程棧的地址區(qū)間
  • stackSize:棧的大小

第6行:

| held mutexes= "mutator lock"(shared held)

  • mutex:所持有mutex類型,有獨占鎖exclusive和共享鎖shared兩類

7.2 ANR案例分析

7.2.1 主線程無卡頓,處于正常狀態(tài)堆棧

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x74b38080 self=0x7ad9014c00
  | sysTid=23081 nice=0 cgrp=default sched=0/0 handle=0x7b5fdc5548
  | state=S schedstat=( 284838633 166738594 505 ) utm=21 stm=7 core=1 HZ=100
  | stack=0x7fc95da000-0x7fc95dc000 stackSize=8MB
  | held mutexes=
  kernel: __switch_to+0xb0/0xbc
  kernel: SyS_epoll_wait+0x288/0x364
  kernel: SyS_epoll_pwait+0xb0/0x124
  kernel: cpu_switch_to+0x38c/0x2258
  native: #00 pc 000000000007cd8c  /system/lib64/libc.so (__epoll_pwait+8)
  native: #01 pc 0000000000014d48  /system/lib64/libutils.so (android::Looper::pollInner(int)+148)
  native: #02 pc 0000000000014c18  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+60)
  native: #03 pc 00000000001275f4  /system/lib64/libandroid_runtime.so (android::android_os_MessageQueue_nativePollOnce(_JNIEnv*, _jobject*, long, int)+44)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:330)
  at android.os.Looper.loop(Looper.java:169)
  at android.app.ActivityThread.main(ActivityThread.java:7073)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:536)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)

比如這個主線程堆棧,看起來很正常,主線程是空閑的,因為它正處于nativePollOnce,正在等待新消息。處于這個狀態(tài),那還發(fā)生了ANR,可能有2個原因:

  1. dump堆棧時機太晚了,ANR已經(jīng)發(fā)生過了,才去dump堆棧,此時主線程已經(jīng)恢復正常了
  2. CPU搶占或者內(nèi)存緊張等其他因素引起

遇到這種情況,要先去分析CPU、內(nèi)存的使用情況。其次可以關注抓取日志的時間和ANR發(fā)生的時間是否相隔太久,時間太久這個堆棧就沒有分析的意義了。

7.2.2 主線程執(zhí)行耗時操作

//模擬主線程耗時操作,View點擊的時候調(diào)用這個函數(shù)
fun makeAnr(view: View) {
    var s = 0L
    for (i in 0..99999999999) {
        s += i
    }
    Log.d("xxx", "s=$s")
}

當主線程執(zhí)行到makeAnr時,會因為里面的東西執(zhí)行太耗時而一直在這里進行計算,假設此時有其他事情要想交給主線程處理,則必須得等到makeAnr函數(shù)執(zhí)行完才行。主線程在執(zhí)行makeAnr時,輸入事件無法被處理,用戶多次點擊屏幕之后,就會輸入超時,觸發(fā)InputEvent Timeout,導致ANR。而如果主線程在執(zhí)行上面這段耗時操作的過程中,沒有其他事情需要處理,那其實是不會發(fā)生ANR的。

suspend all histogram:  Sum: 206us 99% C.I. 0.098us-46us Avg: 7.629us Max: 46us
DALVIK THREADS (16):
"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0 obj=0x73907540 self=0x725f010800
  | sysTid=32298 nice=-10 cgrp=default sched=1073741825/2 handle=0x72e60080d0
  | state=R schedstat=( 6746757297 5887495 256 ) utm=670 stm=4 core=6 HZ=100
  | stack=0x7fca180000-0x7fca182000 stackSize=8192KB
  | held mutexes= "mutator lock"(shared held)
  at com.xfhy.watchsignaldemo.MainActivity.makeAnr(MainActivity.kt:58)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7317)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1219)
  at android.view.View.performClickInternal(View.java:7291)
  at android.view.View.access$3600(View.java:838)
  at android.view.View$PerformClick.run(View.java:28247)
  at android.os.Handler.handleCallback(Handler.java:900)
  at android.os.Handler.dispatchMessage(Handler.java:103)
  at android.os.Looper.loop(Looper.java:219)
  at android.app.ActivityThread.main(ActivityThread.java:8668)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)

從日志上看,主線程處于執(zhí)行狀態(tài),不是空閑狀態(tài),導致ANR了,說明com.xfhy.watchsignaldemo.MainActivity.makeAnr這里有耗時操作。

7.2.3 主線程被鎖阻塞

模擬主線程等待子線程的鎖:

fun makeAnr(view: View) {

    val obj1 = Any()
    val obj2 = Any()

    //搞個死鎖,相互等待

    thread(name = "臥槽") {
        synchronized(obj1) {
            SystemClock.sleep(100)
            synchronized(obj2) {
            }
        }
    }

    synchronized(obj2) {
        SystemClock.sleep(100)
        synchronized(obj1) {
        }
    }
}
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x73907540 self=0x725f010800
  | sysTid=19900 nice=-10 cgrp=default sched=0/0 handle=0x72e60080d0
  | state=S schedstat=( 542745832 9516666 182 ) utm=48 stm=5 core=4 HZ=100
  | stack=0x7fca180000-0x7fca182000 stackSize=8192KB
  | held mutexes=
  at com.xfhy.watchsignaldemo.MainActivity.makeAnr(MainActivity.kt:59)
  - waiting to lock <0x0c6f8c52> (a java.lang.Object) held by thread 22   //注釋1
  - locked <0x01abeb23> (a java.lang.Object)
  at java.lang.reflect.Method.invoke(Native method)
  at androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:441)
  at android.view.View.performClick(View.java:7317)
  at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1219)
  at android.view.View.performClickInternal(View.java:7291)
  at android.view.View.access$3600(View.java:838)
  at android.view.View$PerformClick.run(View.java:28247)
  at android.os.Handler.handleCallback(Handler.java:900)
  at android.os.Handler.dispatchMessage(Handler.java:103)
  at android.os.Looper.loop(Looper.java:219)
  at android.app.ActivityThread.main(ActivityThread.java:8668)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1109)

"臥槽" prio=5 tid=22 Blocked  //注釋2
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x12c8a118 self=0x71d625f800
  | sysTid=20611 nice=0 cgrp=default sched=0/0 handle=0x71d4513d50
  | state=S schedstat=( 486459 0 3 ) utm=0 stm=0 core=4 HZ=100
  | stack=0x71d4411000-0x71d4413000 stackSize=1039KB
  | held mutexes=
  at com.xfhy.watchsignaldemo.MainActivity$makeAnr$1.invoke(MainActivity.kt:52)
  - waiting to lock <0x01abeb23> (a java.lang.Object) held by thread 1
  - locked <0x0c6f8c52> (a java.lang.Object)  
  at com.xfhy.watchsignaldemo.MainActivity$makeAnr$1.invoke(MainActivity.kt:49)
  at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)

......

注意看,下面幾行:

"main" prio=5 tid=1 Blocked
  - waiting to lock <0x0c6f8c52> (a java.lang.Object) held by thread 22
  - locked <0x01abeb23> (a java.lang.Object)

"臥槽" prio=5 tid=22 Blocked
  - waiting to lock <0x01abeb23> (a java.lang.Object) held by thread 1
  - locked <0x0c6f8c52> (a java.lang.Object)  

主線程的tid是1,線程狀態(tài)是Blocked,正在等待0x0c6f8c52這個Object,而這個Object被thread 22這個線程所持有,主線程當前持有的是0x01abeb23的鎖。而臥槽的tid是22,也是Blocked狀態(tài),它想請求的和已有的鎖剛好與主線程相反。這樣的話,ANR原因也就找到了:線程22持有了一把鎖,并且一直不釋放,主線程等待這把鎖發(fā)生超時。在線上環(huán)境,常見因鎖而ANR的場景是SharePreference寫入。

7.2.4 CPU被搶占

CPU usage from 0ms to 10625ms later (2020-03-09 14:38:31.633 to 2020-03-09 14:38:42.257):
  543% 2045/com.test.demo: 54% user + 89% kernel / faults: 4608 minor 1 major //注意看這里
  99% 674/android.hardware.camera.provider@2.4-service: 81% user + 18% kernel / faults: 403 minor
  24% 32589/com.wang.test: 22% user + 1.4% kernel / faults: 7432 minor 1 major
  ......

可以看到,該進程占據(jù)CPU高達543%,搶占了大部分CPU資源,因為導致發(fā)生ANR,這種ANR與我們的app無關。

7.2.5 內(nèi)存緊張導致ANR

如果一份ANR日志的CPU和堆棧都很正常,可以考慮是內(nèi)存緊張??匆幌翧NR日志里面的內(nèi)存相關部分。還可以去日志里面搜一下onTrimMemory,如果dump ANR日志的時間附近有相關日志,可能是內(nèi)存比較緊張了。

10-31 22:37:19.749 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0
10-31 22:37:33.458 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0
10-31 22:38:00.153 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0
10-31 22:38:58.731 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0
10-31 22:39:02.816 20733 20733 E Runtime : onTrimMemory level:80,pid:com.xxx.xxx:Launcher0

7.2.6 系統(tǒng)服務超時導致ANR

系統(tǒng)服務超時一般會包含BinderProxy.transactNative關鍵字,來看一段日志:

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x727851e8 self=0x78d7060e00
  | sysTid=4894 nice=0 cgrp=default sched=0/0 handle=0x795cc1e9a8
  | state=S schedstat=( 8292806752 1621087524 7167 ) utm=707 stm=122 core=5 HZ=100
  | stack=0x7febb64000-0x7febb66000 stackSize=8MB
  | held mutexes=
  kernel: __switch_to+0x90/0xc4
  kernel: binder_thread_read+0xbd8/0x144c
  kernel: binder_ioctl_write_read.constprop.58+0x20c/0x348
  kernel: binder_ioctl+0x5d4/0x88c
  kernel: do_vfs_ioctl+0xb8/0xb1c
  kernel: SyS_ioctl+0x84/0x98
  kernel: cpu_switch_to+0x34c/0x22c0
  native: #00 pc 000000000007a2ac  /system/lib64/libc.so (__ioctl+4)
  native: #01 pc 00000000000276ec  /system/lib64/libc.so (ioctl+132)
  native: #02 pc 00000000000557d4  /system/lib64/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+252)
  native: #03 pc 0000000000056494  /system/lib64/libbinder.so (android::IPCThreadState::waitForResponse(android::Parcel*, int*)+60)
  native: #04 pc 00000000000562d0  /system/lib64/libbinder.so (android::IPCThreadState::transact(int, unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+216)
  native: #05 pc 000000000004ce1c  /system/lib64/libbinder.so (android::BpBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+72)
  native: #06 pc 00000000001281c8  /system/lib64/libandroid_runtime.so (???)
  native: #07 pc 0000000000947ed4  /system/framework/arm64/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+196)
  at android.os.BinderProxy.transactNative(Native method) ————————————————關鍵行?。?!
  at android.os.BinderProxy.transact(Binder.java:804)
  at android.net.IConnectivityManager$Stub$Proxy.getActiveNetworkInfo(IConnectivityManager.java:1204)—關鍵行!
  at android.net.ConnectivityManager.getActiveNetworkInfo(ConnectivityManager.java:800)
  at com.xiaomi.NetworkUtils.getNetworkInfo(NetworkUtils.java:2)
  at com.xiaomi.frameworkbase.utils.NetworkUtils.getNetWorkType(NetworkUtils.java:1)
  at com.xiaomi.frameworkbase.utils.NetworkUtils.isWifiConnected(NetworkUtils.java:1)

從日志堆棧中可以看到是獲取網(wǎng)絡信息發(fā)生了ANR:getActiveNetworkInfo。系統(tǒng)的服務都是Binder機制(16個線程),服務能力也是有限的,有可能系統(tǒng)服務長時間不響應導致ANR。如果其他應用占用了所有Binder線程,那么當前應用只能等待。可進一步搜索:blockUntilThreadAvailable關鍵字:

at android.os.Binder.blockUntilThreadAvailable(Native method)

如果有發(fā)現(xiàn)某個線程的堆棧,包含此字樣,可進一步看其堆棧,確定是調(diào)用了什么系統(tǒng)服務。此類ANR也是屬于系統(tǒng)環(huán)境的問題,如果某類型手機上頻繁發(fā)生此問題,應用層可以考慮規(guī)避策略。

8. ANR影響因素

即使我們利用上面的一系列騷操作,在發(fā)生ANR時,我們拿到了Trace堆棧。但實際情況下這些Trace堆棧中,有很多不是導致ANR的根本原因。Trace堆棧提示某個Service或Receiver導致的ANR,但其實很可能并不是這些組件自身的問題導致的ANR,至于為什么,下面一一道來。

影響ANR的本質(zhì)要素大體來說分為2個:應用內(nèi)部環(huán)境和系統(tǒng)環(huán)境。當系統(tǒng)負載正常,但是應用內(nèi)部主線程消息過多或耗時驗證;另外一類是系統(tǒng)或應用內(nèi)部其他線程或資源負載過高,主線程調(diào)度被嚴重搶占。

系統(tǒng)負載高咱們沒有辦法,但系統(tǒng)負載正常時,主線程的調(diào)度問題主要有下面幾個:

  1. 當前Trace堆棧所在業(yè)務耗時嚴重
  2. 當前Trace堆棧所在業(yè)務耗時并不嚴重,但歷史調(diào)度有一個嚴重耗時
  3. 當前Trace堆棧所在業(yè)務耗時并不嚴重,但歷史調(diào)度有多個消息耗時
  4. 當前Trace堆棧所在業(yè)務耗時并不嚴重,但是歷史調(diào)度存在巨量重復消息(業(yè)務頻繁發(fā)送消息)
  5. 當前Trace堆棧業(yè)務邏輯并不耗時,但是其他線程存在嚴重資源搶占,如IO、Mem、CPU;
  6. 當前Trace堆棧業(yè)務邏輯并不耗時,但是其他進程存在嚴重資源搶占,如IO、Mem、CPU。

請注意,這里的6個影響因素中,除了第一個以外,其他的根據(jù)ANR Trace有可能無法進行判別。這就會導致很多時候看到的ANR Trace里面主線程堆棧對應的業(yè)務其實并不耗時(因為可能是前面的消息導致的耗時,但它已經(jīng)執(zhí)行完了),如何解決這個問題?

9. 彌補不足

字節(jié)跳動內(nèi)部有一個監(jiān)控工具:Raster,這個庫專門解決上面的問題。有一點可惜的是該工具暫時還沒開源,但是我們從字節(jié)發(fā)出來的Raster原理相關的文章能了解到該庫的詳細原理。原文 : 今日頭條 ANR 優(yōu)化實踐系列 - 監(jiān)控工具與分析思路

Raster的大致原理:該工具主要是在主線程消息調(diào)度過程進行監(jiān)控,并按照一定的策略聚合,以保證監(jiān)控工具本身對應用性能和內(nèi)存抖動影響降至最低。比較耗時的消息會抓取主線從堆棧,這樣可以知道那個耗時的消息具體是在干什么,從而針對性優(yōu)化。同時對應用四大組件消息執(zhí)行過程進行監(jiān)控,便于對這類消息的調(diào)度及耗時情況進行跟蹤和記錄。另外對當前正在調(diào)度的消息及消息隊列中待調(diào)度消息進行統(tǒng)計,從而在發(fā)生問題時,可以回放主線程的整體調(diào)度情況。此外,該庫將系統(tǒng)服務的CheckTime機制遷移到應用側(cè),應用為線程CheckTime機制,以便于系統(tǒng)信息不足時,從線程調(diào)度及時性推測過去一段時間系統(tǒng)負載和調(diào)度情況。因此該工具用一句話來概括就是:由點到面,回放過去,現(xiàn)在和將來。

細說一下線程 Checktime:通過借助其他子線程的周期檢測機制,在每次調(diào)度前獲取當前系統(tǒng)時間,然后減去我們設置延遲的時間,即可得到本次線程調(diào)度前的真實間隔時間,如設置線程每隔300ms調(diào)度一次,結(jié)果發(fā)現(xiàn)實際響應時間間隔有時會超過300ms,如果偏差越大則說明線程沒有及時調(diào)度,進一步反映系統(tǒng)響應能力變差。通過這樣的方式,即使線上環(huán)境獲取不到系統(tǒng)日志,也可以從側(cè)面反映不同時段系統(tǒng)負載對線程調(diào)度影響。當連續(xù)發(fā)生多次嚴重Delay時,說明線程調(diào)度受到了影響。

通過上訴監(jiān)控能力,我們就可以清晰的知道ANR發(fā)生時主線程歷史消息調(diào)度以及耗時嚴重消息的采樣堆棧,同時可以知道正在執(zhí)行消息的耗時,以及消息隊列中調(diào)度消息的狀態(tài)。同時通過線程CheckTime機制從側(cè)面反映線程調(diào)度響應能力,由此完成了應用側(cè)監(jiān)控信息從點到面的覆蓋。

有大佬根據(jù)該文章的原理實現(xiàn)了一個類似的開源庫: MoonlightTreasureBox,MoonlightTreasureBox 開源地址

10. QA

10.1 在Activity#onCreate中sleep會導致ANR嗎?

不會,ANR的場景只有下面4種:Service Timeout、BroadcastQueue Timeout、ContentProvider Timeout、InputDispatching Timeout。

當然,如果在Activity#onCreate中sleep的過程中,用戶點擊了屏幕,那是有可能觸發(fā)InputDispatching Timeout的。

11. 小結(jié)

很榮幸地恭喜你,讀完了整篇文章。

ANR是老生常談的問題了,本文從定義、原因、發(fā)生場景、觸發(fā)流程、監(jiān)控與分析等多方面入手,盡力補全ANR這塊的知識。

ANR的發(fā)生場景只有4種:Service Timeout、BroadcastQueue Timeout、ContentProvider Timeout、InputDispatching Timeout,但導致ANR的原因是多種多樣的,可能是App這邊導致的,也可能是系統(tǒng)那邊導致的。觸發(fā)ANR的過程大致又可以分為2種,一種是Service、Broadcast、Provider觸發(fā)ANR:埋炸彈、拆炸彈、引爆炸彈,另外一種是Input觸發(fā)ANR:處理后續(xù)時檢測之前的。觸發(fā)ANR之后,會走dump ANR Trace的流程,收集相關進程的堆棧信息寫入文件。我們可以監(jiān)聽SIGQUIT信號,感知到系統(tǒng)在走dump ANR Trace的流程,我們可以進一步確認一下當前進程是否處于ANR的狀態(tài),然后通過hook系統(tǒng)與App的邊界,從而通過socket拿到系統(tǒng)dump好的ANR Trace內(nèi)容。拿到ANR Trace內(nèi)容之后,當然就是分析了,詳細請看文章。但是有時候,拿到的ANR Trace并不能把真正的ANR原因給分析出來,這時就得上字節(jié)內(nèi)部的大殺器了:Raster,雖然暫時還沒開源,但字節(jié)已將其原理一五一十的分享出來了。Raster主要是能知道主線程的消息調(diào)度在過去、現(xiàn)在、將來的具體情況,配合線程 CheckTime 感知線程調(diào)度能力,要比單單分析 ANR Trace要方便很多。

12. 資料

感謝以下所有大佬的精彩文章。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 卡頓原因 人眼能感覺到的幀率是每秒24幀,而屏幕每16毫秒會刷新一次,也就是每秒會刷新60次。當每秒刷新次數(shù)少于6...
    Archer_J閱讀 3,133評論 0 16
  • Broadcast ANR[#broadcast-anr] Service ANR[#service-anr] C...
    VanceKing閱讀 868評論 0 1
  • ANR原理分析 什么是ANR ANR(Application Not Responding)就是應用在規(guī)定的時間內(nèi)...
    米豆同學閱讀 459評論 0 0
  • 如何定義發(fā)生了卡頓現(xiàn)象: 線下很難復現(xiàn),與發(fā)生場景強相關(所以需要我們?nèi)プ隹D監(jiān)控,收集現(xiàn)場信息) CPU相關知識...
    今陽說閱讀 1,042評論 0 2
  • 日常開發(fā)測試中,我們經(jīng)常會遇到各種ANR問題,ANR的全稱是application not responding...
    liuye099閱讀 741評論 0 1

友情鏈接更多精彩內(nèi)容