Android卡顿检测方案

ty647226 7年前
   <p>应用的流畅度最直接的影响了App的用户体验,轻微的卡顿有时导致用户的界面操作需要等待一两秒钟才能生效,严重的卡顿则导致系统直接弹出ANR的提示窗口,让用户选择要继续等待还是关闭应用。</p>    <p><img src="https://simg.open-open.com/show/76212791f4ab428d7b159562e2fc2b51.png"></p>    <p>所以,如果想要提升用户体验,就需要尽量避免卡顿的产生,否则用户经历几次类似场景之后,只会动动手指卸载应用,再顺手到应用商店给个差评。关于卡顿的分析方案,已经有以下两种:</p>    <ul>     <li>分析trace文件。通过分析系统的/data/anr/traces.txt,来找到导致UI线程阻塞的源头,这种方案比较适合开发过程中使用,而不适合线上环境;</li>     <li>使用BlockCanary开源方案。其原理是利用Looper中的loop输出的>>>>> Dispatching to和<<<<< Finished to这样的log,这种方案适合开发过程和上线的时候使用,但也有个弊端,就是如果系统移除了前面两个log,检测可能会面临失效;</li>    </ul>    <p>下面就开始说本文要提及的卡顿检测实现方案,原理简单,代码量也不多,只有BlockLooper和BlockError两个类。</p>    <h2>基本使用</h2>    <p>在Application中调用BlockLooper.initialize进行一些参数初始化,具体参数项可以参照BlockLooper中的Configuration静态内部类,当发生卡顿时,则会在回调(非UI线程中)OnBlockListener。</p>    <pre>  <code class="language-java">public class AndroidPerformanceToolsApplicationextends Application{        private final static String TAG = AndroidPerformanceToolsApplication.class.getSimpleName();        @Override      public void onCreate(){          super.onCreate();    // 初始化相关配置信息          BlockLooper.initialize(new BlockLooper.Builder(this)                  .setIgnoreDebugger(true)                  .setReportAllThreadInfo(true)                  .setSaveLog(true)                  .setOnBlockListener(new BlockLooper.OnBlockListener() {//回调在非UI线程                      @Override                      public void onBlock(BlockError blockError){                          blockError.printStackTrace();//把堆栈信息输出到控制台                      }                  })                  .build());      }  }  </code></pre>    <p>在选择要启动(停止)卡顿检测的时候,调用对应的API</p>    <pre>  <code class="language-java">BlockLooper.getBlockLooper().start();//启动检测  BlockLooper.getBlockLooper().stop();//停止检测  </code></pre>    <p>使用上很简单,接下来看一下效果演示和源码实现。</p>    <h2>效果演示</h2>    <p>制造一个UI阻塞效果</p>    <p><img src="https://simg.open-open.com/show/ffd144f0076ea393f641124dc4037d18.gif"></p>    <p>看看AS控制台输出的整个堆栈信息</p>    <p><img src="https://simg.open-open.com/show/3d931347d0705057b89bf329f620f97f.png"></p>    <p>定位到对应阻塞位置的源码</p>    <p><img src="https://simg.open-open.com/show/0c8fc437b0f05f824679e29a275484c5.png"></p>    <p>当然,对线程的信息BlockLooper也不仅输出到控制台,也会帮你缓存到SD上对应的应用缓存目录下,在SD卡上的/Android/data/对应App包名/cache/block/下可以找到,文件名是发生卡顿的时间点,后缀是trace。</p>    <p><img src="https://simg.open-open.com/show/c6f2664f0d4e6cbc517c5ed7fcfab740.gif"></p>    <h2>源码解读</h2>    <p>当App在5s内无法对用户做出的操作进行响应时,系统就会认为发生了ANR。BlockLooper实现上就是利用了这个定义,它继承了Runnable接口,通过initialize传入对应参数配置好后,通过BlockLooper的start()创建一个Thread来跑起这个Runnable,在没有stop之前,BlockLooper会一直执行run方法中的循环,执行步骤如下:</p>    <ul>     <li>Step1. 判断是否停止检测UI线程阻塞,未停止则进入Step2;</li>     <li>Step2. 使用uiHandler不断发送ticker这个Runnable,ticker会对tickCounter进行累加;</li>     <li>Step3. BlockLooper进入指定时间的sleep(frequency是在initialize时传入,最小不能低于5s);</li>     <li>Step4. 如果UI线程没有发生阻塞,则sleep过后,tickCounter一定与原来的值不相等,否则一定是UI线程发生阻塞;</li>     <li>Step5. 发生阻塞后,还需判断是否由于Debug程序引起的,不是则进入Step6;</li>     <li>Step6. 回调OnBlockListener,以及选择保存当前进程中所有线程的堆栈状态到SD卡等;</li>    </ul>    <pre>  <code class="language-java">public class BlockLooperimplements Runnable{        ...   private Handler uiHandler = new Handler(Looper.getMainLooper());   private Runnable ticker = new Runnable() {          @Override          public void run(){              tickCounter = (tickCounter + 1) % Integer.MAX_VALUE;          }      };        ...        private void init(Configuration configuration){          this.appContext = configuration.appContext;          this.frequency = configuration.frequency < DEFAULT_FREQUENCY ? DEFAULT_FREQUENCY : configuration.frequency;          this.ignoreDebugger = configuration.ignoreDebugger;          this.reportAllThreadInfo = configuration.reportAllThreadInfo;          this.onBlockListener = configuration.onBlockListener;          this.saveLog = configuration.saveLog;      }        @Override      public void run(){          int lastTickNumber;          while (!isStop) { //Step1              lastTickNumber = tickCounter;              uiHandler.post(ticker); //Step2                try {                  Thread.sleep(frequency); //Step3              } catch (InterruptedException e) {                  e.printStackTrace();                  break;              }                if (lastTickNumber == tickCounter) { //Step4                  if (!ignoreDebugger && Debug.isDebuggerConnected()) { //Step5                      Log.w(TAG, "当前由调试模式引起消息阻塞引起ANR,可以通过setIgnoreDebugger(true)来忽略调试模式造成的ANR");                      continue;                  }                    BlockError blockError; //Step6                  if (!reportAllThreadInfo) {                      blockError = BlockError.getUiThread();                  } else {                      blockError = BlockError.getAllThread();                  }                    if (onBlockListener != null) {                      onBlockListener.onBlock(blockError);                  }                    if (saveLog) {                      if (StorageUtils.isMounted()) {                          File logDir = getLogDirectory();                          saveLogToSdcard(blockError, logDir);                      } else {                          Log.w(TAG, "sdcard is unmounted");                      }                  }              }            }      }     ...        public synchronized void start(){          if (isStop) {              isStop = false;              Thread blockThread = new Thread(this);              blockThread.setName(LOOPER_NAME);              blockThread.start();          }      }        public synchronized void stop(){          if (!isStop) {              isStop = true;          }      }     ...   ...  }  </code></pre>    <p>介绍完BlockLooper后,再简单说一下BlockError的代码,主要有getUiThread和getAllThread两个方法,分别用户获取UI线程和进程中所有线程的堆栈状态信息,当捕获到BlockError时,会在OnBlockListener中以参数的形式传递回去。</p>    <pre>  <code class="language-java">public class BlockErrorextends Error{        private BlockError(ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo){          super("BlockLooper Catch BlockError", threadStackInfo);      }          public staticBlockErrorgetUiThread(){          Thread uiThread = Looper.getMainLooper().getThread();          StackTraceElement[] stackTraceElements = uiThread.getStackTrace();          ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(uiThread), stackTraceElements)                  .new ThreadStackInfo(null);          return new BlockError(threadStackInfo);      }          public staticBlockErrorgetAllThread(){          final Thread uiThread = Looper.getMainLooper().getThread();          Map<Thread, StackTraceElement[]> stackTraceElementMap = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {              @Override              public int compare(Thread lhs, Thread rhs){                  if (lhs == rhs) {                      return 0;                  } else if (lhs == uiThread) {                      return 1;                  } else if (rhs == uiThread) {                      return -1;                  }                  return rhs.getName().compareTo(lhs.getName());              }          });            for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {              Thread key = entry.getKey();              StackTraceElement[] value = entry.getValue();              if (value.length > 0) {                  stackTraceElementMap.put(key, value);              }          }            //Fix有时候Thread.getAllStackTraces()不包含UI线程的问题          if (!stackTraceElementMap.containsKey(uiThread)) {              stackTraceElementMap.put(uiThread, uiThread.getStackTrace());          }            ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = null;          for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraceElementMap.entrySet()) {              Thread key = entry.getKey();              StackTraceElement[] value = entry.getValue();              threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(key), value).                      new ThreadStackInfo(threadStackInfo);          }            return new BlockError(threadStackInfo);        }     ...    }  </code></pre>    <p> </p>    <p>来自:http://blog.coderclock.com/2017/06/04/android/AndroidPerformanceTools-BlockLooper/</p>    <p> </p>