一、問題背景
接上一篇文章 卡頓監測之真正輕量級的卡頓監測工具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