<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          卡頓、ANR、死鎖,線上如何監(jiān)控?

          共 50594字,需瀏覽 102分鐘

           ·

          2021-06-24 12:59

          相關(guān)閱讀:一個90后員工猝死的全過程


          作者:藍(lán)師傅

          鏈接:https://juejin.cn/post/6973564044351373326


          一、前言

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

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

          假如你的App沒有實現(xiàn)ANR監(jiān)控上報,那么你大概率會把這個問題當(dāng)成“未復(fù)現(xiàn)”處理掉,而沒有真正解決問題。

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

          二、卡頓原理和監(jiān)控

          2.1 卡頓原理

          一般來說,主線程有耗時操作會導(dǎo)致卡頓,卡頓超過閾值,觸發(fā)ANR。

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

          首先應(yīng)用進(jìn)程啟動的時候,Zygote會反射調(diào)用 ActivityThread 的 main 方法,啟動 loop 循環(huán)

          ->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、消息處理前回調(diào)
                      if (logging != null) {
                          logging.println(">>>>> Dispatching to " + msg.target + " " +
                                  msg.callback + ": " + msg.what);
                      }
                      ...
                      
                      //3、消息開始處理
                      msg.target.dispatchMessage(msg);// 分發(fā)處理消息
                      ...
                      
                      //4、消息處理完回調(diào)
                      if (logging != null) {
                          logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                      }
                 }
                 ...
          }

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

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

          • 注釋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、先判斷當(dāng)前第一條消息是不是同步屏障消息,
                      if (msg != null && msg.target == null) {
                          //3、遇到同步屏障消息,就跳過去取后面的異步消息來處理,同步消息相當(dāng)于被設(shè)立了屏障
                          // 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、如果沒有取到異步消息,那么下次循環(huán)就走到1那里去了,nativePollOnce為-1,會一直阻塞
                          // No more messages.
                          nextPollTimeoutMillis = -1;
                      }
          }

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

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

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

          Linux IO 多路復(fù)用,select、poll、epoll

          Linux 上IO多路復(fù)用方案有 「select、poll、epol」l。它們?nèi)齻€中 epoll 的性能表現(xiàn)是最優(yōu)秀的,能支持的并發(fā)量也最大。

          1. 「select」 是操作系統(tǒng)提供的系統(tǒng)調(diào)用函數(shù),通過它,我們可以把一個文件描述符的數(shù)組發(fā)給操作系統(tǒng), 讓操作系統(tǒng)去遍歷,確定哪個文件描述符可以讀寫, 然后告訴我們?nèi)ヌ幚怼?/section>
          2. 「poll」:它和 select 的主要區(qū)別就是,去掉了 select 只能監(jiān)聽 1024 個文件描述符的限制
          3. 「epoll」:epoll 主要就是針對select的這三個可優(yōu)化點進(jìn)行了改進(jìn)
          ?

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

          ?

          回到 MessageQueuenext 方法,看看哪里可能阻塞

          同步屏障消息沒移除導(dǎo)致next一直阻塞

          有一種情況,在存在同步屏障消息的情況下,當(dāng)異步消息被處理完之后,如果沒有及時把同步屏障消息移除,會導(dǎo)致同步消息一直沒有機(jī)會處理,一直阻塞在「nativePollOnce」。

          同步屏障消息

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

          系統(tǒng)一些高優(yōu)先級的操作會使用到同步屏障消息,例如View在繪制的時候,最終都要調(diào)用ViewRootImplscheduleTraversals方法,會往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的繪制過程不被主線程其它任務(wù)影響,View在繪制之前會先往MessageQueue插入同步屏障消息,然后再注冊Vsync信號監(jiān)聽,Choreographer$FrameDisplayEventReceiver就是用來接收vsync信號回調(diào)的

          「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、發(fā)送異步消息
                  msg.setAsynchronous(true);
                  mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
              }

              @Override
              public void run() {
                  // 2、doFrame優(yōu)先執(zhí)行
                  doFrame(mTimestampNanos, mFrame);
              }
          }

          收到Vsync信號回調(diào),注釋1會往主線程MessageQueue post一個異步消息,保證注釋2的doFrame優(yōu)先執(zhí)行。

          doFrame才是View真正開始繪制的地方,會調(diào)用ViewRootImpldoTraversal、performTraversals

          performTraversals里面會調(diào)用我們熟悉的View的onMeasure、onLayout、onDraw。

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

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

          異步消息

          首先,SDK中限制了App不能post異步消息到MessageQueue里去的,相關(guān)字段被加了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的構(gòu)造方法可以通過傳async=true,來使用異步消息

          public Handler(@Nullable Callback callback, boolean async) {}

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

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

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

          ->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.classHandler.Callback.class,
                               boolean.class)
                               .newInstance(loopernulltrue)
          ;
                   } catch (IllegalAccessException ignored) {
                   } catch (InstantiationException ignored) {
                   } catch (NoSuchMethodException ignored) {
                   } catch (InvocationTargetException e) {
                       return new Handler(looper);
                   }
               }
               return new Handler(looper);
           }

          需要注意的是,「App要謹(jǐn)慎使用異步消息,使用不當(dāng)?shù)那闆r下可能會出現(xiàn)主線程假死的問題,排查也比較困難」,具體可以參考這一篇文章:今日頭條 ANR 優(yōu)化實踐系列 - Barrier 導(dǎo)致主線程假死https://juejin.cn/post/6947986170135445535

          分析完MessageQueue#next再回頭來看看 HandlerdispatchMessage方法

          2.1.2 dispatchMessage

          上面說到next方法輪詢?nèi)∠⒁话闱闆r下是沒有問題的,那么只剩下處理消息的邏輯

          「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 有三個邏輯,分別對應(yīng)Handler 使用的三種方式

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

          「所以,應(yīng)用卡頓,原因一般都可以認(rèn)為是Handler處理消息太耗時導(dǎo)致的」,細(xì)分的原因可能是方法本身太耗時、算法效率低、cpu被搶占、內(nèi)存不足、IPC超時等等。

          2.2 卡頓監(jiān)控

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

          2.2.1 卡頓監(jiān)控方案一

          看下Looper 循環(huán)的注釋2和注釋4,可以找到一種卡頓監(jiān)控的方法

          Looper#loop
          public static void loop() {
             for (;;) {
                 //1、取消息
                 Message msg = queue.next(); // might block
                 ...
                 //2、消息處理前回調(diào)
                 if (logging != null) {
                     logging.println(">>>>> Dispatching to " + msg.target + " " +
                             msg.callback + ": " + msg.what);
                 }
                 ...
                 
                 //3、消息開始處理
                 msg.target.dispatchMessage(msg);// 分發(fā)處理消息
                 ...
                 
                 //4、消息處理完回調(diào)
                 if (logging != null) {
                     logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                 }
            }
            ...
          }

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

          需要注意的是,監(jiān)聽到發(fā)生卡頓之后,dispatchMessage 早已調(diào)用結(jié)束,已經(jīng)出棧,此時再去獲取主線程堆棧,堆棧中是不包含卡頓的代碼的。

          所以需要在后臺開一個線程,定時獲取主線程堆棧,「將時間點作為key,堆棧信息作為value,保存到Map中,在發(fā)生卡頓的時候,取出卡頓時間段內(nèi)的堆棧信息即可?!?/strong>

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

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

          2.2.2 卡頓監(jiān)控方案二

          對于線上卡頓監(jiān)控,需要了解「字節(jié)碼插樁」技術(shù)。

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

          偽代碼如下

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

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

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


          插樁需要注意的問題:
          1. 「避免方法數(shù)暴增」:在方法的入口和出口應(yīng)該插入相同的函數(shù),在編譯時提前給代碼中每個方法分配一個獨立的 ID 作為參數(shù)。
          2. 「過濾簡單的函數(shù)」:過濾一些類似直接 return、i++ 這樣的簡單函數(shù),并且支持黑名單配置。對一些調(diào)用非常頻繁的函數(shù),需要添加到黑名單中來降低整個方案對性能的損耗。

          微信Matrix做了大量優(yōu)化,整體包體積增加1%-2%,幀率下降2幀以內(nèi),對性能影響整體可以接受,不過依然只會在灰度包使用。

          再來說說ANR~

          三、ANR 原理

          ANR 的類型和觸發(fā)ANR的流程

          3.1 哪些場景會造成ANR呢

          • Service Timeout:比如前臺服務(wù)在20s內(nèi)未執(zhí)行完成,后臺服務(wù)是10s;
          • BroadcastQueue Timeout:比如前臺廣播在10s內(nèi)未執(zhí)行完成,后臺60s
          • ContentProvider Timeout:內(nèi)容提供者,在publish過超時10s;
          • InputDispatching Timeout: 輸入事件分發(fā)超時5s,包括按鍵和觸摸事件。

          相關(guān)超時定義可以參考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觸發(fā)流程

          來簡單分析下源碼,ANR觸發(fā)流程其實可以比喻成「埋炸彈」「拆炸彈」的過程,

          「以后臺Service為例」

          3.2.1 埋炸彈

          Context.startService`
           調(diào)用鏈如下:
           `AMS.startService `
           `ActiveServices.startService  `
           `ActiveServices.realStartServiceLocked  
          ActiveServices.realStartServiceLocked
          private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {
              ...
              //1、這里會發(fā)送delay消息(SERVICE_TIMEOUT_MSG)
              bumpServiceExecutingLocked(r, execInFg, "create");
              try {
                  ...
                  //2、通知AMS創(chuàng)建服務(wù)
                  app.thread.scheduleCreateService(r, r.serviceInfo,
                          mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo),
                          app.repProcState);
              } 
              ...
          }

          注釋1的bumpServiceExecutingLocked內(nèi)部調(diào)用scheduleServiceTimeoutLocked

           void scheduleServiceTimeoutLocked(ProcessRecord proc) {
               ...
               Message msg = mAm.mHandler.obtainMessage(
                       ActivityManagerService.SERVICE_TIMEOUT_MSG);
               msg.obj = proc;
               // 發(fā)送deley消息,前臺服務(wù)是20s,后臺服務(wù)是10s
               mAm.mHandler.sendMessageDelayed(msg,
                       proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
           }

          注釋2通知AMS啟動服務(wù)之前,注釋1處發(fā)送Handler延時消息,埋下炸彈,如果10s內(nèi)(前臺服務(wù)是20s)沒人來拆炸彈,炸彈就會爆炸,即ActiveServices#serviceTimeout方法會被調(diào)用

          3.2.2 拆炸彈

          啟動一個Service,先要經(jīng)過AMS管理,然后AMS會通知應(yīng)用進(jìn)程執(zhí)行Service的生命周期, ActivityThreadhandleCreateService方法會被調(diào)用

          -> 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調(diào)用
                   service.onCreate();
                   mServices.put(data.token, service);
                   try {
                    //2、拆炸彈在這里
                       ActivityManager.getService().serviceDoneExecuting(
                               data.token, SERVICE_DONE_EXECUTING_ANON, 00);
                   } catch (RemoteException e) {
                       throw e.rethrowFromSystemServer();
                   }
               }
           }

          注釋1,ServiceonCreate方法被調(diào)用, 注釋2,調(diào)用AMS的serviceDoneExecuting方法,最終會調(diào)用到ActiveServices. serviceDoneExecutingLocked

          private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying,
                        boolean finishing)
           
          {
          ...
           //移除delay消息
           mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
          ...
           }

          可以看到,onCreate方法調(diào)用完之后,就會移除delay消息,炸彈被拆除。

          3.2.3 引爆炸彈

          假設(shè)Service的onCreate執(zhí)行超過10s,那么炸彈就會引爆,也就是

          ActiveServices#serviceTimeout方法會被調(diào)用

           void serviceTimeout(ProcessRecord proc) {

           ...
           if (anrMessage != null) {
                   mAm.mAppErrors.appNotResponding(proc, nullnullfalse, anrMessage);
               }
           ...
           }

          所有ANR,最終都會調(diào)用AppErrorsappNotResponding方法

          AppErrors #appNotResponding
          final void appNotResponding(ProcessRecord app, ActivityRecord activity,
                  ActivityRecord parent, boolean aboveSystem, final String annotation)
           
          {
                ...
                
                //1、寫入event log
                // Log the ANR to the event log.
                EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
                          app.processName, app.info.flags, annotation);
                 ...
                //2、收集需要的log,anr、cpu等,StringBuilder憑借
                // Log the ANR to the main log.
                StringBuilder info = new StringBuilder();
                info.setLength(0);
                info.append("ANR in ").append(app.processName);
                if (activity != null && activity.shortComponentName != null) {
                    info.append(" (").append(activity.shortComponentName).append(")");
                }
                info.append("\n");
                info.append("PID: ").append(app.pid).append("\n");
                if (annotation != null) {
                    info.append("Reason: ").append(annotation).append("\n");
                }
                if (parent != null && parent != activity) {
                    info.append("Parent: ").append(parent.shortComponentName).append("\n");
                }

                ProcessCpuTracker processCpuTracker = new ProcessCpuTracker(true);

               ...
              // 3、dump堆棧信息,包括java堆棧和native堆棧,保存到文件中
              // For background ANRs, don't pass the ProcessCpuTracker to
              // avoid spending 1/2 second collecting stats to rank lastPids.
              File tracesFile = ActivityManagerService.dumpStackTraces(
                      true, firstPids,
                      (isSilentANR) ? null : processCpuTracker,
                      (isSilentANR) ? null : lastPids,
                      nativePids);

              String cpuInfo = null;
              ...

              //4、輸出ANR 日志
              Slog.e(TAG, info.toString());
              if (tracesFile == null) {
                   // 5、沒有抓到tracesFile,發(fā)一個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,直接殺進(jìn)程
                  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,會調(diào)用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,會發(fā)一個SIGNAL_QUIT信號,這里看注釋是會觸發(fā)收集線程堆棧信息流程,寫入traceFile 6、輸出到drapbox 7、后臺ANR,直接殺進(jìn)程 8、錯誤報告 9、彈出ANR dialog,會調(diào)用 AppErrors#handleShowAnrUi方法。

          ANR觸發(fā)流程小結(jié)

          ?

          ANR觸發(fā)流程,可以比喻為埋炸彈和拆炸彈的過程, 以啟動Service為例,Service的onCreate方法調(diào)用之前會使用Handler發(fā)送延時10s的消息,Service 的onCreate方法執(zhí)行完,會把這個延時消息移除掉。假如Service的onCreate方法耗時超過10s,延時消息就會被正常處理,也就是觸發(fā)ANR,會收集cpu、堆棧等信息,彈ANR Dialog。

          ?

          service、broadcast、provider 的ANR原理都是「埋定時炸彈和拆炸彈」原理,

          但是input的超時檢測機(jī)制稍微有點不同,需要等收到下一次input事件,才會去檢測上一次input事件是否超時,input事件里埋的炸彈是普通炸彈,需要通過「掃雷」來排查。

          具體可以參考:徹底理解安卓應(yīng)用無響應(yīng)機(jī)制

          四、ANR 分析方法

          上面已經(jīng)分析了ANR觸發(fā)流程,最終會把發(fā)生ANR時的線程堆棧、cpu等信息保存起來,我們一般都是分析 「/data/anr/traces.txt」 文件

          4.1 模擬死鎖導(dǎo)致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")
                  }
              }
          }

          觸發(fā)ANR之后,一般我們會拉取anr日志:「adb pull /data/traces.txt」(文件名可能是anr_xxx.txt)

          4.2 分析ANR 文件

          首先看主線程,搜索 main

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

          id為22的線程是Blocked狀態(tài),正在等待一個鎖,這個鎖被id為1的線程持有,同時這個22號線程還持有一個鎖,這個鎖是主線程想要的。

          通過ANR日志,可以很清楚分析出這個ANR是死鎖導(dǎo)致的,并且有具體堆棧信息。

          上面只是舉例一種「死鎖導(dǎo)致ANR」的情況,實際項目中,可能有很多情況會導(dǎo)致ANR,例如「內(nèi)存不足、CPU被搶占、系統(tǒng)服務(wù)沒有及時響應(yīng)」等等。

          如果是線上問題,怎么樣才能拿到ANR日志呢?

          五、ANR 監(jiān)控

          前面已經(jīng)分析了ANR觸發(fā)流程,以及常規(guī)的線下分析方法,看起來還是有點繁瑣的,需要pull出anr日志,然后分析線程堆棧等信息。對于線上ANR,如何搭建一個完善的ANR監(jiān)控系統(tǒng)呢?

          下面將介紹ANR監(jiān)控的方式

          5.1 抓取系統(tǒng)traces.txt 上傳

          1、當(dāng)監(jiān)控線程發(fā)現(xiàn)主線程卡死時,主動向系統(tǒng)發(fā)送SIGNAL_QUIT信號。2、等待/data/anr/traces.txt文件生成。3、文件生成以后進(jìn)行上報。

          這個方案在 《手Q Android線程死鎖監(jiān)控與自動化分析實踐》 https://cloud.tencent.com/developer/article/1064396 這篇文章中有詳細(xì)介紹,

          看起來好像可行,不過有以下兩個問題:1、traces.txt 里面包含所有線程的信息,上傳之后需要人工過濾分析 2、很多高版本系統(tǒng)需要root權(quán)限才能讀取 /data/anr這個目錄

          既然這個方案存在問題,那么可還有其它辦法?

          5.2 ANRWatchDog

          ANRWatchDog 是一個自動檢測ANR的開源庫

          5.2.1 ANRWatchDog 原理

          其源碼只有兩個類,核心是ANRWatchDog這個類,繼承自Thread,它的run 方法如下,看注釋處

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

               long interval = _timeoutInterval;
              // 1、開啟循環(huán)
               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 標(biāo)志,正常情況下_tick 已經(jīng)被主線程改為0,_reported改為false,如果不是,說明 2 的主線程Runnable一直沒有被執(zhí)行,主線程卡住了
                   if (_tick != 0 && !_reported) {
                       ...
                       if (_namePrefix != null) {
                           // 5、判斷發(fā)生ANR了,那就獲取堆棧信息,回調(diào)onAppNotResponding方法
                           error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
                       } else {
                           error = ANRError.NewMainOnly(_tick);
                       }
                       _anrListener.onAppNotResponding(error);
                       interval = _timeoutInterval;
                       _reported = true;
                   }
               }
           }

          ANRWatchDog 的原理是比較簡單的,概括為以下幾個步驟

          1. 開啟一個線程,死循環(huán),循環(huán)中睡眠5s
          2. 往UI線程post 一個Runnable,將_tick 賦值為0,將 _reported 賦值為false
          3. 線程睡眠5s之后檢查_tick和_reported字段是否被修改
          4. 如果_tick和_reported沒有被修改,說明給主線程post的Runnable一直沒有被執(zhí)行,也就說明主線程卡頓至少5s**(只能說至少,這里存在5s內(nèi)的誤差)**。
          5. 將線程堆棧信息輸出

          其中涉及到并發(fā)的一個知識點,關(guān)于  volatile 關(guān)鍵字的使用,面試中的常客, volatile的特點是:保證可見性,禁止指令重排,適合在一個線程寫,其它線程讀的情況?!?/strong>

          面試中一般會展開問JMM,工作內(nèi)存,主內(nèi)存等,以及為什么要有工作內(nèi)存,能不能所有字段都用 volatile 關(guān)鍵字修飾等問題。

          關(guān)于面試中的并發(fā)的各種問題,可以參考之前的一篇文章:面試官:說說多線程并發(fā)問題

          回到ANRWatchDog本身,細(xì)心的同學(xué)可能會發(fā)現(xiàn)一個問題,使用ANRWatchDog有時候會捕獲不到ANR,是什么原因呢?

          5.2.2 ANRWatchDog 缺點

          ANRWatchDog 會出現(xiàn)漏檢測的情況,看圖

          ANRWatchDog漏檢測

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

          1. 假設(shè)主線程卡頓了2s之后,ANRWatchDog這時候剛開始一輪循環(huán),將_tick 賦值為5,并往主線程post一個任務(wù),把_tick修改為0
          2. 主線程過了3s之后不卡頓了,將_tick賦值為0
          3. 等到ANRWatchDog睡眠5s之后,發(fā)現(xiàn)_tick的值是0,判斷為沒有發(fā)生ANR。而實際上,主線程中間是卡頓了5s,ANRWatchDog誤差是在5s之內(nèi)的(5s是默認(rèn)的,線程的睡眠時長)

          針對這個問題,可以做一下優(yōu)化。

          5.3 ANRMonitor

          ANRWatchDog 漏檢測的問題,根本原因是因為線程睡眠5s,不知道前一秒主線程是否已經(jīng)出現(xiàn)卡頓了,「如果改成每間隔1秒檢測一次,就可以把誤差降低到1s內(nèi)」。

          接下來通過改造ANRWatchDog ,來做一下優(yōu)化,命名為「ANRMonitor」。

          我們想讓子線程間隔1s執(zhí)行一次任務(wù),可以通過 HandlerThread來實現(xiàn)

          流程如下:

          核心的Runnable代碼

           @Volatile
           var mainHandlerRunEnd = true
           
           //子線程會間隔1s調(diào)用一次這個Runnable
           private val mThreadRunnable = Runnable {
               
               blockTime++
               //1、標(biāo)志位 mainHandlerRunEnd 沒有被主線程修改,說明有卡頓
               if (!mainHandlerRunEnd && !isDebugger()) {
                   logw(TAG, "mThreadRunnable: main thread may be block at least $blockTime s")
               }

               //2、卡頓超過5s,觸發(fā)ANR流程,打印堆棧
               if (blockTime >= 5) {
                   if (!mainHandlerRunEnd && !isDebugger() && !mHadReport) {
                       mHadReport = true
                       //5s了,主線程還沒更新這個標(biāo)志,ANR
                       loge(TAG, "ANR->main thread may be block at least $blockTime s ")
                       loge(TAG, getMainThreadStack())
                       //todo 回調(diào)出去,這里可以按需把其它線程的堆棧也輸出
                       //todo debug環(huán)境可以開一個新進(jìn)程,彈出堆棧信息
                   }
               }

               //3、如果上一秒沒有卡頓,那么重置標(biāo)志位,然后讓主線程去修改這個標(biāo)志位
               if (mainHandlerRunEnd) {
                   mainHandlerRunEnd = false
                   mMainHandler.post {
                    mainHandlerRunEnd = true
                   }
                   
               }

             //子線程間隔1s調(diào)用一次mThreadRunnable
               sendDelayThreadMessage()
           }
          1. 子線程每隔1s會執(zhí)行一次mThreadRunnable,檢測標(biāo)志位 mainHandlerRunEnd 是否被修改
          2. 假如mainHandlerRunEnd如期被主線程修改為true,那么重置mainHandlerRunEnd標(biāo)志位為false,然后繼續(xù)執(zhí)行步驟1
          3. 假如mainHandlerRunEnd沒有被修改true,說明有卡頓,累計卡頓5s就觸發(fā)ANR流程

          在監(jiān)控到ANR的時候,除了獲取主線程堆棧,還有cpu、內(nèi)存占用等信息也是比較重要的,demo中省略了這部分內(nèi)容。

          5.3.1 測試ANR


          5.3.2 ANR檢測結(jié)果

          logcat打印所示

          主線程卡頓超過5s,會打堆棧信息,如果是卡頓1-5s內(nèi),會有warning的log 提示,線下可以做成彈窗或者toast提示,

          「看到這里,大家應(yīng)該能想到,線下也可以用這種方法檢測卡頓,定位到耗時的代碼?!?/strong>

          此方案可以結(jié)合ProcessLifecycleOwner,應(yīng)用在前臺才開啟檢測,進(jìn)入后臺則停止檢測。

          六、死鎖監(jiān)控

          在發(fā)生ANR的時候,有時候只有主線程堆棧信息可能還不夠,例如發(fā)生死鎖的情況,「需要知道當(dāng)前線程在等待哪個鎖,以及這個鎖被哪個線程持有」,然后把發(fā)生死鎖的線程堆棧信息都收集到。

          流程如下:

          1. 獲取當(dāng)前blocked狀態(tài)的線程
          2. 獲取該線程想要競爭的鎖
          3. 獲取該鎖被哪個線程持有
          4. 通過關(guān)系鏈,判斷死鎖的線程,輸出堆棧信息

          在Java層并沒有相關(guān)API可以實現(xiàn)死鎖監(jiān)控,可以從Native層入手。

          6.1 獲取當(dāng)前blocked狀態(tài)的線程

          這個比較簡單,一個for循環(huán)就搞定,不過我們要的線程id是native層的線程id,Thread 內(nèi)部有一個native線程地址的字段叫 nativePeer,通過反射可以獲取到。

                  Thread[] threads = getAllThreads();
                  for (Thread thread : threads) {
                      if (thread.getState() == Thread.State.BLOCKED) {
                          long threadAddress = (long) ReflectUtil.getField(thread, "nativePeer");
                          // 找不到地址,或者線程已經(jīng)掛了,此時獲取到的可能是0和-1
                          if (threadAddress <= 0) {
                              continue;
                     }
                    ...后續(xù)
                      }
                  }

          有了native層線程地址,還需要找到native層相關(guān)函數(shù)

          6.2 獲取當(dāng)前線程想要競爭的鎖

          從ART 源碼可以找到這個函數(shù) androidxref.com/8.0.0_r4/xr…

          函數(shù):「Monitor::GetContendedMonitor」

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

          順便說說Monitor以及Java對象結(jié)構(gòu)

          Monitor

          「Monitor是一種并發(fā)控制機(jī)制」,提供多線程環(huán)境下的互斥和同步,以支持安全的并發(fā)訪問。

          Monitor由以下3個元素組成:

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

          感興趣可以參考這一篇文章 說一說管程(Monitor)及其在Java synchronized機(jī)制中的體現(xiàn)

          Java的Class對象

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

          1. 對象頭:MarkWord和對象指針

            ?

            「MarkWord(標(biāo)記字段)」:保存哈希碼、分代年齡、「鎖標(biāo)志位」、偏向線程ID、偏向時間戳等信息 「對象指針」:即指向當(dāng)前對象的類的元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例。

            ?
          2. 實例數(shù)據(jù):對象實際的數(shù)據(jù)

          3. 對齊填充:按8字節(jié)對齊(JVM自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍)。例如Integer對象,對象頭MarkWord和對象指針分別占用4字節(jié),實例數(shù)據(jù)4字節(jié),那么對齊填充就是4字節(jié),Integer占用內(nèi)存是int的4倍。


          回到 GetContendedMonitor 函數(shù),我們可以通過打開動態(tài)庫libart.so,然后使用dlsym獲取函數(shù)的符號地址,然后就可以進(jìn)行調(diào)用了。

          由于Android 7.0開始,系統(tǒng)限制App中調(diào)用dlopendlsym等函數(shù)打開系統(tǒng)動態(tài)庫,我們可以使用 ndk_dlopen這個庫來繞過這個限制

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

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

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

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

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

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

          6.3 獲取目標(biāo)鎖被哪個線程持有

          「函數(shù):Monitor::GetLockOwnerThreadId」

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

          // Monitor::GetLockOwnerThreadId
          //這個函數(shù)是用來獲取 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函數(shù)符號有變化,所以需要通過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 源碼中這個函數(shù)符號變了,需要自行查閱
                  return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
              }
          }

          到此,就得到了兩個函數(shù)符號地址,接下來就把blocked狀態(tài)的native線程id傳過去,調(diào)用就行了

          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、調(diào)用一下獲取monitor的函數(shù),返回當(dāng)前線程想要競爭的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. 獲取當(dāng)前線程要競爭的鎖
          2. 獲取這個鎖被哪個線程持有

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

          6.5 通過算法,找到死鎖

          前面已經(jīng)知道當(dāng)前blocked狀態(tài)的線程id(還需要轉(zhuǎn)換成native線程id),以及這個blocked線程在等待哪個線程釋放鎖,也就是得到關(guān)系鏈:

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

          如何判斷有死鎖?我們可以用Map來保存對應(yīng)關(guān)系

          map[A]=B

          map[B]=A

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

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

          6.6 死鎖監(jiān)控小結(jié)

          死鎖監(jiān)控原理還是比較清晰的:

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

          由于死鎖監(jiān)控涉及到native層代碼,對于很多應(yīng)用層開發(fā)的同學(xué)來說可能有點難度,

          「但是正因為有難度,我們?nèi)チ私?,去學(xué)習(xí),并且掌握了,才能在眾多競爭者中脫穎而出?!?/strong>

          七、形成閉環(huán)

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

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

          八、總結(jié)

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

          本文以卡頓作為切入點

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

          本文源碼:

          ANRMonitorDemo

          參考文章:

          • 線程監(jiān)控 - 死鎖、 存活周期與 CPU 占用率https://www.jianshu.com/p/a4efccd09e02
          • 《手Q Android線程死鎖監(jiān)控與自動化分析實踐》 https://cloud.tencent.com/developer/article/1064396
          • 說一說管程(Monitor)及其在Java synchronized機(jī)制中的體現(xiàn) https://www.jianshu.com/p/e624460c645c
          • 圖解 | 深入揭秘 epoll 是如何實現(xiàn) IO 多路復(fù)用的
          • 理解Android ANR的觸發(fā)原理 http://gityuan.com/2016/07/02/android-anr/
          • ANRWatchDoghttps://github.com/SalomonBrys/ANR-WatchDog/
          • [Android開發(fā)高手課 06 | 卡頓優(yōu)化(下):如何監(jiān)控應(yīng)用卡頓?]


          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    www国产亚洲精品久久网站 | 青娱乐成人分类视频 | 欧美后门菊门交3p视频在线观看 | 无码视频高清在线不卡 | 美国中文字幕在线 |