卡頓監測之遠程收集log(潛入Bugly這趟順風車)

一、問題背景

    接上一篇文章 卡頓監測之真正輕量級的卡頓監測工具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();
}
    經測試,可行,測試結果就不貼了,等與卡頓監測的代碼結合後再貼最終的測試結果。

四、與卡頓監測的代碼結合

合體!


合體後的超級賽亞人如下

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章