导言

忙忙碌碌又一天,最近学习了一个新项目,每天都是吭哧吭哧的去看项目中的各种源码,都来自于各路大神的奉献。每看到一块地方的时分都感觉触到了自己的知识鸿沟,然后又得吭哧吭哧的去查找。不知道大家的查找途径是什么,我的查找途径从以前的 Google -> -> 简书 -> Other(Baidu) 变成了 ChatGPT -> Google -> 其他

Android 怎么监控 Loop Message 长音讯

最近在排查主线程耗时的一个任务,既然在主线程了,那还不好办,直接上 Trace 分析主线程中的耗时任务都有哪些不就完了。完了分析完一波后又无从下手啦。接下来考虑从 MainLooper 下手吧,看看哪些是长音讯,履行任务又比较耗时。

Android 怎么监控 Loop Message 长音讯

Handler

Looper

那么怎么做长音讯的耗时监控呐?这又遇到了一个问题,在以往许多都是经过 LooperPrinter 进行控制长音讯的检查,但是 Printer 也是有缺点的,便是无法知道它的 callback 来自于哪里,及时统计出来了时刻,也不满足需求。

一切的源头都应该从源码进行着手,经过阅览源码发现,在 Looper 中发现了 Observer,它是什么鬼?

public final class Looper {
    private static Observer sObserver;
    /**  
    * Set the transaction observer for all Loopers in this process.  
    *  
    * @hide  
    */  
    public static void setObserver(@Nullable Observer observer) {  
        sObserver = observer;  
    }
    public static void loop() {
        // ...此处省掉无关紧要的 code
        for (;;) {
            // ...此处省掉无关紧要的 code
            // This must be in a local variable, in case a UI event sets the logger  
            final Printer logging = me.mLogging;  
            if (logging != null) {  
            logging.println(">>>>> Dispatching to " + msg.target + " " +  
            msg.callback + ": " + msg.what);  
            }  
            // Make sure the observer won't change while processing a transaction.  
            final Observer observer = sObserver;
            // ...此处省掉无关紧要的 code
            Object token = null;
            // 在履行音讯履行音讯告诉
            if (observer != null) {  
                token = observer.messageDispatchStarting();  
            }  
            long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);  
            try {  
                msg.target.dispatchMessage(msg);  
                // 在音讯履行结束后又履行了音讯告诉
                if (observer != null) {  
                    observer.messageDispatched(token, msg);  
                }  
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;  
            } catch (Exception exception) {  
                // 音讯履行的进程中产生了异常的回调
                if (observer != null) {  
                    observer.dispatchingThrewException(token, msg, exception);  
                }  
                throw exception;  
            } finally {  
                ThreadLocalWorkSource.restore(origWorkSource);  
                if (traceTag != 0) {  
                    Trace.traceEnd(traceTag);  
                }  
            }
        }
    }
}

依据阅览源码来看,我觉得能够从这个地方下手搞工作。

/** {@hide} */
public interface Observer {  
    /**  
    * Called right before a message is dispatched.  
    *  
    * <p> The token type is not specified to allow the implementation to specify its own type.  
    *  
    * @return a token used for collecting telemetry when dispatching a single message.  
    * The token token must be passed back exactly once to either  
    * {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException}  
    * and must not be reused again.  
    *  
    */  
    Object messageDispatchStarting();  
    /**  
    * Called when a message was processed by a Handler.  
    *  
    * @param token Token obtained by previously calling  
    * {@link Observer#messageDispatchStarting} on the same Observer instance.  
    * @param msg The message that was dispatched.  
    */  
    void messageDispatched(Object token, Message msg);  
    /**  
    * Called when an exception was thrown while processing a message.  
    *  
    * @param token Token obtained by previously calling  
    * {@link Observer#messageDispatchStarting} on the same Observer instance.  
    * @param msg The message that was dispatched and caused an exception.  
    * @param exception The exception that was thrown.  
    */  
    void dispatchingThrewException(Object token, Message msg, Exception exception);  
}

Observer 是 Looper 的一个内部接口类,用来做事件的回调处理的。首要包含了三个函数 messageDispatchStartingmessageDispatcheddispatchingThrewException,别离会在某条音讯调度前、调度处理后、调度处理进程中产生异常时回调。需求注意的是 messageDispatchStarting 要求回来一个 Object 对象,对类型不做任何约束,每个音讯类型都对应一个 token,并且理应为独立且仅有。

于是乎,我兴高采烈的开端码起来了。

开搞开搞

先来介绍一会儿思路,咱们是不是能够直接经过反射调用 setObserver 的方法,是不是直接就能够设置Observer,既然都到了反射,为啥不直接操作 sObserver,提到这就开干。这儿运用的动态署理进行 hook Observer

public class HandlerLoopHookHelper {
    /**  
    * Hook Looper 的 sObserver 调查音讯耗时  
    *  
    * @param context 上下文  
    */  
    public static void hookLooperObserver(@Nullable Context context) {  
        Log.d(TAG, "hookLoopObserver: " + Build.VERSION.SDK_INT);  
            try {  
                @SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");  
                final ObserverInvocation invocation = new ObserverInvocation();  
                final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));  
                final Class<?> looperClass = Class.forName("android.os.Looper");  
                @SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");  
                sObserver.setAccessible(true);  
                sObserver.set(getMainLooper(), o);  
                getMainLooper().setMessageLogging(invocation.printer);  
            } catch (Throwable e) {  
                e.printStackTrace();  
            }  
        }
}
public class ObserverInvocation implements InvocationHandler {
    private long dispatchStart = 0;  
    private long dispatchEnd = 0;
    private static final long MESSAGE_WORK_TIME_300 = 300;  
    private static final long MESSAGE_WORK_TIME_100 = 100;  
    private static final long MESSAGE_WORK_TIME_50 = 50;  
    private static final long MESSAGE_WORK_TIME_10 = 10;
    @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
        if (Looper.getMainLooper().isCurrentThread()) {  
            if ("messageDispatchStarting".equals(method.getName())) {  
                return messageDispatchStarting();  
            } else if ("messageDispatched".equals(method.getName())) {  
                messageDispatched((long) args[0], (Message) args[1]);  
            } else if ("dispatchingThrewException".equals(method.getName())) {  
                dispatchingThrewException((long) args[0], (Message) args[1], (Exception) args[2]);  
            }  
        }  
        return null;  
    }
    public Object messageDispatchStarting() {  
        dispatchStart = SystemClock.uptimeMillis();  
        final long token = atomicLong.getAndIncrement();  
        return token;  
    }  
    public void messageDispatched(Object token, Message msg) {  
        dispatchEnd = SystemClock.uptimeMillis();  
        getTime(msg, (long) token);  
    }  
    public void dispatchingThrewException(Object token, Message msg, Exception exception) {  
    }
    /**
     * 计时,可依据自己的需求进行具体的计
     */
    private void getTime(Message message, long token) {  
        // message 指定开端时刻,基于开机时刻  
        final long when = message.getWhen();  
        // 等候的时长:开端履行 - 指定开端时刻  
        final long wait = dispatchStart - when;  
        // 履行的时长:履行结束时刻 - 履行开端时刻  
        final long work = dispatchEnd - dispatchStart;  
        printRecord(message, token, wait, work);  
    }  
    private void printRecord(Message message, long token, long wait, long work) {  
        final StringBuilder stringBuilder = new StringBuilder();  
        if (work <= MESSAGE_WORK_TIME_10) {  
            stringBuilder.append("(可忽略)").append(dispatchStart).append("-").append(dispatchEnd)  
            .append(">>> token: ").append(token).append(message.toString())  
            .append(" wait: ").append(wait).append("ms")  
            .append(" work: ").append(work).append("ms");  
            Log.v(TAG, stringBuilder.toString());  
        } else if (work <= MESSAGE_WORK_TIME_50) {  
            stringBuilder.append("(看看就好)").append(dispatchStart).append("-").append(dispatchEnd)  
            .append(">>> token: ").append(token).append(message.toString())  
            .append(" wait: ").append(wait).append("ms")  
            .append(" work: ").append(work).append("ms");  
            Log.i(TAG, stringBuilder.toString());  
        } else if (work <= MESSAGE_WORK_TIME_100) {  
            stringBuilder.append("(需求重视一下)").append(dispatchStart).append("-").append(dispatchEnd)  
            .append(">>> token: ").append(token).append(message.toString())  
            .append(" wait: ").append(wait).append("ms")  
            .append(" work: ").append(work).append("ms");  
            Log.w(TAG, stringBuilder.toString());  
        } else if (work <= MESSAGE_WORK_TIME_300) {  
            stringBuilder.append("(需求处理啦~)").append(dispatchStart).append("-").append(dispatchEnd)  
            .append(">>> token: ").append(token).append(message.toString())  
            .append(" wait: ").append(wait).append("ms")  
            .append(" work: ").append(work).append("ms");  
            Log.d(TAG, stringBuilder.toString());  
        } else {  
            stringBuilder.append("(这个就超级严重啦~)").append(dispatchStart).append("-").append(dispatchEnd)  
            .append(">>> token: ").append(token).append(message.toString())  
            .append(" wait: ").append(wait).append("ms")  
            .append(" work: ").append(work).append("ms");  
            Log.e(TAG, stringBuilder.toString());  
        }  
    }
}

Coding 结束啦,这儿面的输出监控建议不要搞到线上,或许会比较的耗时,依据实际情况来确认怎么运用,定制监控的策略,能够考虑超越多长时刻来进行报警,或许上报等操作。废话不多说,run 一会儿吧,结果gg。

Android 怎么监控 Loop Message 长音讯

日志的大致意思是,这个类是被 @hide 的,对开发者是屏蔽调用及运用的。查阅相关代码得知,基于 Android 10 版别增加的 Observer,在规划之初就只是为了调查并统计体系服务的Looper音讯调度功用(能够查阅LooperStatsLooperStatsService),所以这个API只给自己内部运用。

只要是代码总会有处理的方案,能不能绕过去躲藏 API 呐?能,下来就看看看一下怎么如绕开躲藏API 。

Android 怎么监控 Loop Message 长音讯

绕开 Observer 躲藏 API

处理 Hidden API 的方法有许多种,或许某些在 Android 高版别体系重被官方封堵。不过因为 Android 体系是开源的,所以无论怎么封堵,仍是能够经过其他的方法绕过,毕竟体系是不会约束用户修正自身进程的内存。

在这儿我运用的方案是 FreeReflection,大致的思路是将自己伪装成体系类,然后就能够调用这些私有 API 了。具体的内容能够参阅作者博客里面的介绍。

另一种绕过 Android P以上非揭露API约束的方法

Android 14 无法加载.dex文件

在这儿提个醒,在Android 14及以上版别,关于动态代码的加载产生了一些更改,否则会产生如下内容过错。

Android 怎么监控 Loop Message 长音讯

经过阅览 DexClassLoader更安全的动态代码加载 发现 Android 14 关于动态加载 .dex 文件做了安全策略的约束,Android 14 关于动态加载(DCL)功用,必须将所有动态加载的文件标记为只读。否则,体系将会抛出异常。

Android 怎么监控 Loop Message 长音讯

正确操作姿态,便是将 FreeReflection 三方库 Copy 下来,然后修正源码,完美处理在 Android 14 上面不能动态加载 .dex 问题。

Android 怎么监控 Loop Message 长音讯

然后再跑一会儿。完美处理。

Android 怎么监控 Loop Message 长音讯

正确的打开姿态

public class HandlerLoopHookHelper {
    private static volatile boolean isHooked = false;  
    private static final String TAG = "HandlerLoopHookHelper";  
    private static void checkHooked(Context context) {  
        if (!isHooked) {  
            Reflection.unseal(context);  
            isHooked = true;  
        }  
    }  
    /**  
    * Hook Looper 的 sObserver 调查音讯耗时  
    *  
    * @param context 上下文  
    */  
    public static void hookLooperObserver(@Nullable Context context) {  
        Log.d(TAG, "hookLoopObserver: " + Build.VERSION.SDK_INT);  
        try {  
            checkHooked(context);  
            @SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");  
            final ObserverInvocation invocation = new ObserverInvocation();  
            final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));  
            final Class<?> looperClass = Class.forName("android.os.Looper");  
            @SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");  
            sObserver.setAccessible(true);  
            sObserver.set(getMainLooper(), o);  
            getMainLooper().setMessageLogging(invocation.printer);  
        } catch (Throwable e) {  
            e.printStackTrace();  
        }  
    }  
    /**  
    * 钩针活套调查者  
    *  
    * @param context 上下文  
    */  
    public static void unHookLooperObserver(@Nullable Context context) {  
        Log.d(TAG, "unHookLooperObserver: " + Build.VERSION.SDK_INT);  
        try {  
            checkHooked(context);  
            @SuppressLint("PrivateApi") final Class<?> sObserverClass = Class.forName("android.os.Looper$Observer");  
            final ObserverInvocation invocation = new ObserverInvocation();  
            final Object o = sObserverClass.cast(Proxy.newProxyInstance(sObserverClass.getClassLoader(), new Class[]{sObserverClass}, invocation));  
            final Class<?> looperClass = Class.forName("android.os.Looper");  
            @SuppressLint("BlockedPrivateApi") final Field sObserver = looperClass.getDeclaredField("sObserver");  
            sObserver.setAccessible(true);  
            sObserver.set(getMainLooper(), null);  
            getMainLooper().setMessageLogging(null);  
        } catch (Throwable e) {  
            e.printStackTrace();  
        }  
    }  
    /**  
    * 挂钩主活套音讯空闲处理程序  
    *  
    * @param context 上下文  
    */  
    public static void hookMainLooperMessageIdleHandlers(@Nullable Context context) {  
        Log.d(TAG, "hookMainLooperMessageIdleHandlers: " + Build.VERSION.SDK_INT);  
        try {  
            checkHooked(context);  
            final Class<?> looperClass = Class.forName("android.os.Looper");  
            final Field mQueueF = looperClass.getDeclaredField("mQueue");  
            mQueueF.setAccessible(true);  
            final Object mainMessageQueue = mQueueF.get(getMainLooper());  
            final Class<?> mQueueClass = Class.forName("android.os.MessageQueue");  
            final Field mainIdleHandlerF = mQueueClass.getDeclaredField("mIdleHandlers");  
            mainIdleHandlerF.setAccessible(true);  
            final Object o = mainIdleHandlerF.get(mainMessageQueue);  
            final ArrayList<MessageQueue.IdleHandler> mIdleHandlers = (ArrayList<MessageQueue.IdleHandler>) o;  
            Log.d(TAG, "hookMainLooperMessageIdleHandlers size: " + mIdleHandlers.size());  
            for (MessageQueue.IdleHandler mIdleHandler : mIdleHandlers) {  
                Log.d(TAG, "hookMainLooperMessageIdleHandlers content is: " + mIdleHandler.toString());  
            }  
        } catch (Throwable e) {  
            e.printStackTrace();  
        }  
    }  
}

Android 怎么监控 Loop Message 长音讯

小结

  • Observer 相比于 Printer 能够直接拿到 Message 对象,并且不需求设置 Printer 能够防止每个音讯调度时额定拼接字符串的成本。
  • 处理开发阶段 Hidden API 访问约束,能够经过许多种方法绕过,也能够考虑经过 CompileOnly 一个 假工程 来完成,假工程 里面模仿相应的体系源码,从而完成 Observer 类的访问。不过我试过这种方法,同样也需求搞定体系的 hidden api 形似也是绕不过去的,如果运用这个库 FreeReflection 的话,那么 假工程 的方法也是能够完成,在这儿不做赘述,感兴趣的同学能够自己尝试。
  • Observer 机制是在 Anroid 10 开端增加的,因此低版别仍是需求用 Printer 的方法进行监听
  • Android 14 版别动态加载 .dex 文件需求设置 file 为只读模式,否则将会抛出安全异常。