一、问题背景
接上一篇文章 卡顿监测之真正轻量级的卡顿监测工具BlockDetectUtil(仅一个类) 这篇文章实现了一个轻量级的卡顿监测工具,通过logCat输出log的形式找出卡顿的元凶,可以很方便的在开发中使用,但现在摆在眼前的问题就是当项目上线后,或者遇到无法查看logCat的情况,就不能查看监测的log,尤其是上线后在不同用户的各种各样的手机中,出现卡顿问题几率就更大了,这时候无法查看到log,就无法针对性的排查问题。所以现在就需要接入远程log收集的功能,那么可以让后台写一个提交log数据的接口,然后做个前端展示,但是这些都是需要成本的。所以不妨想想还有什么别的现成的方案,然后我就想到了Bugly。
那Bugly是什么呢?Bugly是腾讯出品的一个工具集,支持APP应用崩溃日志分析、ANR分析、APP升级,热更新等,由于它傻瓜式的接入,并且信息界面友好、明朗等优点,相信不少开发者都在项目中用到了它。那么针对APP应用崩溃日志分析这一项功能来说,它是可以在应用崩溃时抓取log发送到后台,开发者在后台可以实时地查看到崩溃日志,而且还可以看到相关手机信息,既然它可以在本地抓取异常信息发给后台,那么我们也就肯定可以伪造异常信息(实际是卡顿的堆栈)发送到Bugly后台。有的同学可能要问了,Bugly已经支持采集ANR了,为什么还要卡顿监测,注意了,这里ANR是卡死非卡顿,卡死是卡顿时间达到一定程度所造成的结果。好了,回归正题,既然可以伪造异常,那么当务之急就是得研究Bugly的源码找出它是在哪里提交异常的。
二、研究Bugly源码
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(); }经测试,可行,测试结果就不贴了,等与卡顿监测的代码结合后再贴最终的测试结果。
四、与卡顿监测的代码结合
合体!
合体后的超级赛亚人如下
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(); } } }
接下来就是验证了
四、验证
验证代码:
验证结果
可见,卡顿的堆栈数据已经成功地提交到了Bugly的后台,当然不仅仅堆栈数据,还有其他相关的机型信息,这些都可以帮助我们更好地排查问题。这里可以延伸一下的就是,我们不仅可以利用Bugly这趟顺风车来远程收集应用卡顿的堆栈log,还可以传递其他的数据,这里就需要发挥各位老司机的想象力,看怎么来好好利用这趟免费的顺风车了。
五、总结
这篇文章主要讲解了如何利用现有的log采集工具Bugly来远程收集应用的卡顿信息,以及展示了超级赛亚人合体之强大。最后,相关源码请前往github处查阅,喜欢的点个 ★ 哦 !您的支持,是我荆棘道路上前行的动力。
https://github.com/qq542391099/BlockCollect