一、背景

APM 的全称叫做 Application Performance Monitor,属于应用性能监控部分。其中有一项比较重要的指标参数,叫做页面可视耗时,本文将介绍一套耗时检测方案。
首先看一段耗时检测的视频:https://live.csdn.net/v/260516

二、方案

1、Activity页面加载时间


public class BaseActivity extends Activity {public boolean isNeedLoadingTimeDetect() {return true;}public int getLoadingTimeDetectType() {return LoadingDetector.AREA_DETECT;}@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);if (isNeedLoadingTimeDetect()) {LoadingDetector.getInstance().startWatch(this, getLoadingTimeDetectType());}}@Overrideprotected void onDestroy() {super.onDestroy();if(isNeedLoadingTimeDetect()) {LoadingDetector.getInstance().destroyWatch(this, getLoadingTimeDetectType());}}
}

首先启动一个定时线程池服务ScheduledThreadPoolExecutor,每隔60ms去做屏幕decorview的检测,判断view是否是加载页面完成的状态,检测处理是在线程池,为了不影响UI线程的处理。

private void checkPageVisible(Activity activity, ViewTimeEntry entry, int detectType) {if (entry == null) {return;}ScheduledThreadPoolExecutor scheduledExecutorService = new ScheduledThreadPoolExecutor(2);scheduledExecutorService.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {boolean isVisible;isVisible = detectType == AREA_DETECT ? checkAreaVisible(activity) : checkPixelsPageVisible(activity);if (isVisible) {scheduledExecutorService.shutdown();if (entry != null) {entry.setEndTime(System.currentTimeMillis());showToastOnMainThread(activity, entry);}} else {long currentTime = System.currentTimeMillis();if (currentTime - entry.getStartTime() >= 10000) {//兜底操作scheduledExecutorService.shutdown();entry.setEndTime(currentTime);showToastOnMainThread(activity, entry);}}}}, 60, 60, TimeUnit.MILLISECONDS);entry.setScheduledExecutorServic(scheduledExecutorService);}

如果大于10s还没有加载完成页面,默认关闭线程池结束,作为一个兜底方案。

针对Activity页面加载时间检测算法,可分为两种方案,

   public static final int AREA_DETECT = 1;public static final int PIXELS_DETECT = 2;

一种是面积可见区域算法,一种是像素计算法。
a、面积可见区域算法
第一步,首先获取decorview下的所有子view,如果是viewgroup,就继续遍历所有的子view,知道得到所有view的集合。
第二步,计算所有子view的可视范围内的面积(如果超过了屏幕范围了则截取屏幕范围内的部分),去除以phone的屏幕面积,比值如果大于0.6f则当作页面加载完成。
其中需要注意,子view包含的有:textview, editview,imageview,自定义view等等。

针对这些view我们增加更细的规则:

  • View 在全部或者部分在屏幕范围内,且 Visibility 必须为 View.VISIBLE
  • 只针对 View 进行计算,ViewGroup 不在计算范围之列,且不是 ViewStub
  • 如果是 ImageView,必须是加载图片完成后才能当作有效的view
  • 如果是 TextView,必须要有文字

遍历所有子view如下:

public static List<View> getAllViews(Activity activity) {List<View> viewList = new ArrayList<>();ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();for (int i = 0; i < decorView.getChildCount(); i++) {if (decorView.getChildAt(i).getVisibility() != View.VISIBLE) {continue;}if (decorView.getChildAt(i) instanceof ViewGroup) {viewList.addAll(getAllViews((ViewGroup) decorView.getChildAt(i)));} else if (!(decorView.getChildAt(i) instanceof ViewStub)) {viewList.add(decorView.getChildAt(i));}}return viewList;}private static List<View> getAllViews(ViewGroup viewGroup) {List<View> viewList = new ArrayList<>();for (int i = 0; i < viewGroup.getChildCount(); i++) {if (viewGroup.getChildAt(i).getVisibility() != View.VISIBLE) {continue;}if (viewGroup.getChildAt(i) instanceof ViewGroup) {viewList.addAll(getAllViews((ViewGroup) viewGroup.getChildAt(i)));} else if (!(viewGroup.getChildAt(i) instanceof ViewStub)) {viewList.add(viewGroup.getChildAt(i));}}return viewList;}

处理Imageview遇到了问题,如果图片没有加载出来,不能算是有效view的面积进行计算。解决方案:

public class ViewTag {/*** 当前状态是无效的View,但是仅仅表示当前状态,有可能变成有效,例如 ImageView*/public static final String APM_VIEW_VALID = "valid_view";/*** 当前状态是有效的View*/public static final String APM_VIEW_INVALID = "invalid_view";/*** 需要完全忽略的无用 View,这个 View 完全是计算的噪点,例如鱼骨图*/public static final String APM_VIEW_IGNORE = "ignore_view";
}
 public static void loadImage(Context context, ImageView imageView, String url) {imageView.setTag(ViewTag.APM_VIEW_INVALID);Glide.with(context).load(url).apply(new RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE)).into(new SimpleTarget<Drawable>() {@Overridepublic void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {imageView.setImageDrawable(resource);imageView.setTag(ViewTag.APM_VIEW_VALID);}});}
private static List<View> getAllViews(ViewGroup viewGroup) {List<View> viewList = new ArrayList<>();for (int i = 0; i < viewGroup.getChildCount(); i++) {if (viewGroup.getChildAt(i) instanceof ViewGroup) {viewList.addAll(getAllViews((ViewGroup) viewGroup.getChildAt(i)));} else if (!(viewGroup.getChildAt(i) instanceof ViewStub)) {viewList.add(viewGroup.getChildAt(i));}}return viewList;}

在图片加载框架中处理,如果图片加载完成,设置一个有效标志tag,遍历所有的子view时,如果是imageview且tag标志为有效view的标志才能算入到有效view的面积当中去。
如果是项目添加了骨架图,也是可以通过这种设置tag有效标志的方法来进行处理

计算面积的方法如下:

public static float calculateVisibleArea(Activity activity, List<View> viewList) {int screenWidth = activity.getWindowManager().getDefaultDisplay().getWidth();int screenHeight = activity.getWindowManager().getDefaultDisplay().getHeight();float phoneArea = screenHeight * screenWidth;float area = 0;for (int i = 0; i < viewList.size(); i++) {View view = viewList.get(i);if (view instanceof ImageView && !ViewTag.APM_VIEW_VALID.equals(view.getTag())) {continue;}if(view instanceof TextView || view instanceof ImageView) {area += getViewArea(viewList.get(i), screenWidth, screenHeight);}}return area / phoneArea;}private static float getViewArea(View view, int screenWidth, int screenHeight) {int[] location = new int[2];view.getLocationOnScreen(location);return (getPos(location[0], true, screenWidth, screenHeight) - getPos(location[0] + view.getRight() - view.getLeft(), true, screenWidth, screenHeight))* (getPos(location[1], false, screenWidth, screenHeight) - getPos(location[1] + view.getBottom() - view.getTop(), false, screenWidth, screenHeight));}private static int getPos(int pos, boolean isX, int screenWidth, int screenHeight) {if (pos < 0) {return 0;}if (isX) {return Math.min(pos, screenWidth);} else {return Math.min(pos, screenHeight);}}

b、像素计算法
这种方法主要是针对非Activity页面,如H5页面,RN,Flutter页面。我们无法去获取子view的面积。我们可以通过这种像素计算的方法来处理。

public static boolean doCheckViewPixels(final View view) {boolean isDrawFinish = false;Bitmap bitmap;try {view.setDrawingCacheEnabled(true);bitmap = view.getDrawingCache();if (bitmap == null || bitmap.isRecycled()) {return false;}try {int[] pixelArray = collectPixel(bitmap);//只有三种情况全是 true 的时候,才认为页面是加载成功了if (planA(pixelArray) && planC(pixelArray) && planB(pixelArray)) {isDrawFinish = true;}} catch (Exception e) {isDrawFinish = false;}try {view.setDrawingCacheEnabled(false);} catch (Exception e) {}} catch (Exception e) {return false;}return isDrawFinish;}

首先去采集屏幕上的像素点,比如200个

private final static int SIZE = 200;private final static int TOP_SIZE = 100;private static double randomValue = 0;/*** 采集 bitmap 中的像素点* 页面一分为2,上面40个点,下面20个点** @return 采集到的像素数组*/private static int[] collectPixel(Bitmap b) {int[] pixels = new int[SIZE];int w = b.getWidth();int h = b.getHeight();int segmentLength = w * h / 2;  //平均分为2段,每一段的长度int spaceUP = segmentLength / TOP_SIZE;int spaceDOWN = segmentLength / (SIZE - TOP_SIZE);randomValue = Math.random();int offset = (int) (randomValue * spaceUP);for (int i = 0; i < TOP_SIZE; i++) {int index = offset + spaceUP * i;int y = index / w;int x = index % w;pixels[i] = b.getPixel(x, y);}offset = offset + segmentLength;for (int i = 0; i < (SIZE - TOP_SIZE); i++) {int index = offset + spaceDOWN * i;int y = index / w;int x = index % w;pixels[i + TOP_SIZE] = b.getPixel(x, y);}return pixels;}

获取到了像素的数据,在满足了三个条件下才能算作页面加载完成,分别是:

  • 如果页面40%的像素值和loading一致,当做还在loading(其中LOADING_PIXEL_COLOR是默认加载的颜色像素值)
  • 如果页面95%的像素值一样,当做还在loading
  • 页面像素值种类小于等于4,当做还在loading
 /*** 页面40%的像素值和loading一致,当做还在loading* @return 是否加载完成*/private static boolean planA(int[] pixels) {int count = 0;for (int pix : pixels) {if (pix == LOADING_PIXEL_COLOR) {count++;}}return count < SIZE * 0.4;}/*** 页面95%的像素值一样,当做还在loading* @return 是否加载完成*/private static boolean planB(int[] data) {int max = 0;SparseIntArray sparseIntArray = new SparseIntArray();for (int pix : data) {int count = sparseIntArray.get(pix) + 1;sparseIntArray.put(pix, count);if (count > max) {max = count;}}boolean res = max < SIZE * 0.95;Log.d(TAG, "planB: " + res + "  max size " + max + "   random " + randomValue);return res;}/*** 页面像素值种类小于等于4,当做还在loading* @return 是否加载完成*/private static boolean planC(int[] data) {if (data == null) {return false;}Set<Integer> set = new HashSet<Integer>();for (int pix : data) {set.add(pix);}boolean res = set.size() > 4;Log.d(TAG, "planC: " + res + "  set size = " + set.size());if (res) {Log.d(TAG, "planC: ");}return res;}

2、Fragment页面加载时间


public class BaseFragment extends Fragment {public boolean isNeedLoadingTimeDetect() {return true;}public int getLoadingTimeDetectType() {return LoadingDetector.AREA_DETECT;}@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);if (isNeedLoadingTimeDetect()) {LoadingDetector.getInstance().startWatch(this, getLoadingTimeDetectType());}}@Overridepublic void onDestroy() {super.onDestroy();if (isNeedLoadingTimeDetect()) {LoadingDetector.getInstance().destroyWatch(this);}}
}

具体处理方法同Activity一样。

3、H5、RN、Flutter页面加载时间

采用像素计算法,不赘述。

4、代码地址
https://github.com/kkloqin

参考链接:
https://tech.taobao.org/news/nvk0si

APM之页面加载耗时检测方案相关推荐

  1. 提升页面加载速度的方案

    性能优化是一个庞大而相对复杂的知识,如今互联网发展迅速,市场竞争激烈,在这样的环境下一个网站的性能决定着一个项目的好与坏.为了降低软件项目的跳出率.提高访问速度.减少加载时间.带给用户流畅的终端体验, ...

  2. 关于请求被挂起页面加载缓慢问题的追查

    本文前戏较多,务实的同学可以直接跳到结论. 由「钢的琴」网友脑洞大开延伸出了吉的他二的胡琵的琶,以及后来许嵩的「苏格拉没有底」,是否可以再拓展一下,得到哥本不爱吃哈根,哈根爱达斯等剧情乱入的关系. 上 ...

  3. Python+selenium自动化:页面加载慢、超时加载情况下内容已经加载完毕的快速执行脚本解决方案,页面加载时间过长优化方案

    driver.set_page_load_timeout(3) 页面加载时间设置 3 秒,执行到某一步涉及页面加载如果加载时间超过 3 秒就会停止加载并抛出异常,其实这个时候页面内的元素已经加载出来了 ...

  4. layui 如何动态加载局部页面_从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!

    前言 见解有限,如有描述不当之处,请帮忙指出,如有错误,会及时修正. 为什么要梳理这篇文章? 最近恰好被问到这方面的问题,尝试整理后发现,这道题的覆盖面可以非常广,很适合作为一道承载知识体系的题目. ...

  5. 从输入URL到页面加载的过程?由一道题完善自己的前端知识体系!

    梳理主干流程 这道题上,如何回答呢?先梳理一个骨架. 知识体系中,最重要的是骨架,脉络.有了骨架后,才方便填充细节.所以,先梳理下主干流程: 从浏览器接收url到开启网络请求线程(这一部分可以展开浏览 ...

  6. 从输入url到页面加载完成中间都发生了什么?

    从输入 URL 到页面加载完成的过程中都发生了什么事情? nwind | 24 May 2014 背景 本文来自于之前我发的一篇微博: 不过写这篇文章并不是为了帮大家准备面试,而是想借这道题来介绍计算 ...

  7. 试简述smtp通信的三个阶段的过程_从输入URL到页面加载的过程?《转载》

    这是我看过这个问题最完整/优质的回答了,转来分享 知乎的排版不太好,可以浏览博客原文: http://gaoxiang.ga/index.php/archives/36/​gaoxiang.ga 前言 ...

  8. layui 如何动态加载局部页面_从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!...

    作者:撒网要见鱼 原文链接:http://www.dailichun.com/2018/03/12/whenyouenteraurl.html 「----超长文预警,需要花费大量时间.----」 本文 ...

  9. 从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!

    前言 见解有限,如有描述不当之处,请帮忙指出,如有错误,会及时修正. 为什么要梳理这篇文章? 最近恰好被问到这方面的问题,尝试整理后发现,这道题的覆盖面可以非常广,很适合作为一道承载知识体系的题目. ...

  10. WEB前端性能优化,提高页面加载速度

    可能有人会说:网站的性能是后端工程师的事情,与前端并无多大关系.我只能说,too young too simple.事实上,只有10%~20%的最终用户响应时间是用在从Web服务器获取HTML文档并传 ...

最新文章

  1. UITableView 重用机制
  2. 计算阶比分析 matlab_(案例)层次聚类分析Matlab编码计算
  3. java 读文件夹_java怎么读取读取文件夹下的所有文件夹和文件?
  4. 基于pygame的射击小游戏制作(四)击杀外星人
  5. 任务03——简单程序测试及 GitHub Issues 的使用
  6. CF1142C U2(计算几何,凸包)
  7. java开源对象池_JAVA 对象池
  8. ATL 线程池的使用
  9. 什么是气泡图?怎样用Python绘制?怎么用?终于有人讲明白了
  10. js实现椭圆轨迹_华为开发者大会2020隆重召开,亿健T10椭圆机荣耀参展
  11. oracle java 面试题及答案_Oracle面试题及答案
  12. 使用Zabbix进行IPMI监控
  13. 通过 JavaScript调用Asp.net(C#)后台方法
  14. SqlServer2005日志清理
  15. 日期格式化java_JAVA格式化时间日期
  16. 8根网线的排序和作用
  17. Asp.Net Core 系列教程 (一)
  18. 集线器、交换机、路由器
  19. 利用Google博客搜索查看加密QQ空间(qzone)日志
  20. You must address the points described in the following [1] lines before starting Elasticsearch.

热门文章

  1. PHP获取未来三天天气接口
  2. 鬼扯LENOVO-IBM
  3. Java社区项目开发社区首页
  4. windows 10中pip install总是失败解决方法
  5. 如何利用设计团队分享提高表达能力
  6. java io 系列之1 decorator模式
  7. 中学生考计算机学校,初中生没考上高中,读什么学校好?
  8. SD卡容量等级-SD/SDHC/SDXC
  9. 2019UMS培训day6解题报告
  10. 小绿人显示服务器异常,一行代码帮你搞定Android版本更新