BlockCanary与性能监控

1,597 阅读6分钟

BlockCanary开源解码

使用

implementation 'com.github.markzhai:blockcanary-android:1.5.0'

Application中

BlockCanary.install(this,new BlockCanaryContext()).start();

下面在Activity中加入一个耗时操作

public class BlockCanaryActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_block_canary);
    }

    public void evilMethod(View view) {
        SystemClock.sleep(1000);
    }
}

点击按钮,执行这个耗时操作,观察BlockCanary的日志,可以看到BlockCanary为我们打印出了耗时操作的执行时间及对应代码的位置
1

那么BlockCanary是如何做到的呢,下面我们分析BlockCanary的执行过程

BlockCanary执行过程

如何打印日志

  1. 在调用 start() 时,通过调用主线程的 Looper.setMessageLogging() 方法,为 Looper 的 mLogging 成员变量赋值。

  2. 在 Looper 死循环中, println 方法分别会在 dispatchMessage(msg) 之 前和之后被调用。

    • 所以通过自定义 Printer 对象,我们就可以获得 dispatchMessage 的耗时, 从而判断出是否有应用卡顿。
    • 我们知道Android主线程中发生的事情都是在Looper.loop中发生的,所以这里可以监控到主线程中发生的各种事情,如果dispacthMessage耗时过长,说明可能有卡顿了,那么如何判断耗时呢,通过logging.println的打印
      伪代码可以理解为下面这样
      logging.println(">>>>> Dispatching to " + msg.target + " " +
      msg.target.dispatchMessage(msg);
      logging.println("<<«< Finished to " + msg.target + "" + msg.callback);
      
      由于我们传入了监视器,实际上上面的代码也可以认为是这样的
      monitor.start()
      msg.target.dispatchMessage(msg);
      monitor.end()
      
  3. 同时 BlockCanary 还会在子线程中执行一个获取主线程堆栈信息的定时任务,这个 任务会在 dispatchMessage 结束的时候被移除。

现在我们知道了BlockCanary是如何打印的,那么BlockCanary又是如何定位到代码的呢,下面看下BlockCanary是如何定位代码

如何定位出错堆栈

  • LooperMonitor继承自Printer,并重写了println方法
    class LooperMonitor implements Printer {
        @Override
        public void println(String x) {
            if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
                return;
            }
            if (!mPrintingStarted) {
                mStartTimestamp = System.currentTimeMillis();
                mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
                mPrintingStarted = true;
                startDump();
            } else {
                final long endTime = System.currentTimeMillis();
                mPrintingStarted = false;
                if (isBlock(endTime)) {
                    notifyBlockEvent(endTime);
                }
                stopDump();
            }
        }
    }
    
  • 这里面主要调用了startDump()和stopDump()方法
        private void startDump() {
            if (null != BlockCanaryInternals.getInstance().stackSampler) {
                BlockCanaryInternals.getInstance().stackSampler.start();
            }
    
            if (null != BlockCanaryInternals.getInstance().cpuSampler) {
                BlockCanaryInternals.getInstance().cpuSampler.start();
            }
        }
    
        private void stopDump() {
            if (null != BlockCanaryInternals.getInstance().stackSampler) {
                BlockCanaryInternals.getInstance().stackSampler.stop();
            }
    
            if (null != BlockCanaryInternals.getInstance().cpuSampler) {
                BlockCanaryInternals.getInstance().cpuSampler.stop();
            }
        }
    
  • 我们需要关注的是stackSampler的start()和stop()方法
        public void start() {
            if (mShouldSample.get()) {
                return;
            }
            mShouldSample.set(true);
    
            HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
            HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                    BlockCanaryInternals.getInstance().getSampleDelay());
        }
    
        public void stop() {
            if (!mShouldSample.get()) {
                return;
            }
            mShouldSample.set(false);
            HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        }
    
    • 我们可以看到,在start方法中,实际上就是通过Handler延时800ms后执行了一个Runnable,Runnable中触发了doSample方法
  • doSample的具体实现在子类StackSampler中
    class StackSampler extends AbstractSampler {
            @Override
        protected void doSample() {
            StringBuilder stringBuilder = new StringBuilder();
    
            for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
                stringBuilder
                        .append(stackTraceElement.toString())
                        .append(BlockInfo.SEPARATOR);
            }
    
            synchronized (sStackMap) {
                if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                    sStackMap.remove(sStackMap.keySet().iterator().next());
                }
                sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
            }
        }
    }
    
    • 在这段代码中,创建了一个StringBuilder,然后mCurrentThread.getStackTrace()通过遍历当前线程(其实就是主线程)不断获取堆栈信息并拼接,从而实现了堆栈信息数据的收集,然后sStackMap中存储了时间戳和出错的堆栈信息,最后我们就能看到出错的堆栈信息了

800ms延时带来的问题

假设我们在点击按钮的时候依次执行如下三个耗时方法,那么最后出错的堆栈会定位到方法b,然而b才21ms,它并不应该是哪个出错的堆栈,所以可以看出BlockCanary在定位代码的时候会有一定偏差,在我们的开发中,实际情况可能比这还要复杂。

不过由于BlockCanary能够打印足够的堆栈信息供我们去分析,所以我们还是可以通过分析定位到问题代码

public void evilMethod(View view) {
        //SystemClock.sleep(1000);
        a();
        b();
        c();
    }

    public void a() {
        SystemClock.sleep(780);
    }

    public void b() {
        SystemClock.sleep(21);
    }

    public void c() {
        SystemClock.sleep(200);
    }

缺点

  • 依靠定时获取堆栈的方法,定位不够精准。
  • println 方法中会拼接字符串对象(拼接过程构造了太多StringBuilder对象)

获取方法运行时间

Choreographer监测卡顿

Android系统从4.1(API16)开始加入Choreographer这个类来控制同步处理输入(Input)、动画(Animation)、绘制(Draw)三个UI操作。
Choreographer为我们提供了检测UI绘制的回调方法,我们知道安卓UI刷新是通过VSync信号,一般为60HZ,也就是每一帧的绘制大概在16-17ms之间,如果超出这个值,那么就可以判断为产生了卡顿

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            long lastTimeNano = 0;
            @Override public void doFrame(long frameTimeNanos) {
                //两次绘制之间的耗时
                Log.e("frame", "frameTimeNanos: "+(frameTimeNanos - lastTimeNano)/(1024*1024));//16-17ms之间
                lastTimeNano = frameTimeNanos;
                Choreographer.getInstance().postFrameCallback(this);
            }
        });

hugo监测方法耗时

JakeWharton/hugo
使用JakeWharton大神的hugo,通过@DebugLog注解就可以在logcat中监测方法执行耗时

  • 注解在类名或者方法上都可以
  • 直接在类上可以监测所有方法执行耗时
@DebugLog
public class BlockCanaryActivity extends AppCompatActivity {
    ...
}

效果如下:
6

hugo的局限性

  • 侵入性太强,要在大量的类或者方法中添加注解
  • 如果有一天弃用了,需要修改大量代码

TraceView监测方法耗时(已弃用)

通过Debug开启方法追踪,会在sdcard下生成sample.trace文件,Android可以直接打开它,然后可以通过可视化图表去查看方法的执行耗时

public class BlockCanaryActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Debug.startMethodTracing("sample");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_block_canary);
        Debug.stopMethodTracing();
    }
}

弃用的原因

TraceView开销太大,监测的数据不够准确,Google已经不再推荐使用TraceView

其他性能分析工具

Systrace

  • 使用 python 终端命令生成 Trace 文件 官方文档
  • 在高版本中,可以通过 System tracing 生成 trace 文件,生成的文件可以 在这里 在线分析

Trace 分析界面常用操作:

  • W :放大 (加 Shift 效果加倍)
  • S :缩小 (加 Shift 效果加倍)
  • A :左移
  • D :右移
  • M:标记当前选中的时间线
  • 1 :选中区域
  • 2 :拖拽
  • 3 :放大缩小
  • 4 :裁剪时间线

查看函数时间

查看绘制帧
绿色表示在 16ms 内完成,⻩色和红色表示超过 16 ms

查看绘制状态

  • 绿色(Running)表示运行中
  • 蓝色(Runnbale)表示可以被运行但是没有分配到 CPU
  • 灰色(白色)(Sleeping)
  • 桔红色(Uninterruptible Sleep)表示在执行 I/O 操作

Aspect J

AspectJ是一个面向切面编程的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AspectJ还支持原生的Java,只需要加上AspectJ提供的注解即可。在Android开发中,一般就用它提供的注解和一些简单的语法就可以实现绝大部分功能上的需求了。

AspectJ现在托管于Eclipse项目中,官方网站是: AspectJ官方网站 AspectJ类库参考文档

我们知道AOP面向切面编程的 AspectJ是可以进行日志追踪、性能监控、统计埋点的,其本质也是通过织入代码的方式来实现的,比如在一个跳转新闻详情的代码在使用AspectJ之后的字节码是这样的

 @ClickCollect(
        type = "Home"
    )
    private void skipNewsDetail(String id) {
        JoinPoint var3 = Factory.makeJP(ajc$tjp_2, this, this, id);

        try {
            Intent intent = new Intent(this, NewsDetailActivity.class);
            intent.putExtra("id", id);
            this.startActivity(intent);
        } catch (Throwable var6) {
            ClickCollectAspect.aspectOf().clickCollect(var3);
            throw var6;
        }

        ClickCollectAspect.aspectOf().clickCollect(var3);
    }

ASM

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

kotlin和Groovy都是使用ASM生成字节码

ASM编写一个kotlin插件

核心是在方法前后插入代码

  • 首先创建插件类TracePlugin,注册TraceTransform(稍后编写)

    class TracePlugin : Plugin<Project> {
        override fun apply(project: Project) {
            project.extensions
                .getByType(AppExtension::class.java)
                .registerTransform(TraceTransform())
        }
    }
    
  • 编写TraceTransform代码

    • 重点:在 transform 方法中,递归遍历文件夹。
    • 然后通过 ASM 对 class 文件进行处理
  • 在ClassTraceVisitor中对类访问(visit方法)和方法访问(visitMethod)时处理代码插入逻辑

    • visit方法获取到了类名
    • visitMethod方法通过MethodTraceVisitor处理了
    class ClassTraceVisitor(cv: ClassVisitor?) : ClassVisitor(Opcodes.ASM7, cv), Opcodes {
        lateinit var className: String
    
        override fun visit(version: Int, access: Int, name: String, signature: String?,
                           superName: String?, interfaces: Array<out String>?) {
            super.visit(version, access, name, signature, superName, interfaces)
            this.className = name
        }
    
        override fun visitMethod(access: Int, name: String?, descriptor: String?,
                                 signature: String?, exceptions: Array<out String>?): MethodVisitor? {
            return MethodTraceVisitor(
                    Opcodes.ASM7,
                    super.visitMethod(access, name, descriptor, signature, exceptions),
                    access,
                    name,
                    descriptor
            ).also {
                it.className = this.className
            }
        }
    
    }
    
  • 方法访问者MethodTraceVisitor

    • 重写了onMethodEnter,即方法进入时
      • 首先方法名入栈
      • 然后访问方法指令
    • 重写了onMethodExit,即方法退出时
    class MethodTraceVisitor(api: Int, methodVisitor: MethodVisitor,
                             access: Int, name: String?, descriptor: String?)
        : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
    
        lateinit var className: String
    
        override fun onMethodEnter() {
            super.onMethodEnter()
            mv.visitLdcInsn("$className/${this.name}")
            mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                    "android/os/Trace",
                    "beginSection",
                    "(Ljava/lang/String;)V",
                    false)
        }
    
        override fun onMethodExit(opcode: Int) {
            super.onMethodExit(opcode)
    
            mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                    "android/os/Trace",
                    "endSection",
                    "()V",
                    false)
        }
    }
    
  • 接着回到TraceTransform,以下代码将增强后的字节码写入到文件中,代码就织入进去了

    val classTraceVisitor = ClassTraceVisitor(classWriter)
    classReader.accept(classTraceVisitor, EXPAND_FRAMES)
    file.writeBytes(classWriter.toByteArray())
    
使用
Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Trace.beginSection("OnCreate");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Trace.endSection();
    }
}

微信开源Matrix

Matrix 是一款微信研发并日常使用的 APM (Application Performance Manage) ,当前主要运行在 Android 平台上。Matrix 的目标是建立统一的应用性能接入框架,通过对各种性能监控方案快速集成,对性能监控项的异常数据进行采集和分析,输出相应问题的分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。

cloud.tencent.com/developer/a…

美团移动端性能监控方案Hertz

360 性能监控框架ArgusAPM