本文目录:
关于代理
代理模式用一句话简单概括,就是为对象创建代理,然后由代理来控制对原对象的访问。
代理模式有很多种形态,比如静态代理、动态代理。
静态代理通常需要代理对象和原对象实现同一个接口,然后使用代理对象把原对象包住,对原对象的方法调用做一些增强操作。这个有一个缺点,如果每个方法都要做同样的增强操作,那么这些操作的代码就要每个地方都写一遍,机械性劳动多,不方便维护。而且,每个不同的原对象,为了代理他都要写对应的代理对象,代理对象会变得很多。所以这里的静态,指的是代理类要人工写好。
动态代理就可以解决上面的弊端。动态代理一个很重要的特点,对象是动态生成的。常用的动态代理有 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 异常。
做这个工具的目的,提供一种简易的检测方式,尽量地在开发中就把卡顿问题扼杀在摇篮里。