日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

一、前言

最近參加了幾輪面試,發現很多5-7年工作經驗的候選人在性能優化這一塊,基本上只能說出傳統的分析方式,例如ANR分析,是通過查看/data/anr/ 下的log,分析主線程堆棧、cpu、鎖信息等,

然而,這種方法有一定的局限性,并不是每次都奏效,很多時候是沒有堆棧信息給你分析的,例如有些高版本設備需要root權限才能訪問/data/anr/ 目錄,或者是線上用戶的反饋,只有一張ANR的截圖加上一句話描述。

假如你的App沒有實現ANR監控上報,那么你大概率會把這個問題當成“未復現”處理掉,而沒有真正解決問題。

于是我整理了這一篇文章,主要關于卡頓、ANR、死鎖監控方案。

二、卡頓原理和監控

2.1 卡頓原理

一般來說,主線程有耗時操作會導致卡頓,卡頓超過閾值,觸發ANR。

從源碼層面一步步分析卡頓原理:

首先應用進程啟動的時候,Zygote會反射調用 ActivityThread 的 main 方法,啟動 loop 循環

->ActivityThread

public static void main(String[] args) {
      ...
    Looper.prepareMainLooper();
    Looper.loop();
    ...
}
復制代碼

看下Looper的loop方法

->Looper

public static void loop() {
      for (;;) {
            //1、取消息
            Message msg = queue.next(); // might block
            ...
            //2、消息處理前回調
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ...

            //3、消息開始處理
            msg.target.dispatchMessage(msg);// 分發處理消息
            ...

            //4、消息處理完回調
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
       }
       ...
}

復制代碼

由于loop循環存在,所以主線程可以長時間運行。如果想要在主線程執行某個任務,唯一的辦法就是通過主線程Handler post一個任務到消息隊列里去,然后loop循環中拿到這個msg,交給這個msg的target處理,這個target是Handler。

從上面的代碼塊可以看出,導致卡頓的原因可能有兩個地方

  • 注釋1的queue.next()阻塞,
  • 注釋3的dispatchMessage耗時太久。

2.1.1 MessageQueue#next 耗時

看下源碼

MessageQueue#next

    Message next() {
        for (;;) {
            //1、nextPollTimeoutMillis 不為0則阻塞
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                // 2、先判斷當前第一條消息是不是同步屏障消息,
                if (msg != null && msg.target == null) {
                    //3、遇到同步屏障消息,就跳過去取后面的異步消息來處理,同步消息相當于被設立了屏障
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }

                //4、正常的消息處理,判斷是否有延時
                if (msg != null) {
                    if (now < msg.when) {
                        //3.1 
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    //5、如果沒有取到異步消息,那么下次循環就走到1那里去了,nativePollOnce為-1,會一直阻塞
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
    }
復制代碼

next方法的大致流程是這樣的:

  1. MessageQueue是一個鏈表數據結構,判斷MessageQueue的頭部(第一個消息)是不是一個同步屏障消息,所謂同步屏障消息,就是給同步消息加一層屏障,讓同步消息不被處理,只會處理異步消息;
  2. 如果遇到同步屏障消息,就會跳過MessageQueue中的同步消息,只獲取里面的異步消息來處理。如果里面沒有異步消息,那就會走到注釋5,nextPollTimeoutMillis設置為-1,下次循環調用注釋1的nativePollOnce就會阻塞;
  3. 如果looper能正常獲取到消息,不管是異步消息或者同步消息,處理流程都是一樣的,在注釋4,先判斷是否帶延時,如果是,nextPollTimeoutMillis就會被賦值,然后下次循環調用注釋1的nativePollOnce就會阻塞一段時間。如果不是delay消息,就直接返回這個msg,給handler處理;

從上面分析可以看出,next方法是不斷從MessageQueue里取出消息,有消息就處理,沒有消息就調用nativePollOnce阻塞,nativePollOnce 底層是linux的epoll機制,這里涉及到一個Linux IO 多路復用的知識點

Linux IO 多路復用,select、poll、epoll

Linux 上IO多路復用方案有 select、poll、epoll。它們三個中 epoll 的性能表現是最優秀的,能支持的并發量也最大。

  1. select 是操作系統提供的系統調用函數,通過它,我們可以把一個文件描述符的數組發給操作系統, 讓操作系統去遍歷,確定哪個文件描述符可以讀寫, 然后告訴我們去處理。
  2. poll:它和 select 的主要區別就是,去掉了 select 只能監聽 1024 個文件描述符的限制
  3. epoll:epoll 主要就是針對select的這三個可優化點進行了改進

1、內核中保存一份文件描述符集合,無需用戶每次都重新傳入,只需告訴內核修改的部分即可。 2、內核不再通過輪詢的方式找到就緒的文件描述符,而是通過異步 IO 事件喚醒。 3、內核僅會將有 IO 事件的文件描述符返回給用戶,用戶也無需遍歷整個文件描述符集合。

關于epoll機制就總結這么多啦。

回到 MessageQueue的next 方法,看看哪里可能阻塞

同步屏障消息沒移除導致next一直阻塞

有一種情況,在存在同步屏障消息的情況下,當異步消息被處理完之后,如果沒有及時把同步屏障消息移除,會導致同步消息一直沒有機會處理,一直阻塞在nativePollOnce

同步屏障消息

Android 是禁止App往MessageQueue插入同步屏障消息的,代碼會報錯

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

系統一些高優先級的操作會使用到同步屏障消息,例如View在繪制的時候,最終都要調用ViewRootImpl的scheduleTraversals方法,會往MessageQueue插入同步屏障消息,繪制完成后會移除同步屏障消息。

->ViewRootImpl

    @UnsupportedAppUsage
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //插入同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            //移除同步屏障消息
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

復制代碼

為了保證View的繪制過程不被主線程其它任務影響,View在繪制之前會先往MessageQueue插入同步屏障消息,然后再注冊Vsync信號監聽,Choreographer$FrameDisplayEventReceiver就是用來接收vsync信號回調的

Choreographer$FrameDisplayEventReceiver

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        ...
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
           ...
            //
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            //1、發送異步消息
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            // 2、doFrame優先執行
            doFrame(mTimestampNanos, mFrame);
        }
    }
復制代碼

收到Vsync信號回調,注釋1會往主線程MessageQueue post一個異步消息,保證注釋2的doFrame優先執行。

doFrame才是View真正開始繪制的地方,會調用ViewRootImpl的doTraversal、performTraversals,

而performTraversals里面會調用我們熟悉的View的onMeasure、onLayout、onDraw。

這里還可以延伸到vsync信號原理,以及為什么要等vsync信號回調才開始View的繪制流程、掉幀的原理、屏幕的雙緩沖、三緩沖,由于文章篇幅關系,不是本文的重點,就不一一分析了~

雖然app無法發送同步屏障消息,但是使用異步消息是允許的

異步消息

首先,SDK中限制了App不能post異步消息到MessageQueue里去的,相關字段被加了UnsupportedAppUsage注解

-> Message

    @UnsupportedAppUsage
    /*package*/ int flags;

    /**
     * Returns true if the message is asynchronous, meaning that it is not
     * subject to {@link Looper} synchronization barriers.
     *
     * @return True if the message is asynchronous.
     *
     * @see #setAsynchronous(boolean)
     */
    public boolean isAsynchronous() {
        return (flags & FLAG_ASYNCHRONOUS) != 0;
    }
復制代碼

不過呢,高版本的Handler的構造方法可以通過傳async=true,來使用異步消息

public Handler(@Nullable Callback callback, boolean async) {}
復制代碼

然后在Handler發送消息的時候,都會走到 enqueueMessage 方法,如下代碼塊所示,每個消息都帶了異步屬性,有優先處理權

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,long uptimeMillis) {
        ...
        //如果mAsynchronous為true,就都設置為異步消息
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
復制代碼

對于低版本SDK,想要使用異步消息,可以通過反射調用Handler(@Nullable Callback callback, boolean async),參考androidx內部的一段代碼如下

->androidx.arch.core.executor.DefaultTaskExecutor

    private static Handler createAsync(@NonNull Looper looper) {
        if (Build.VERSION.SDK_INT >= 28) {
            return Handler.createAsync(looper);
        }
        if (Build.VERSION.SDK_INT >= 16) {
            try {
                return Handler.class.getDeclaredConstructor(Looper.class, Handler.Callback.class,
                        boolean.class)
                        .newInstance(looper, null, true);
            } catch (IllegalAccessException ignored) {
            } catch (InstantiationException ignored) {
            } catch (NoSuchMethodException ignored) {
            } catch (InvocationTargetException e) {
                return new Handler(looper);
            }
        }
        return new Handler(looper);
    }
復制代碼

需要注意的是,App要謹慎使用異步消息,使用不當的情況下可能會出現主線程假死的問題,排查也比較困難

分析完MessageQueue#next再回頭來看看 Handler的dispatchMessage方法

2.1.2 dispatchMessage

上面說到next方法輪循取消息一般情況下是沒有問題的,那么只剩下處理消息的邏輯

Handler#dispatchMessage

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
復制代碼

dispatchMessage 有三個邏輯,分別對應Handler 使用的三種方式

  1. Handler#post(Runnable r)
  2. 構造方法傳CallBack,public Handler(@Nullable Callback callback, boolean async) {}
  3. Handler 重寫 handleMessage 方法

所以,應用卡頓,原因一般都可以認為是Handler處理消息太耗時導致的,細分的原因可能是方法本身太耗時、算法效率低、cpu被搶占、內存不足、IPC超時等等。

2.2 卡頓監控

面試中,被問到如何監控App卡頓,統計方法耗時,我們可以從源碼開始切入,講講如何通過Looper提供的Printer接口,計算Handler處理一個消息的耗時,判斷是否出現卡頓。

2.2.1 卡頓監控方案一

看下Looper 循環的注釋2和注釋4,可以找到一種卡頓監控的方法

Looper#loop

public static void loop() {
        for (;;) {
            //1、取消息
            Message msg = queue.next(); // might block
            ...
            //2、消息處理前回調
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
            ...

            //3、消息開始處理
            msg.target.dispatchMessage(msg);// 分發處理消息
            ...

            //4、消息處理完回調
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
       }
       ...
}
復制代碼

注釋2和注釋4的logging.println是谷歌提供給我們的一個接口,可以監聽Handler處理消息耗時,我們只需要調用Looper.getMainLooper().setMessageLogging(printer),即可從回調中拿到Handler處理一個消息的前后時間。

需要注意的是,監聽到發生卡頓之后,dispatchMessage 早已調用結束,已經出棧,此時再去獲取主線程堆棧,堆棧中是不包含卡頓的代碼的。

所以需要在后臺開一個線程,定時獲取主線程堆棧,將時間點作為key,堆棧信息作為value,保存到Map中,在發生卡頓的時候,取出卡頓時間段內的堆棧信息即可。

不過這種方案只適合線下使用,原因如下:

  1. logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);存在字符串拼接,頻繁調用,會創建大量對象,造成內存抖動。
  2. 后臺線程頻繁獲取主線程堆棧,對性能有一定影響,獲取主線程堆棧,會暫停主線程的運行

2.2.2 卡頓監控方案二

對于線上卡頓監控,需要了解字節碼插樁技術。

通過Gradle Plugin+ASM,編譯期在每個方法開始和結束位置分別插入一行代碼,統計方法耗時,

偽代碼如下

插樁前
fun method(){
   run()
}

插樁后
fun method(){
   input(1)
   run()
   output(1)
}

復制代碼

目前微信的Matrix 使用的卡頓監控方案就是字節碼插樁,如下圖所示

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

插樁需要注意的問題:

  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的生命周期, ActivityThread的handleCreateService方法會被調用

-> 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,Service的onCreate方法被調用,
注釋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,最終都會調用AppErrors的appNotResponding方法

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

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

ANR日志中有很多信息,可以看到,主線程id是1(tid=1),在等待一個鎖,這個鎖一直被id為22的程持有,那么看下22號線程的堆棧

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

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這個目錄

Android性能優化高階:卡頓、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 會出現漏檢測的情況,看圖

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

如上圖這種情況,紅色表示卡頓,

  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

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

5.3.2 ANR檢測結果

logcat打印所示

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

主線程卡頓超過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

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

從源碼和源碼的解釋可以看出,這個函數是用來獲取當前線程等待的Monitor。

順便說說Monitor以及Java對象結構

Monitor

Monitor是一種并發控制機制,提供多線程環境下的互斥和同步,以支持安全的并發訪問。

Monitor由以下3個元素組成:

  1. 臨界區:例如synchronize修飾的代碼塊
  2. 條件變量:用來維護因不滿足條件而阻塞的線程隊列
  3. Monitor對象,維護Monitor的入口、臨界區互斥量(即鎖)、臨界區和條件變量,以及條件變量上的阻塞和喚醒

Java的Class對象

Java的Class對象包括三部分組成:

  1. 對象頭:Markword和對象指針MarkWord(標記字段):保存哈希碼、分代年齡、鎖標志位、偏向線程ID、偏向時間戳等信息 對象指針:即指向當前對象的類的元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
  2. 實例數據:對象實際的數據
  3. 對齊填充:按8字節對齊(JVM自動內存管理系統要求對象起始地址必須是8字節的整數倍)。例如Integer對象,對象頭MarkWord和對象指針分別占用4字節,實例數據4字節,那么對齊填充就是4字節,Integer占用內存是int的4倍。

回到 GetContendedMonitor 函數,我們可以通過打開動態庫libart.so,然后使用dlsym獲取函數的符號地址,然后就可以進行調用了。

由于Android 7.0開始,系統限制App中調用dlopen,dlsym等函數打開系統動態庫,我們可以使用 ndk_dlopen這個庫來繞過這個限制

    //1、初始化
    ndk_init(env);

    //2、打開動態庫libart.so
    void *so_addr = ndk_dlopen("libart.so", RTLD_NOLOAD);
    if (so_addr == NULL) {
        return 1;
    }

復制代碼

打開動態庫之后,會返回動態庫的內存地址,接下來就可以通過dlsym獲取GetContendedMonitor這個函數的符號地址,只不過要注意,c++可以重載,所以它的函數符號比較特殊,需要從libart.so中搜索匹配找到

    //c++跟c不一樣,c++可以重載,描述符會變,需要打開libart.so,在里面搜索查找GetContendedMonitor的函數符號
    //http://androidxref.com/8.0.0_r4/xref/system/core/libbacktrace/testdata/arm/libart.so

    //獲取Monitor::GetContendedMonitor函數符號地址
    get_contended_monitor = ndk_dlsym(so_addr, "_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE");
    if (get_contended_monitor == NULL) {
        return 2;
    }
復制代碼

到此,第一個函數的符號地址找到了,接下來要找另外一個函數

6.3 獲取目標鎖被哪個線程持有

函數:
Monitor::GetLockOwnerThreadId

Android性能優化高階:卡頓、ANR、死鎖,線上如何監控?

 

用同樣的方式來獲取這個函數符號地址


    // Monitor::GetLockOwnerThreadId
    //這個函數是用來獲取 Monitor的持有者,會返回線程id
    get_lock_owner_thread = ndk_dlsym(so_addr, get_lock_owner_symbol_name(api_level));
    if (get_lock_owner_thread == NULL) {
        return 3;
    }

復制代碼

由于從android 10開始,這個GetLockOwnerThreadId函數符號有變化,所以需要通過api版本來判斷使用哪一個

const char *get_lock_owner_symbol_name(jint level) {
    if (level <= 29) {
        //android 9.0 之前
        //http://androidxref.com/9.0.0_r3/xref/system/core/libbacktrace/testdata/arm/libart.so 搜索 GetLockOwnerThreadId
        return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
    } else {
        //android 10.0
        // todo 10.0 源碼中這個函數符號變了,需要自行查閱
        return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
    }
}
復制代碼

到此,就得到了兩個函數符號地址,接下來就把blocked狀態的native線程id傳過去,調用就行了

6.4 找到一直不釋放鎖的線程

Java_com_lanshifu_demo_anrmonitor_DeadLockMonitor_getContentThreadIdArt(JNIEnv *env,jobject thiz,jlong native_thread) {

    LOGI("getContentThreadIdArt");
    int monitor_thread_id = 0;
    if (get_contended_monitor != NULL && get_lock_owner_thread != NULL) {
        LOGI("get_contended_monitor != NULL");
        //1、調用一下獲取monitor的函數,返回當前線程想要競爭的monitor
        int monitorObj = ((int (*)(long)) get_contended_monitor)(native_thread);
        if (monitorObj != 0) {
            LOGI("monitorObj != 0");
            // 2、獲取這個monitor被哪個線程持有,返回該線程id
            monitor_thread_id = ((int (*)(int)) get_lock_owner_thread)(monitorObj);
        } else {
            LOGE("GetContendedMonitor return null");
            monitor_thread_id = 0;
        }
    } else {
        LOGE("get_contended_monitor == NULL || get_lock_owner_thread == NULL");

    }
    return monitor_thread_id;
}

復制代碼

兩個步驟:

  1. 獲取當前線程要競爭的鎖
  2. 獲取這個鎖被哪個線程持有

通過兩個步驟,得到的是那個一直不釋放鎖的線程id。

6.5 通過算法,找到死鎖

前面已經知道當前blocked狀態的線程id(還需要轉換成native線程id),以及這個blocked線程在等待哪個線程釋放鎖,也就是得到關系鏈:

  1. A等待B B等待A
  2. A等待B B等待C C等待A ...
  3. 其它...

如何判斷有死鎖?我們可以用Map來保存對應關系

map[A]=B

map[B]=A

最后通過互斥條件判斷出死鎖線程,把造成死鎖的線程堆棧信息輸出,如下

 

檢查出死鎖,線下可以彈窗或者toast,線上則可以采集數據上報。

6.6 死鎖監控小結

死鎖監控原理還是比較清晰的:

  1. 獲取blocked狀態的線程
  2. 獲取該線程想要競爭的鎖(native層函數)
  3. 獲取這個鎖被哪個線程持有(native層函數)
  4. 有了關系鏈,就可以找出造成死鎖的線程

由于死鎖監控涉及到native層代碼,對于很多應用層開發的同學來說可能有點難度,

但是正因為有難度,我們去了解,去學習,并且掌握了,才能在眾多競爭者中脫穎而出。

七、形成閉環

前面分開講了卡頓監控、ANR監控和死鎖監控,我們可以把它連接起來,在發生ANR的時候,將整個監控流程形成一個閉環

  1. 發生ANR
  2. 獲取主線程堆棧信息
  3. 檢測死鎖
  4. 獲取死鎖對應線程堆棧信息
  5. 上報到服務器
  6. 結合git,定位到最后修改代碼的同學,給他提一個線上問題單

八、總結

這篇文章從源碼層面分析了卡頓、ANR,以及死鎖監控,平時開發中,大部分同學可能都是做業務需求為主,對于ANR問題,可能不太注重,或者直接依賴第三方,例如Bugly,但是呢,在面試中,面試官基本不太會問你這些工具的使用,要問也是從原理層面問。

本文以卡頓作為切入點

  1. 講解卡頓原理以及卡頓監控的方式;
  2. 引申了Handler機制、Linux的epoll機制
  3. 分析ANR觸發流程,可以比喻為埋炸彈和拆炸彈過程
  4. ANR常規分析方案,/data/anr/traces.txt,
  5. ANRWatchDog 方案
  6. ANRWatchDog存在問題,進行優化
  7. 死鎖導致的ANR,死鎖監控
  8. 形成閉環

分享到:
標簽:Android
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定