首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android 性能采集之Fps,Memory,Cpu

Android 性能采集之Fps,Memory,Cpu

作者头像
逮虾户
发布于 2020-11-05 06:59:27
发布于 2020-11-05 06:59:27
1.5K00
代码可运行
举报
文章被收录于专栏:逮虾户逮虾户
运行总次数:0
代码可运行

背景

各位大佬好久不见了,憋了一阵子发育了一下(主要是我在拼神龙斗士),基本上完成了简单的性能采集的Demo,分享一下相关的经验给各位吧。

APM(Application perfmance monitor)就是应用性能监控。在移动互联网对人们生活影响越来越大的今天,App的功能越来越全面,随之而来的就是App性能的要求越来越高,不能被动的等待用户异常的发生然后根据线上日志去修复bug,再发补丁版本。主动监控App性能变得越来越重要,分析App的耗电,UI卡顿,网络性能成为了当物之急。

当前项目的Apm开发参考了腾讯的Matrix,360的ArgusAPM,滴滴的Dokit,还有一些细小的项目等等。根据项目进行定制,之后完成自己的Apm采集系统。

后续文章会根据我当前的开发进度缓慢更新,大家可以跟着我这个小菜鸡缓慢前行,当前完成了这三个性能指标的采集工作,后续可能还会添加线程FD信息,所以本文就会着重分析这三个点。

先抛出问题

  1. 一个性能数据采集系统,你不能成为一个app的负担,不能在采集的时候耽误主线程的渲染,接入了Apm之后反倒让App变得更加卡顿。
  2. 由于Fps,内存,Cpu等都是需要频繁采样的,比如Fps,一秒钟刷新60帧,如果全量数据上报,那么后端大佬可能就把我给打死了。
  3. 在业务最少介入的情况下完成关键页面数据的收集,以及将页面数据和性能数据进行绑定。

Fps采集

首先我们还是要先介绍下什么是Fps.

流畅度,是页面在滑动、渲染等过程中的体验。Android系统要求每一帧都要在 16ms 内绘制完成,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和 CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。如果没有在期间完成渲染秒就会发生掉帧。掉帧是用户体验中一个非常核心的问题。丢弃了当前帧,并且之后不能够延续之前的帧率,这种不连续的间隔会容易会引起用户的注意,也就是我们常说的卡顿、不流畅。

那么是不是1s只要绘制了60帧是不是就是流畅的呢?也不一定,如果发生抖动的情况,那么肯定会有其中几帧是有问题的。其中肯定会有最大绘制帧,和最小绘制帧的情况,所以平均值,最大值最小值都是我们需要知道的。

在讨论采集之前,我们要先简单的说下两个东西ChoreographerLooperPrinter

Choreographer(编舞者)

Choreographer中文翻译过来是"舞蹈指挥",字面上的意思就是优雅地指挥以上三个UI操作一起跳一支舞。这个词可以概括这个类的工作,如果android系统是一场芭蕾舞,他就是Android UI显示这出精彩舞剧的编舞,指挥台上的演员们相互合作,精彩演出。Google的工程师看来挺喜欢舞蹈的!

其中关于Choreographer的介绍相关的可以参考这篇文章,面试中也经常会问到这个问题,ViewRootImp和Vsync等等,但是我们还是比较专注于数据的采集和上报,所以关注的重点还是有点不同。

一般常规的Fps采集可以通过Choreographer既UI线程绘制的编舞者,Choreographer是一个ThreadLocal的单例,接收vsync信号进行界面的渲染,我们只要对其添加一个CallBack,就可以巧妙的计算出这一帧的绘制时长。

Matrix对于核心Choreographer是对CallbackQueue的hook,通过hook addCallbackLocked分别在不同类型的回调队列的头部添加自定义的FrameCallback。这样 系统CALLBACK_INPUT = 自定义CALLBACK_ANIMATION的开始时间-自定义CALLBACK_INPUT的完成时间 系统CALLBACK_ANIMATION = 自定义CALLBACK_TRAVERSAL开始时间-自定义CALLBACK_ANIMATION开始时间 系统CALLBACK_TRAVERSAL = msg dipatch结束时间- 自定义CALLBACK_TRAVERSAL开始时间

LooperPrinter

首先我们先要有个概念,所有的View相关的和生命周期相关的都被执行在主线程上。那么我们有没有方法可以监控到主线程耗时呢?

我们盘一盘Handler是如何执行的,首先LooperMessageQueue中获取到Message,之后判断Message内部的Handler或者Runnable来决定执行后续的操作。

ActivityThread分析,所有的主线程操作的被执行在主线程的Looper上,那么我们是不是只要在主线程的Looperloop方法,获取到Message的前后执行加上写代码就能监控到一个Message 被执行的时长了呢。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void loop() {
      final Looper me = myLooper();
      if (me == null) {
          throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
      }
      final MessageQueue queue = me.mQueue;

      // Make sure the identity of this thread is that of the local process,
      // and keep track of what that identity token actually is.
      Binder.clearCallingIdentity();
      final long ident = Binder.clearCallingIdentity();

      // Allow overriding a threshold with a system prop. e.g.
      // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
      final int thresholdOverride =
              SystemProperties.getInt("log.looper."
                      + Process.myUid() + "."
                      + Thread.currentThread().getName()
                      + ".slow", 0);

      boolean slowDeliveryDetected = false;

      for (;;) {
          Message msg = queue.next(); // might block
          if (msg == null) {
              // No message indicates that the message queue is quitting.
              return;
          }

          // This must be in a local variable, in case a UI event sets the logger
          final Printer logging = me.mLogging;
          if (logging != null) {
              logging.println(">>>>> Dispatching to " + msg.target + " " +
                      msg.callback + ": " + msg.what);
          }
          ...

          if (logging != null) {
              logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
          }
          ...
      }
  }

复制代码

从源码上我们可以看到Looper一开始就预留了一个Printer的类,其中在Message执行开始和Message执行结束之后都会执行Printer方法。我们可以通过 looper.setMessageLogging(new LooperPrinter());的方法来设置这个监控。

IdleHandler

Looper中的MessageQueue为空的情况下,会触发IdleHandler,所以主线程卡顿,一般都会配合这个一起来重置耗时时间,这样就能保证主线程空置的情况下,方法耗时不会计算出错。

UIThreadMonitor(主线程监控)

简单的介绍了下上面几个东西之后,我们的Fps采集的这部分实际的采样代码我参考了下MatrixUIThreadMonitor,而UIThreadMonitor则是通过上述几个组合的方式来完成的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   private void dispatchEnd() {
        long traceBegin = 0;
        if (config.isDevEnv()) {
            traceBegin = System.nanoTime();
        }
        long startNs = token;
        long intendedFrameTimeNs = startNs;
        if (isVsyncFrame) {
            doFrameEnd(token);
            intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);
        }

        long endNs = System.nanoTime();

        synchronized (observers) {
            for (LooperObserver observer : observers) {
                if (observer.isDispatchBegin()) {
                    observer.doFrame(AppMethodBeat.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
                }
            }
        }

        dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
        dispatchTimeMs[1] = System.nanoTime();

        AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);

        synchronized (observers) {
            for (LooperObserver observer : observers) {
                if (observer.isDispatchBegin()) {
                    observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isVsyncFrame);
                }
            }
        }
        this.isVsyncFrame = false;

        if (config.isDevEnv()) {
            MatrixLog.d(TAG, "[dispatchEnd#run] inner cost:%sns", System.nanoTime() - traceBegin);
        }
    }
复制代码

UIThreadMonitor则不太一样,其中dispatchEnd方法有其中LooperMonitor所接受到的。

LooperMonitor他通过主线程的LoopersetMessageLogging方法设置一个LooperPrinterdispatchEnd在主线程的方法执行结束之后,通过反射Choreographer获取当前的绘制的Vsync和渲染时长。最后当IdleHandler被触发的时候,则重置LooperPrinter时间的方式,从而避免主线程闲置状况下方法耗时计算出问题。

这部分源代的传送门Matrix LoopMonitor

为什么要绕一个大圈子来监控Fps呢?这么写的好处是什么呢?我特地去翻查了下Matrix官方的wiki,Martix参考了BlockCanary的代码,通过结合了下ChoreographerBlockCanary,当出现卡顿帧的时候获取当前的主线程卡顿的堆栈,然后通过LooperPrinter把当前的卡顿的堆栈方法输出,这样可以更好的辅助开发去定位卡顿问题,而不是直接告诉业务方你的页面卡顿了。

采样分析

文章开始抛出过一个问题,如果采集的每一个数据都上报首先会对服务器产生巨大的无效数据压力,其次也会有很多无效的数据上报,那么应该怎么做呢?

这一块我们参考了Matrix的代码,首先Fps数据不可能是实时上报的,其次最好能从一个时间段内的数据中筛选出有问题的数据,Matrix的Fps采集的有几个小细节其实做的很好。

  1. 延迟200毫秒.先收集200帧的数据,然后对其数据内容进行分析,筛选遍历出最大帧最小帧,以及平均帧,之后内存保存数据。
  2. 子线程处理数据,筛选遍历的操作移动到子线程,这样避免APM反倒造成App卡顿问题。
  3. 200毫秒的数据只是作为其中一个数据片段,Matrix的上报节点是以一个更长的时间段作为上报的,当时间超过1分钟左右的情况下,才会作为一个Issue片段上报。
  4. 前后台切换状态并不需要采集数据。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
                                final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
        long traceBegin = System.currentTimeMillis();
        try {
            final long jiter = endNs - intendedFrameTimeNs;
            final int dropFrame = (int) (jiter / frameIntervalNs);
            droppedSum += dropFrame;
            durationSum += Math.max(jiter, frameIntervalNs);

            synchronized (listeners) {
                for (final IDoFrameListener listener : listeners) {
                    if (config.isDevEnv()) {
                        listener.time = SystemClock.uptimeMillis();
                    }
                    if (null != listener.getExecutor()) {
                        if (listener.getIntervalFrameReplay() > 0) {
                            listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                    intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                        } else {
                            listener.getExecutor().execute(new Runnable() {
                                @Override
                                public void run() {
                                    listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                                }
                            });
                        }
                    } else {
                        listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                    }

                   ...
                }
            }
        }
    }

复制代码

上面是Matirx的源代码,其中我们可以看出listener.getIntervalFrameReplay() > 0当这个条件触发的情况下,listener会先做一次collection操作,当触发到一定的数据量之后,才会触发后续的逻辑。其次我们可以看到判断了null != listener.getExecutor(),所以这部分收集的操作被执行在线程池中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 private class FPSCollector extends IDoFrameListener {

        private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());

        Executor executor = new Executor() {
            @Override
            public void execute(Runnable command) {
                frameHandler.post(command);
            }
        };

        private HashMap map = new HashMap<>();

        @Override
        public Executor getExecutor() {
            return executor;
        }

        @Override
        public int getIntervalFrameReplay() {
            return 200;
        }

        @Override
        public void doReplay(List list) {
            super.doReplay(list);
            for (FrameReplay replay : list) {
                doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame,
                        replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs);
            }
        }

        public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,
                                  boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,
                                  long animationCostNs, long traversalCostNs) {

            if (Utils.isEmpty(visibleScene)) return;
            if (!isVsyncFrame) return;

            FrameCollectItem item = map.get(visibleScene);
            if (null == item) {
                item = new FrameCollectItem(visibleScene);
                map.put(visibleScene, item);
            }

            item.collect(droppedFrames);

            if (item.sumFrameCost >= timeSliceMs) { // report
                map.remove(visibleScene);
                item.report();
            }
        }
    }

    private class FrameCollectItem {
        String visibleScene;
        long sumFrameCost;
        int sumFrame = 0;
        int sumDroppedFrames;
        // record the level of frames dropped each time
        int[] dropLevel = new int[DropStatus.values().length];
        int[] dropSum = new int[DropStatus.values().length];

        FrameCollectItem(String visibleScene) {
            this.visibleScene = visibleScene;
        }

        void collect(int droppedFrames) {
            float frameIntervalCost = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO;
            sumFrameCost += (droppedFrames + 1) * frameIntervalCost;
            sumDroppedFrames += droppedFrames;
            sumFrame++;
            if (droppedFrames >= frozenThreshold) {
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;
                dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
            } else if (droppedFrames >= highThreshold) {
                dropLevel[DropStatus.DROPPED_HIGH.index]++;
                dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
            } else if (droppedFrames >= middleThreshold) {
                dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
                dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
            } else if (droppedFrames >= normalThreshold) {
                dropLevel[DropStatus.DROPPED_NORMAL.index]++;
                dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
            } else {
                dropLevel[DropStatus.DROPPED_BEST.index]++;
                dropSum[DropStatus.DROPPED_BEST.index] += Math.max(droppedFrames, 0);
            }
        }
    }
复制代码

这部分代码则是Matrix对于一个帧片段进行数据处理的逻辑。可以看出,collect方法内筛选出了最大最小等等多个纬度的数据,丰富了一个数据片段。这个地方数据越多越能帮助一个开发定位问题。

采集逻辑也参考了Matrix的这部分代码,但是在实际测试阶段发现了个小Bug,因为上报的是一个比较大的时间片段,用户切换了页面之后,会把上个页面的fps数据也当做下个页面的数据上报。

所以我们增加了一个ActivityLifeCycle,当页面发生变化的情况下进行一次数据上报操作。其次我们把Matrix内的前后台切换等逻辑也进行了一次调整,更换成更可靠的ProcessLifecycleOwner

Cpu和Memory

内存和Cpu的使用状况可以更好的帮我们检测线上用户的真实情况,而不是等到用户crash之后我们再去反推这个问题,可以根据页面维度筛选出不同的页面数据,方便开发分析对应的问题。

在已经获取到Fps的经验之后,我们在这个基础上增加了Cpu和Memory的数据收集。相对来说我们可以借鉴大量的采集逻辑,然后只要在获取关键性数据进行调整就好了。

  1. 数据在子线程中采集,避免采集数据卡顿主线程。
  2. 同时每秒采集一次数据,数据内容本地分析,计算峰值谷值均值
  3. 数据上报节点拆分,一定时间内,页面切换,生成一个数据。
  4. 合并Cpu和内存数据,作为同一个数据结构上报,优化数据流量问题。

Memory 数据采集

Memory的数据我们参考了下Dokit的代码,高低版本也有差异,高版本可以直接通过Debug.MemoryInfo()获取到内存的数据,低版本则需要通过ams获取到ActivityManager从中获取数据。

以下是性能采集的工具类同时采集了cpu数据,各位可以直接使用。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object PerformanceUtils {
    private var CPU_CMD_INDEX = -1
    @JvmStatic
  fun getMemory(): Float {
      val mActivityManager: ActivityManager? = Hasaki.getApplication().getSystemService(Context.ACTIVITY_SERVICE)
              as ActivityManager?
      var mem = 0.0f
      try {
          var memInfo: Debug.MemoryInfo? = null
          if (Build.VERSION.SDK_INT > 28) {
              // 统计进程的内存信息 totalPss
              memInfo = Debug.MemoryInfo()
              Debug.getMemoryInfo(memInfo)
          } else {
              //As of Android Q, for regular apps this method will only return information about the memory info for the processes running as the caller's uid;
              // no other process memory info is available and will be zero. Also of Android Q the sample rate allowed by this API is significantly limited, if called faster the limit you will receive the same data as the previous call.
              val memInfos = mActivityManager?.getProcessMemoryInfo(intArrayOf(Process.myPid()))
              memInfos?.firstOrNull()?.apply {
                  memInfo = this
              }
          }
          memInfo?.apply {
              val totalPss = totalPss
              if (totalPss >= 0) {
                  mem = totalPss / 1024.0f
              }
          }
      } catch (e: Exception) {
          e.printStackTrace()
      }
      return mem
  }


  /**
   * 8.0以下获取cpu的方式
   *
   * @return
   */
  private fun getCPUData(): String {
      val commandResult = ShellUtils.execCmd("top -n 1 | grep ${Process.myPid()}", false)
      val msg = commandResult.successMsg
      return try {
          msg.split("\\s+".toRegex())[CPU_CMD_INDEX]
      } catch (e: Exception) {
          "0.5%"
      }
  }

  @WorkerThread
  fun getCpu(): String {
      if (CPU_CMD_INDEX == -1) {
          getCpuIndex()
      }
      if (CPU_CMD_INDEX == -1) {
          return ""
      }
      return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          getCpuDataForO()
      } else {
          getCPUData()
      }
  }

    /**
     * 8.0以上获取cpu的方式
     *
     * @return
     */
    private fun getCpuDataForO(): String {
        return try {
            val commandResult = ShellUtils.execCmd("top -n 1 | grep ${Process.myPid()}", false)
            var cpu = 0F
            commandResult.successMsg.split("\n").forEach {
                val cpuTemp = it.split("\\s+".toRegex())
                val cpuRate = cpuTemp[CPU_CMD_INDEX].toFloatOrNull()?.div(Runtime.getRuntime()
                        .availableProcessors())?.div(100) ?: 0F
                cpu += cpuRate
            }
            NumberFormat.getPercentInstance().format(cpu)
        } catch (e: Exception) {
            ""
        }
    }
  private fun getCpuIndex() {
      try {
          val process = Runtime.getRuntime().exec("top -n 1")
          val reader = BufferedReader(InputStreamReader(process.inputStream))
          var line: String? = null
          while (reader.readLine().also { line = it } != null) {
              line?.let {
                  line = it.trim { it <= ' ' }
                  line?.apply {
                      val tempIndex = getCPUIndex(this)
                      if (tempIndex != -1) {
                          CPU_CMD_INDEX = tempIndex
                      }
                  }
              }

          }
      } catch (e: Exception) {
          e.printStackTrace()
      }
  }


  private fun getCPUIndex(line: String): Int {
      if (line.contains("CPU")) {
          val titles = line.split("\\s+".toRegex()).toTypedArray()
          for (i in titles.indices) {
              if (titles[i].contains("CPU")) {
                  return i
              }
          }
      }
      return -1
  }
}
复制代码

Cpu采集

Cpu的数据采集代码也在上面,这部分代码相对来说也比较简单,复杂的地方在于命令行以及版本适配的问题。获取的时候也需要区分系统版本,高低版本均通过cmd命令获取,修复了低版本的CpuId获取失败的问题。然后优化了下DoKit的代码逻辑。ShellUtils则可以参考Blank写的工具类集合。

总结

Fps,cpu,内存这些数据只能算是Apm中最简单的一个环节而已,Apm的实际目的是要更好的辅助开发人员去定位线上出现的问题状况,通过预警机制避免线上问题的产生以及对性能的监控。而后续还需要收集更多的用户行为数据等来辅助开发来更准确的定位线上的问题。

因为站在了巨人的肩膀上,所以其实这部分开发的难度相对来说少了很多,但是还是有一部分可以继续优化的空间的,比如当前我们只监控了Activity的变化,有没有办法根据Fragment的不同进行数据上报呢,还有数据内容有没有办法提取更多的信息相关。

下一篇文章会给大家介绍下关于Apm中关于IO读写监控相关的内容,这一部分的代码我们这边魔改的量要更大一点,基本上我这边已经做好了,但是内容可能我还是要重新整理下。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Android轻量级APM性能监测方案
[GITHUB链接 Collie ](https://github.com/happylishang/Collie)
看书的小蜗牛
2020/09/16
4K0
“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
在Android中,当我们谈到 布局优化、卡顿优化 时,通常都知道 需要减少布局层级、减少主线程耗时操作,这样可以减少丢帧。如果丢帧比较严重,那么界面可能会有明显的卡顿感。我们知道 通常手机刷新是每秒60次,即每隔16.6ms刷新一次。 问题来了:
胡飞洋
2020/08/25
10.7K1
“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
手写解析微信Matrix性能监控日志的工具
在精读了Matrix的源码之后,我发出了赞叹和吐槽两种声音。值得赞扬的是,「这个APM框架的设计思路确实鬼斧神工,有很多值得Android开发者学习的地方,深入了解它,能够扩宽我们的编程视野。」 令人吐槽的就是,「从文档的丰富性,代码的可读性,代码的注释量,开源的一条龙服务等方面讲,他们做的还不太好。」 作为国内的顶尖开发团队,这些方面与国外的顶尖开源开发团队还是有不小的差距。
音视频开发进阶
2021/06/09
2.6K1
手写解析微信Matrix性能监控日志的工具
​一帧图像的Android之旅 :应用的首个绘制请求
Android 框架提供了各种用 2D 和 3D 图形渲染的 API 与制造商的图形驱动程序实现方法交互,在Android平台上应用开发者可通过三种方式将图像绘制到屏幕上:Canvas、OpenGLES、Vulkan 无论使用什么方式进行内容的生产,这个离用户最近的图形系统都扮演者一个非常重要的角色,在此系统一系列关键组件的协同帮助下,最终按照我们的预期将画面展示给用户。
音视频开发进阶
2020/07/29
2.2K1
面试官:如何监测应用的 FPS ?
即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。
音视频开发进阶
2020/11/10
1.6K0
面试官:如何监测应用的 FPS ?
从 Android 开发到读懂源码 第08期:Android应用层视图渲染机制
Android应用层是不涉及 SurfaceFlinger,FrameBuffer 之类的底层框架,常用刷新视图都是在 View 的 draw 相关方法中进行标准绘制 api 操作,然后通过 View.invalidate 或者 View.requestLayout 通知系统进行视图显示的刷新。在此不讨论 draw 相关的 api , draw 的所有绘制方法都是直接jni调用对应 skia 的绘制,具体的自己查看 skia 引擎相关的资料。
数据库交流
2022/04/25
7140
从 Android 开发到读懂源码 第08期:Android应用层视图渲染机制
面试必考体系庞大的Handler你真的都了解吗?Handler二十七问带你打破砂锅问到底!
既然它如此重要,不知对面的你了解它多深呢?今天就和大家一起打破砂锅问到底,看看Handler这口砂锅的底到底在哪里。
Android技术干货分享
2021/03/24
6200
面试必考体系庞大的Handler你真的都了解吗?Handler二十七问带你打破砂锅问到底!
Android卡顿优化 | 自动化卡顿检测方案与优化(AndroidPerformanceMonitor / BlockCanary)
当然,我们可以在logcat中定位关键词blockInfo, 看到同样的详细的信息:
凌川江雪
2020/04/09
2.8K0
Android卡顿优化 | 自动化卡顿检测方案与优化(AndroidPerformanceMonitor / BlockCanary)
Choreographer全解析
早呀各位。今天继续屏幕刷新机制的知识讲解,上文说到vsync的处理,每一帧UI的绘制前期处理都在Choreographer中实现,那么今天就来看看这个神奇的舞蹈编舞师是怎么将UI变化反应到屏幕上的。
码上积木
2021/01/11
5160
Choreographer全解析
Handler的初级、中级、高级问法,你都掌握了吗?
Handler是Android中的消息处理机制,是一种线程间通信的解决方案,同时你也可以理解为它天然的为我们在主线程创建一个队列,队列中的消息顺序就是我们设置的延迟的时间,如果你想在Android中实现一个队列的功能,不妨第一时间考虑一下它。本文分为三部分:
没关系再继续努力
2021/12/30
1.3K0
《广研Android卡顿监控系统》
实现背景 应用的使用流畅度,是衡量用户体验的重要标准之一。Android 由于机型配置和系统的不同,项目复杂App场景丰富,代码多人参与迭代历史较久,代码可能会存在很多UI线程耗时的操作,实际测试时候也会偶尔发现某些业务场景发生卡顿的现象,用户也经常反馈和投诉App使用遇到卡顿。因此,我们越来越关注和提升用户体验的流畅度问题。 已有方案 在这之前,我们将反馈的常见卡顿场景,或测试过程中常见的测试场景使用UI自动化来重复操作,用adb系统工具观察App的卡顿数据情况,试图重现场景来定位问题。 常用的方式是使用
腾讯Bugly
2018/03/23
4.8K2
Android GPU呈现模式原理及卡顿掉帧浅析
APP开发中,卡顿绝对优化的大头,Google为了帮助开发者更好的定位问题,提供了不少工具,如Systrace、GPU呈现模式分析工具、Android Studio自带的CPU Profiler等,主要是辅助定位哪段代码、哪块逻辑比较耗时,影响UI渲染,导致了卡顿。拿Profile GPU Rendering工具而言,它用一种很直观的方式呈现可能超时的节点,该工具及其原理也是本文的重点:
看书的小蜗牛
2019/01/07
3.1K0
Android GPU呈现模式原理及卡顿掉帧浅析
Android性能优化之2个帧率和卡顿监控方案(附详细实现代码)
在 Android 开发中,监控应用的帧率(FPS)是评估应用性能和发现卡顿问题的一个重要指标。本次介绍一些常用的方法来监控 Android 应用的 FPS,包括如何实现这些方法的详细说明和代码示例。
AntDream
2024/10/31
8640
Android性能优化之2个帧率和卡顿监控方案(附详细实现代码)
Android 开发必须了解的屏幕刷新机制
原文链接 https://juejin.cn/post/7291935035530313755
GeeJoe
2023/10/24
5370
Android 开发必须了解的屏幕刷新机制
从 Android 开发到读懂源码 第03期:View.post 源码解析
这个方法在日常开发中是经常用到的,例如在子线程中我们需要更新 UI,可以通过 post 一个 runnable ,在 run 方法中去绘制 UI ,或者我们需要在 Activity 的 onCreate 中获取一个 View 的宽高时,也会通过 post 一个 runnable 并在 run 方法中获取这个 View 的 width 和 height 信息。本文基于 Android 9.0 的源码进行分析。
数据库交流
2022/04/25
3560
从 Android 开发到读懂源码 第03期:View.post 源码解析
揭秘:60FPS流畅体验背后,Android渲染机制深度解析与性能优化秘籍!
Android 的渲染显示是一个复杂的过程,涉及多个系统组件和层次。为了理解帧率、掉帧和卡顿的原理,我们需要深入了解 Android 的渲染体系。
AntDream
2024/10/31
4290
揭秘:60FPS流畅体验背后,Android渲染机制深度解析与性能优化秘籍!
面试官:如何监测应用的 FPS ?
即使你不知道 FPS,但你一定听说过这么一句话,在 Android 中,每一帧的绘制时间不要超过 16.67ms。那么,这个 16.67ms 是怎么来的呢?就是由 FPS 决定的。
路遥TM
2021/08/31
1.7K0
Android VSYNC (Choreographer)与UI刷新原理分析.md
从UI控件内容更改到被重新绘制到屏幕上,这中间到底经历了什么?另外,连续两次setTextView到底会触发几次UI重绘呢?为什么Android APP的帧率最高是60FPS呢,这就是本文要讨论的内容。
看书的小蜗牛
2020/02/13
1.8K0
Android 子线程 UI 操作真的不可以?
某 SDK 有 PopupWindow 弹窗及动效,由于业务场景要求,对于 App 而言,SDK 的弹窗弹出时机具有随机性。
2020labs小助手
2022/05/24
1.2K0
触摸Android的心脏跳动
在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。
Rouse
2023/11/05
3010
触摸Android的心脏跳动
推荐阅读
相关推荐
Android轻量级APM性能监测方案
更多 >
交个朋友
加入HAI高性能应用服务器交流群
探索HAI应用新境界 共享实践心得
加入架构与运维学习入门群
系统架构设计入门 运维体系构建指南
加入架构与运维工作实战群
高并发系统设计 运维自动化实践
换一批
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验