使用 JDK 动态代理实现简单的耗时监控工具

本文目录:

关于代理

代理模式用一句话简单概括,就是为对象创建代理,然后由代理来控制对原对象的访问。

代理模式有很多种形态,比如静态代理、动态代理。

静态代理通常需要代理对象和原对象实现同一个接口,然后使用代理对象把原对象包住,对原对象的方法调用做一些增强操作。这个有一个缺点,如果每个方法都要做同样的增强操作,那么这些操作的代码就要每个地方都写一遍,机械性劳动多,不方便维护。而且,每个不同的原对象,为了代理他都要写对应的代理对象,代理对象会变得很多。所以这里的静态,指的是代理类要人工写好。

动态代理就可以解决上面的弊端。动态代理一个很重要的特点,对象是动态生成的。常用的动态代理有 JDK 的 reflect 包提供的工具,或者使用 ASM 修改字节码。在 Android 平台上使用 ASM 会比较复杂,因为 Android 虽然使用 Java 语言开发,但实际上字节码用了自己的格式,虚拟机也不是严格的 Java 虚拟机规范。 网上有大神做了兼容方案可以使用。所以这里的冬天,指的是类在代码运行时或编译时自动生成。

我这里做了简单应用,为了在开发阶段对代码方法的耗时进行监控,使用了 JDK 动态代理就基本满足需求。

业务需求场景

先看业务需求:

项目使用了 Google 推荐的 MVP 架构,分层设计,面向接口编程,但是因为有一些执行方法发生在主线程,希望能有一个工具能够监控这些方法,设置最大耗时阈值,在耗时超过阈值后打印警告。目的就是能够开发阶段定位在主线程引起卡顿的方法。

比如我这是设置阈值为 20 ms。取这个值的原因是,如果一个主线程方法或者代码块,执行时间超过20 ms,这个会直接影响到页面的流畅度,fps 会下降,出现掉帧的情况。

怎么去实现这个业务?

有一个最简单的方式,直接在方法头记录一个时间,方法结束的时候记录一个时间:

long startTime = System.currentTimeMillis();
// 一堆业务代码
...
long passTime = System.currentTimeMillis() - startTime;
if (passTime > 20) {
     // 打印警告信息
    ...
}

这样弊端很明显,就是如此机械的代码,在每个方法体里都要来一遍。耗时耗力,侵入式代码还会影响可读性和可维护性。

所以,我们思考一个更好的方式去实现我们的目标。

JDK 动态代理可以解决这个问题。JDK 动态代理可以在运行时创建代理类,然后在每个方法执行的时候进行增强,不用写一堆的代理类,把我们的主要精力放在了增强操作的实现上面。

而且 JDK 动态代理要求原对象一定要实现某个接口,我们使用 MVP 架构完美满足要求。

简易计时工具

首先对上面的计时业务进行封装,创建一个简单的计时工具。

因为考虑到要用到会有多线程计时操作的场景,使用了 ThreadLocal 对象来记录起始时间。小工具起名为 TimeSprite,只有两个方法 start 和 stop。

搞这个工具的好处,就是不需要再复制粘贴各种计时代码。

小工具代码如下:

/**
 * 计时小工具
 *
 * @author lidiqing
 * @since 2017/12/23.
 */

public class TimeSprite {
    private static final String TAG = "TimeSprite";
    private final String mTag;
    private final ThreadLocal<Long> mStartTime;

    public TimeSprite(@NonNull String tag) {
        this.mTag = tag;
        mStartTime = new ThreadLocal<>();
    }

    public void start() {
        mStartTime.set(System.currentTimeMillis());
    }

    public void stop(@NonNull String msg) {
        if (mStartTime.get() != null) {
            long passTime = System.currentTimeMillis() - mStartTime.get();
            String threadType = Thread.currentThread() == Looper.getMainLooper().getThread() ? "main" : "worker";
            // 方法执行时间超过 20ms 会发出警告
            if (passTime > 20) {
                Log.w(TAG, String.format(Locale.getDefault(),
                        "[%s][%s][%s][%s] pass:%d", mTag, Thread.currentThread().getName(), threadType, msg, passTime));
            }
        }
    }
}

这个小工具,除了复用计时代码,还添加了阈值 20ms,在时间超过 20ms 的时候打印警告日志。我这边做得很简单,如果想做复杂一些,还可以做本地持久化,或者上报到某个云端平台做统一计算,输出可视化图表等等。简单做是个小工具,复杂点可以做一整套 APM 系统的零件。

TimeSprite 的使用方式如下:

 TimeSprite mTimeSprite = new TimeSprite("XXXX");
 ...
 public void methodXXXX() {
    mTimeSprite.start();
    // 业务代码
    ...
    mTimeSprite.stop("methodXXXX");
 }

使用注解进行标记

这里引入注解的目标,就是为方法打标记。因为我们不希望对所有都去进行监控,原因是有的方法天然不耗时,比如简单的 Getter 和 Setter 方法 。

有注解在会更灵活:

/**
 * 注解,需要进行监控的方法进行标注
 *
 * @see MonitorProxyHandler
 *
 * @author lidiqing
 * @since 2017/12/22.
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnableMonitor {

监控代理 MonitorProxy

进入主题,监控代理业务的实现怎么写。其实很简单,就是创建 InvocationHandler 实现类的过程。

创建一个 MonitorProxyHandler 实现 InvocationHandler 接口,把原对象包住。在每个方法的时候,会去调用 MonitorProxyHandler 的 invoke 方法,这时候去判断是否有被 @EnableMonitor 注解,有的话使用 TimeSprite 打印耗时。

是不是很简单?

/**
 * 方法的执行时间监控
 *
 * @author lidiqing
 * @since 2017/12/22.
 */

class MonitorProxyHandler implements InvocationHandler {

    private final Object mTarget;
    private final TimeSprite mTimeSprite;

    MonitorProxyHandler(@NonNull Object obj) {
        mTarget = obj;
        mTimeSprite = new TimeSprite(obj.getClass().getName());
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        EnableMonitor enable = method.getAnnotation(EnableMonitor.class);
        if (enable != null) {
            mTimeSprite.start();
        }
        Object result = method.invoke(mTarget, args);
        if (enable != null) {
            mTimeSprite.stop(method.getName());
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    <T> T create() {
        return (T) Proxy.newProxyInstance(
                mTarget.getClass().getClassLoader(),
                mTarget.getClass().getInterfaces(),
                this);
    }
}

最后面,在 create 方法中,传入代理业务处理类 MonitorProxyHandler 的实例,原对象的接口和类加载器,使用 JDK 的 Proxy 工厂动态地生产出一个代理对象来。这个对象和原对象实现同样的接口,行为一致。外界直接调用代理对象,就和调用原对象一样。

为了更方便的使用,我们也建一个简单工厂。把 MonitorProxyHandler 做成包可见,把这些实现细节都包住,不让外界接触到。创建 MonitorProxy 类,去包住希望被代理的类。

/**
 * @author lidiqing
 * @since 2018/5/26.
 */

public class MonitorProxy {

    private MonitorProxy() {
    }

    public static <T> T wrap(@NonNull Object obj) {
        return new MonitorProxyHandler(obj).create();
    }
}

使用者只需要加个注解,然后在对象创建时调用 MonitorProxy.wrap 方法,就可以监控该方法的耗时了。

使用举例

比如有这么一个 MVP 结构的业务模块。我们直接在其接口上注解要监控的方法:

public interface Contract {
    interface View {    
        ...
        @EnableMonitor
        @MainThread
        void showListRefreshSuccess();  
        ...
    }   

    interface Presenter {
        ...
        @EnableMonitor
        @MainThread
        void handleRefresh();
        ...
    }
}

然后然后实现类创建的地方,使用 MonitorProxy 做一下代理。

比如拿这里 MVP 的 Presenter 的实现类为 SimplePresenter 来举例。我只想要 debug 状态下去进行监控,于是直接在它的静态工厂方法中加入代理,而不用在每个方法中写侵入式代码:

public class SimplePresenter implements Contract.Presenter {
    ...
    public static Contract.Presenter newInstance(Contract.View view) {
        Contract.Presenter presenter = new SimplePresenter(view);
        if (Config.DEBUG) {
            presenter = MoniotrProxy.wrap(presenter);
        }       
        return presenter;
    }
    ...
    @Override
    publie void handleRefresh() {
        ...
    }
    ...
}

然后,如果 handleRefresh 方法执行超时后,LogCat 会有这样警告日志:

...
W/TimeSprite: [SimplePresenter][main][main][handleRefresh] pass:29
W/TimeSprite: [SimplePresenter][main][main][handleRefresh] pass:28
W/TimeSprite: [SimplePresenter][main][main][handleRefresh] pass:20
W/TimeSprite: [SimplePresenter][main][main][handleRefresh] pass:23
...

这样子,我们开发的时候,就可以针对性对做优化,避免某些卡顿或者掉帧的操作影响到用户体验。有些小卡顿我们可能不重视,但积少成多,体验下降。同时如果系统内存吃紧或者 CPU 繁忙,这些问题可能会被放大,严重到可能会有 ANR 异常。

做这个工具的目的,提供一种简易的检测方式,尽量地在开发中就把卡顿问题扼杀在摇篮里。

猜你喜欢

转载自blog.csdn.net/firefile/article/details/80566841
今日推荐