APM监控 · 入门篇 · Android端测监控平台建设(1)

本文涉及的产品
云拨测,每月3000次拨测额度
简介: APM 全称 Application Performance Management & Monitoring (应用性能管理/监控)性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿、响应速度慢、发热严重、流量电量消耗大等问题的时候,很可能就会卸载掉我们的 App。这也是我们在目前工作中面临的巨大挑战之一,尤其是低端机型。

1681523293361.png

改不完的 Bug,写不完的矫情。公众号 小木箱成长营 现在专注移动基础开发 ,涵盖音视频和 APM,信息安全等各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!

APM简要介绍

APM 全称 Application Performance Management & Monitoring (应用性能管理/监控)

性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿、响应速度慢、发热严重、流量电量消耗大等问题的时候,很可能就会卸载掉我们的 App。这也是我们在目前工作中面临的巨大挑战之一,尤其是低端机型。

商业化的APM平台:著名的 NewRelic,还有国内的听云、OneAPM 、阿里百川-码力APM的SDK、百度的APM收费产品等等。

APM工作方式:

  1. 首先在客户端(Android、iOS、Web等)采集数据;
  2. 接着将采集到的数据整理上报到服务器(多种方式 json、xml,上传策略等等);
  3. 服务器接收到数据后建模、存储、挖掘分析,让后将数据进行可视化展示(spark+flink),供用户使用。
  4. 1681523455850.png
  5. 那么移动端需要做的事情就是:
  • 双端统一原则(技术选型 NDK c++)
  • 数据采集 (采集指标、细化等等)
  • 数据存储(写文件?mmap、fileIO流)
  • 数据上报(上报策略、上报方式)

那我们到底应该怎么做?一定要学会看开源的东西。让我们先看看大厂的开源怎么做的?我们在自己造轮子,完成自己的APM采集框架。

目前核心开源APM框架产品

你会发现自定义Gradle插件技术、ASM技术、打包流程Hook、Android打包流程等。那思考一下,为什么大家做的主要的流程都是一样的,不一样的是具体的实现细节,比如如何插桩采集到页面帧率、流量、耗电量、GC log等等。

ArgusAPM性能监控平台介绍&SDK开源-卜云涛.pdf

我们先简单来看下在matrix中,如何利用Java Hook和 Native Hook完成IO 磁盘性能的监控?

1681523548125.png

Java Hook的hook点是系统类CloseGuard,hook的方式是使用动态代理。

github.com/Tencent/mat…

private boolean tryHook() {
        try {
            Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard");
            Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter");
            Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter");
            Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls);
            Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class);
            sOriginalReporter = methodGetReporter.invoke(null);
            methodSetEnabled.invoke(null, true);
            // open matrix close guard also
            MatrixCloseGuard.setEnabled(true);
            ClassLoader classLoader = closeGuardReporterCls.getClassLoader();
            if (classLoader == null) {
                return false;
            }
            methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader,
                new Class<?>[]{closeGuardReporterCls},
                new IOCloseLeakDetector(issueListener, sOriginalReporter)));
            return true;
        } catch (Throwable e) {
            MatrixLog.e(TAG, "tryHook exp=%s", e);
        }
        return false;
    }

这里的CloseGuard有啥用?为什么腾讯的人要hook这个。这个在后续的分线中我们在来详细的说。如果要解决这个疑问,做好的办法就是看源码。(==系统埋点方式,监控系统资源的异常回收==)

关于native hook:

Native Hook是采用PLT(GOT) Hook的方式hook了系统so中的IO相关的openreadwriteclose方法。在代理了这些系统方法后,Matrix做了一些逻辑上的细分,从而检测出不同的IO Issue。

github.com/Tencent/mat…

 JNIEXPORT jboolean JNICALL
        Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
            __android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
            for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
                const char* so_name = TARGET_MODULES[i];
                __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
                //打开so文件,并在内存中映射成ELF文件格式
                loaded_soinfo* soinfo = elfhook_open(so_name);
                if (!soinfo) {
                    __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
                    continue;
                }
                //替换open函数
                elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
                elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
                bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
                if (is_libjavacore) {
                    if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) {
                        __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
                        //http://refspecs.linux-foundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/libc---read-chk-1.html 类似于read()
                        if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) {
                            __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
                            elfhook_close(soinfo);
                            return false;
                        }
                    }
                    if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) {
                        __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
                        if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) {
                            __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
                            elfhook_close(soinfo);
                            return false;
                        }
                    }
                }
                elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
                elfhook_close(soinfo);
            }
            return true;
        }

关于transform api

google.github.io/android-gra…

我们编译Android项目时,如果我们想拿到编译时产生的Class文件,并在生成Dex之前做一些处理,我们可以通过编写一个Transform来接收这些输入(编译产生的Class文件),并向已经产生的输入中添加一些东西。

1681523668026.png

如何使用的?

github.com/Tencent/mat…

  • 编写一个自定义的Transform
  • 注册一个Plugin完成或者在gradle文件中直接注册。
//MyCustomPlgin.groovy
public class MyCustomPlgin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getExtensions().findByType(BaseExtension.class)
                .registerTransform(new MyCustomTransform());
    }
}
project.extensions.findByType(BaseExtension.class).registerTransform(new MyCustomTransform());  //在build.gradle中直接写

MatrixTraceTransform 利用编译期字节码插桩技术,优化了移动端的FPS、卡顿、启动的检测手段。在打包过程中,hook生成Dex的Task任务,添加方法插桩的逻辑。我们的hook点是在Proguard之后,Class已经被混淆了,所以需要考虑类混淆的问题。

MatrixTraceTransform主要逻辑在transform方法中:

@Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long start = System.currentTimeMillis()
        //是否增量编译
        final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
        //transform的结果,重定向输出到这个目录
        final File rootOutput = new File(project.matrix.output, "classes/${getName()}/")
        if (!rootOutput.exists()) {
            rootOutput.mkdirs()
        }
        final TraceBuildConfig traceConfig = initConfig()
        Log.i("Matrix." + getName(), "[transform] isIncremental:%s rootOutput:%s", isIncremental, rootOutput.getAbsolutePath())
        //获取Class混淆的mapping信息,存储到mappingCollector中
        final MappingCollector mappingCollector = new MappingCollector()
        File mappingFile = new File(traceConfig.getMappingPath());
        if (mappingFile.exists() && mappingFile.isFile()) {
            MappingReader mappingReader = new MappingReader(mappingFile);
            mappingReader.read(mappingCollector)
        }
        Map<File, File> jarInputMap = new HashMap<>()
        Map<File, File> scrInputMap = new HashMap<>()
        transformInvocation.inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput dirInput ->
                //收集、重定向目录中的class
                collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
            }
            input.jarInputs.each { JarInput jarInput ->
                if (jarInput.getStatus() != Status.REMOVED) {
                    //收集、重定向jar包中的class
                    collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
                }
            }
        }
        //收集需要插桩的方法信息,每个插桩信息封装成TraceMethod对象
        MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
        HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
       //执行插桩逻辑,在需要插桩方法的入口、出口添加MethodBeat的i/o逻辑
        MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
        methodTracer.trace(scrInputMap, jarInputMap)
        //执行原transform的逻辑;默认transformClassesWithDexBuilderForDebug这个task会将Class转换成Dex
        origTransform.transform(transformInvocation)
        Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
    }

看到这里了,我们是不是应该总结下APM的核心技术是什么?

大厂面试之一:APM的核心技术是什么?做过自研的APM吗?

APM核心原理一句话总结:

依据打包原理,在class转换为dex的过程中,调用gradletransformapi遍历class文件,借助JavassistASM等框架修改字节码,插入我们自己的代码实现性能数据的统计。这个过程是在编译器见完成

你掌握了APM的核心原理,也可以做Android的无痕埋点了,本质是一样的,不一样的是Hook的地方不一样。

APM监控维度和指标

App基础性能指标集中为8类:网络性能、崩溃、启动加载、内存、图片、页面渲染、IM和VoIP(业务相关性和你的APP相关)、用户行为监控,基础维度包括App、系统平台、App版本和时间维度。

网络性能

网络服务成功率,平均耗时和访问量、上下行的速率监控。访问的链接、耗时等等。思考怎么结合okhttp来做?

网络监控业务背景

在不确定哪个网络url耗时比较慢的情况下,做一个全链路网络监控体系,这个是我们值得深思的问题,这边使用AspectJ和利用OKhttp自身的EventListener对网络信息进行二次封装上报

网络监控实现步骤

第一步: 将我们需要监控的数据源转换成 Bundle 对象方便传输
public interface BundleMapping {
    /**
     * 转换数据结构为Bundle
     */
    Bundle asBundle();
}
第二步: 确定我们需要监控的字段
字段 字段含义 备注
total 总网络时间 调用结束时间 减去 请求开始时间
pathname 请求地址 /
dns dnsEndTime - dnsStartTime DNS查询结束时间 减去 DNS查询开始时间
protocol 请求协议 /
tcp 连接结束时间 调用结束时间 减去 连接开始时间
no_dns_tcp_tls 总网络时间 调用结束时间 减去 请求开始时间
tls 总网络时间 调用结束时间 减去 请求开始时间
no_response 是否无响应内容(304或返回body为空) /
ttfb ttfb首字节时间 响应结束时间 减去 请求开始时间
download 总网络时间 响应结束时间 减去 响应开始时间
pure_network 总网络时间 响应结束时间 减去 响应开始时间
transfer_size 传输大小 /
failed 请求失败信息 /
public class NetWorkData implements BundleMapping {
    //请求地址
    String url;
    //请求协议
    String protocol;
    //请求开始时间
    long callStartTime;
    //响应开始时间
    long responseStartTime;
    //响应结束时间
    long responseEndTime;
    //DNS查询开始时间
    long dnsStartTime;
    //DNS查询结束时间
    long dnsEndTime;
    //连接开始时间
    long connectStartTime;
    //连接结束时间
    long connectEndTime;
    //是否同时重用DNS、TCP、TLS
    boolean isDnsTcpTls;
    //SSL 连接开始时间
    long secureConnectStartTime;
    //SSL 连接结束时间
    long secureConnectEndTime;
    //是否无响应内容(304或返回body为空)
    boolean isNoResponse;
    //调用结束时间
    long callEndTime;
    //请求失败信息
    String failMessage;
    //传输大小
    long byteCount;
    @Override
    public Bundle asBundle() {
        Bundle bundle = new Bundle();
        //总网络时间
        bundle.putLong("total", callEndTime - callStartTime);
        bundle.putString("pathname", url);
        //dns
        bundle.putLong("dns", dnsEndTime - dnsStartTime);
        bundle.putString("protocol", protocol);
        //tcp
        bundle.putLong("tcp", connectEndTime - connectStartTime);
        bundle.putBoolean("no_dns_tcp_tls", isDnsTcpTls);
        //tls
        bundle.putLong("tls", secureConnectEndTime - secureConnectStartTime);
        bundle.putBoolean("no_response", isNoResponse);
        //ttfb首字节时间
        bundle.putLong("ttfb", responseEndTime - callStartTime);
        //download
        bundle.putLong("download", responseEndTime - responseStartTime);
        //pure_network
        bundle.putLong("pure_network", responseEndTime - callStartTime);
        //transfer_size
        bundle.putLong("transfer_size", byteCount);
        //连接失败
        bundle.putString("failed", failMessage);
        return bundle;
    }
}
第三步: 添加我们需要上报的标记,根据不同的域名去区分
    /**
     * 获取上报Tag
     */
    String getDataTag() {
        String tag;
        if (url == null) {
            tag = "biz";
        } else {
            if (url.contains("microkibaco_report")) {
                tag = "data";
            } else if (url.contains("blog")) {
                tag = "blog";
            } else {
                tag = "github";
            }
        }
        return "network_api_" + tag;
    }
第五步: 添加沪江aspectj插件
    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.6'
    implementation 'org.aspectj:aspectjrt:1.8.9'
    implementation "com.squareup.okhttp3:okhttp:3.12.1"
第六步: 给OkHttp 添加事件监听
  @Keep
  public class MkOkHttpEventListener extends EventListener {
      private NetWorkData mNetWorkData;
      public static final Factory FACTORY = new Factory() {
          @Override
          public EventListener create(Call call) {
              return new MkOkHttpEventListener();
          }
      };
      /**
       * 每个请求都会构建
       */
      private MkOkHttpEventListener() {
          mNetWorkData = new NetWorkData();
      }
      @Override
      public void callStart(Call call) {
          mNetWorkData.url = call.request().url().toString();
          mNetWorkData.callStartTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
          mNetWorkData.connectStartTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
              Protocol protocol) {
          mNetWorkData.connectEndTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
              Protocol protocol, IOException ioe) {
      }
      @Override
      public void dnsStart(Call call, String domainName) {
          mNetWorkData.dnsStartTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
          mNetWorkData.dnsEndTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void secureConnectStart(Call call) {
          mNetWorkData.secureConnectStartTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void secureConnectEnd(Call call, Handshake handshake) {
          mNetWorkData.secureConnectEndTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void responseHeadersStart(Call call) {
          mNetWorkData.responseStartTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void responseHeadersEnd(Call call, Response response) {
          if (response.code() == 304) {
              mNetWorkData.isNoResponse = true;
          }
          mNetWorkData.protocol = response.protocol().toString();
          mNetWorkData.responseEndTime = SystemClock.elapsedRealtime();
      }
      @Override
      public void responseBodyEnd(Call call, long byteCount) {
          //响应为空
          if (byteCount == 0) {
              mNetWorkData.isNoResponse = true;
          }
          mNetWorkData.byteCount = byteCount;
      }
      @Override
      public void callEnd(Call call) {
          mNetWorkData.callEndTime = SystemClock.elapsedRealtime();
          //上报
          APM.getReportStrategy().report(mNetWorkData.getDataTag(), mNetWorkData.asBundle());
      }
      @Override
      public void callFailed(Call call, IOException ex) {
          mNetWorkData.failMessage = ex.getMessage();
          //上报
          APM.getReportStrategy().report(mNetWorkData.getDataTag(), mNetWorkData.asBundle());
      }
  }

Firebase 有一定的时效性,所以我们把日志全部采集到Json里面交给Bundle统一上报

崩溃

崩溃数据的采集和分析,类似于Bugly平台的功能

启动加载

App的启动我们做了大的力气进行了优化,多线程等等, Spark的有向无环图(DAG)来处理业务的依赖性。对App的冷启动时长、Android安装后首次启动时长和Android Bundle(atlas框架)启动加载时长进行监控。

内存

四大监测目标:内存峰值、内存均值、内存抖动、内存泄露。

IM和VoIP等业务指标

这两项都属于业务型技术指标的监控,例如对各类IM消息到达率和VoIP通话的成功率、平均耗时和请求量进行监控。这里需要根据自己的APP的业务进行针对性的梳理。

用户行为监控

用于App统计用户行为,实际上就是监控所有事件并把事件发送到服务上去。这在以前是埋点做的事情,现在也规整成APM需要做的事情,比如用户的访问路径,类似于PC时代的PV,UV等概念。

图片

资源文件的监测,比如Bitmap冗余处理。haha库处理,索引值。

页面渲染

界面流畅性监测、FPS的监测、慢函数监测、卡顿监测、文件IO开销监测等导致页面渲染的各种问题。

相关文章
|
20天前
|
Android开发
Android MediaTek 平台增加UART接口的红外模块支持,支持NEC红外遥控
Android MediaTek 平台增加UART接口的红外模块支持,支持NEC红外遥控
12 0
|
2月前
|
API 开发工具 Android开发
iOS 和 Android 平台的开发有哪些主要区别?
iOS与Android开发区别:iOS用Objective-C/Swift,App Store唯一下载渠道;Android用Java/Kotlin,多商店发布(如Google Play、华为市场)。设计上,iOS简洁一致,Android灵活可定制。开发工具,iOS用Xcode,Android用Android Studio。硬件和系统多样性,iOS统一,Android复杂。权限管理、审核流程及API各有特点,开发者需依据目标平台特性进行选择。
38 3
|
5天前
|
开发工具 Android开发
rk平台Android12屏幕永不休眠
rk平台Android12屏幕永不休眠
16 1
|
3天前
|
Android开发
Android游戏引擎AndEngine入门资料
Android游戏引擎AndEngine入门资料
|
3天前
|
Java Android开发
android AsyncTask入门
android AsyncTask入门
|
4天前
|
Java API 开发工具
java与Android开发入门指南
java与Android开发入门指南
11 0
|
5天前
|
Android开发
Android 高通平台集成无源码apk示例
Android 高通平台集成无源码apk示例
15 0
|
5天前
|
Android开发
关于高通Android 平台上qssi的介绍
关于高通Android 平台上qssi的介绍
8 0
|
19天前
|
存储 Linux Android开发
Android存储分区与Rockchip平台的分区命名及U-Boot配置
Android存储分区与Rockchip平台的分区命名及U-Boot配置
14 0
|
20天前
|
存储 安全 Ubuntu
Android 生成平台应用签名keystore文件
Android 生成平台应用签名keystore文件
9 0

推荐镜像

更多