卡顿监测之远程收集log(潜入Bugly这趟顺风车)

时间:2024-04-04 18:04:44

一、问题背景

    接上一篇文章 卡顿监测之真正轻量级的卡顿监测工具BlockDetectUtil(仅一个类) 这篇文章实现了一个轻量级的卡顿监测工具,通过logCat输出log的形式找出卡顿的元凶,可以很方便的在开发中使用,但现在摆在眼前的问题就是当项目上线后,或者遇到无法查看logCat的情况,就不能查看监测的log,尤其是上线后在不同用户的各种各样的手机中,出现卡顿问题几率就更大了,这时候无法查看到log,就无法针对性的排查问题。所以现在就需要接入远程log收集的功能,那么可以让后台写一个提交log数据的接口,然后做个前端展示,但是这些都是需要成本的。所以不妨想想还有什么别的现成的方案,然后我就想到了Bugly。

    那Bugly是什么呢?Bugly是腾讯出品的一个工具集,支持APP应用崩溃日志分析、ANR分析、APP升级,热更新等,由于它傻瓜式的接入,并且信息界面友好、明朗等优点,相信不少开发者都在项目中用到了它。那么针对APP应用崩溃日志分析这一项功能来说,它是可以在应用崩溃时抓取log发送到后台,开发者在后台可以实时地查看到崩溃日志,而且还可以看到相关手机信息,既然它可以在本地抓取异常信息发给后台,那么我们也就肯定可以伪造异常信息(实际是卡顿的堆栈)发送到Bugly后台。有的同学可能要问了,Bugly已经支持采集ANR了,为什么还要卡顿监测,注意了,这里ANR是卡死非卡顿,卡死是卡顿时间达到一定程度所造成的结果。好了,回归正题,既然可以伪造异常,那么当务之急就是得研究Bugly的源码找出它是在哪里提交异常的。

二、研究Bugly源码

    由于代码是混淆的,所以研究起来得有些耐心,首先明确一点的就是它肯定写了一个全局异常捕获的类,这个类实现了Thread.UncaughtExceptionHandler,拿到它的jar包,通过jadx打开,全局搜索实现了这个接口的类,找到了这个类e:
public final class e implements Thread.UncaughtExceptionHandler {
    private Context a;
    private com.tencent.bugly.crashreport.crash.b b;
    private com.tencent.bugly.crashreport.common.strategy.a c;
    private com.tencent.bugly.crashreport.common.info.a d;
    private Thread.UncaughtExceptionHandler e;
    private Thread.UncaughtExceptionHandler f;
    private boolean g = false;
    private static String h = null;
    private static final Object i = new Object();
    private int j;

    public e(Context var1, b var2, a var3, com.tencent.bugly.crashreport.common.info.a var4) {
        this.a = var1;
        this.b = var2;
        this.c = var3;
        this.d = var4;
    }
    ...

    public final void uncaughtException(Thread var1, Throwable var2) {
        Object var3 = i;
        synchronized (i) {
            this.a(var1, var2, true, (String) null, (byte[]) null);
        }
    }
    ...
}

    那么异常都是通过这个接口的uncaughtException方法回调的,那么我们可以拿到这个类的实例,然后直接伪造一个异常给这个方法吗?显然是不可以的,因为通过这个方法最终会在如下代码里交给系统来处理这个异常,就直接崩溃了

 finally {
    if(var3) {
        if(this.e != null && a(this.e)) {
            x.e("sys default last handle start!", new Object[0]);
            this.e.uncaughtException(var1, var2);
            x.e("sys default last handle end!", new Object[0]);
        }

    所以这里我们得找到提交异常到Bugly后台具体方法,经过我多次的调试找到了此方法,那么我是怎么调试的呢,单步调试,执行一个方法就刷新一下Bugly的后台看异常提交上来没有,虽然有点笨,但是很实用,没几下就找到了,就是下面代码里的this.b.a(var11, 3000L, var3),而且也可以看出它是构造了一个CrashDetailBean对象,然后提交这个对象的。

CrashDetailBean var11;
if((var11 = this.b(var1, var2, var3, var4, var5)) != null) {
    b.a(var3?"JAVA_CRASH":"JAVA_CATCH", z.a(), this.d.d, var1, z.a(var2), var11);
    if(!this.b.a(var11)) {
        this.b.a(var11, 3000L, var3);
    }

    this.b.b(var11);
    return;
}

    方法找到了,就是b的a方法,那么我们要调用a方法,就必须得有b实例,而这里b是e的成员变量,所以找到e实例就可以获取b实例,那就先找找e是在那被实例化的。通过全局搜索new e找到具体代码this.r = new e(var2, this.o, this.t, var10),它是在c的构造器里被初始化的。

private c(int var1, Context var2, w var3, boolean var4, com.tencent.bugly.BuglyStrategy.a var5, o var6, String var7) {
    a = var1;
    var2 = z.a(var2);
    this.p = var2;
    this.t = a.a();
    this.u = var3;
    u var8 = u.a();
    p var9 = p.a();
    this.o = new b(var1, var2, var8, var9, this.t, var5, var6);
    com.tencent.bugly.crashreport.common.info.a var10 = com.tencent.bugly.crashreport.common.info.a.a(var2);
    this.r = new e(var2, this.o, this.t, var10);
    this.s = NativeCrashHandler.getInstance(var2, var10, this.o, this.t, var3, var4, var7);
    var10.D = this.s;
    this.v = new com.tencent.bugly.crashreport.crash.anr.b(var2, this.t, var10, var3, this.o);
}

    要想获得e实例,既是c里的成员变量r,那么只要获得c实例即可,接下来继续找c是在哪里被实例化的,经过一番查找,找到了它的实例化代码

public static synchronized void a(int var0, Context var1, boolean var2, com.tencent.bugly.BuglyStrategy.a var3, o var4, String var5) {
    if(q == null) {
        q = new c(1004, var1, w.a(), var2, var3, (o)null, (String)null);
    }

}

    看到这个静态方法就鸡冻了有木有,因为显然这个c的实例q是个静态的对象了,那么就好办了

private static c q;
    果不其然,那么接下来就可以编写代码了。

三、编写代码

    代码很好写,无非就是反射,按照上面的思路,简单的测试代码就出来了(需要导入Bugly的包,最好就是你的项目里已经用着Bugly了,当然以下代码得放在Bugly初始化的后面)

try {
   Object c = ReflectUtil.getStaticField("com.tencent.bugly.crashreport.crash.c","q");
   e e = ReflectUtil.getField(c,"r");
   b b = ReflectUtil.getField(e,"b");
   CrashDetailBean crashDetailBean = (CrashDetailBean) ReflectUtil.invokeMethod(e, "b",
         new Class[]{Thread.class,Throwable.class,boolean.class,String.class,byte[].class},
         new Object[]{Thread.currentThread(),new Throwable("卡顿监测"),true,null,null});
   b.a(crashDetailBean, 3000L, true);
} catch (Exception e) {
   e.printStackTrace();
}
    经测试,可行,测试结果就不贴了,等与卡顿监测的代码结合后再贴最终的测试结果。

四、与卡顿监测的代码结合

合体!

卡顿监测之远程收集log(潜入Bugly这趟顺风车)

合体后的超级赛亚人如下

public class BlockDetectUtil {

    private static final int TIME_BLOCK = 600;//阈值
    private static final int FREQUENCY = 6;//采样频率
    private static Handler mIoHandler;
    public static void start() {
        HandlerThread mLogThread = new HandlerThread("yph");
        mLogThread.start();
        mIoHandler = new Handler(mLogThread.getLooper());
        mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK/FREQUENCY);
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                mIoHandler.removeCallbacks(mLogRunnable);
                mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK/FREQUENCY);
                Choreographer.getInstance().postFrameCallback(this);
            }
        });
    }
    private static Runnable mLogRunnable = new Runnable() {

        int time = FREQUENCY;
        List<String> list = new ArrayList();
        HashMap<String,StackTraceElement[]> hashMap = new HashMap();
        @Override
        public void run() {
            if(Debug.isDebuggerConnected())return;
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString() + "\n");
            }
            list.add(sb.toString());
            hashMap.put(sb.toString(),stackTrace);
            time -- ;
            if(time == 0) {
                time = FREQUENCY;
                reList(list);
                for(String s : list) {
                    Log.e("BlockDetectUtil", s);
                    toBugly(hashMap.get(s));
                }
                list.clear();
                hashMap.clear();
            }else
                mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK/FREQUENCY);
        }
    };
    private static void reList(List<String> list){
        List<String> reList = new ArrayList<>();
        String lastLog = "";
        for(String s : list){
            if(s.equals(lastLog) && !reList.contains(s)) {
                reList.add(s);
            }
            lastLog = s;
        }
        list.clear();
        list.addAll(reList);
    }
    private static void toBugly(StackTraceElement[] stacks){
        Throwable throwable = new Throwable("卡顿监测");
        throwable.setStackTrace(stacks);
        try {
            Object c = ReflectUtil.getStaticField("com.tencent.bugly.crashreport.crash.c","q");
            e e = ReflectUtil.getField(c,"r");
            b b = ReflectUtil.getField(e,"b");
            CrashDetailBean crashDetailBean = (CrashDetailBean) ReflectUtil.invokeMethod(e, "b",
                    new Class[]{Thread.class,Throwable.class,boolean.class,String.class,byte[].class},
                    new Object[]{Looper.getMainLooper().getThread(),throwable,true,null,null});
            b.a(crashDetailBean, 3000L, true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

    接下来就是验证了

四、验证

验证代码:

卡顿监测之远程收集log(潜入Bugly这趟顺风车)

卡顿监测之远程收集log(潜入Bugly这趟顺风车)

验证结果

卡顿监测之远程收集log(潜入Bugly这趟顺风车) 可见,卡顿的堆栈数据已经成功地提交到了Bugly的后台,当然不仅仅堆栈数据,还有其他相关的机型信息,这些都可以帮助我们更好地排查问题。这里可以延伸一下的就是,我们不仅可以利用Bugly这趟顺风车来远程收集应用卡顿的堆栈log,还可以传递其他的数据,这里就需要发挥各位老司机的想象力,看怎么来好好利用这趟免费的顺风车了。

五、总结

    这篇文章主要讲解了如何利用现有的log采集工具Bugly来远程收集应用的卡顿信息,以及展示了超级赛亚人合体之强大。最后,相关源码请前往github处查阅,喜欢的点个  哦 !您的支持,是我荆棘道路上前行的动力。

https://github.com/qq542391099/BlockCollect