Android 监控如何 Loop Message 长消息

引言

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

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

Handler

Looper

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

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

java 复制代码
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);  
                }  
            }
        }
    }
}

根据阅读源码来看,我觉得可以从这个地方下手搞事情。

java 复制代码
/** {@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

kotlin 复制代码
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。

日志的大致意思是,这个类是被 @hide 的,对开发者是屏蔽调用及使用的。查阅相关代码得知,基于 Android 10 版本增加的 Observer,在设计之初就只是为了观察并统计系统服务的Looper消息调度性能(可以查阅LooperStatsLooperStatsService),所以这个API只给自己内部使用。

只要是代码总会有解决的方案,能不能绕过去隐藏 API 呐?能,下来就看看看一下如何如绕开隐藏API 。

绕开 Observer 隐藏 API

解决 Hidden API 的方式有很多种,可能某些在 Android 高版本系统重被官方封堵。不过由于 Android 系统是开源的,所以无论怎么封堵,还是可以通过其他的方式绕过,毕竟系统是不会限制用户修改自身进程的内存。

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

另一种绕过 Android P以上非公开API限制的办法

Android 14 无法加载.dex文件

在这里提个醒,在Android 14及以上版本,对于动态代码的加载发生了一些更改,否则会发生如下内容错误。

通过阅读 DexClassLoader更安全的动态代码加载 发现 Android 14 对于动态加载 .dex 文件做了安全策略的限制,Android 14 对于动态加载(DCL)功能,必须将所有动态加载的文件标记为只读。否则,系统将会抛出异常。

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

然后再跑一下子。完美解决。

正确的打开姿势

java 复制代码
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();  
        }  
    }  
}

小结

  • Observer 相比于 Printer 可以直接拿到 Message 对象,并且不需要设置 Printer 可以避免每个消息调度时额外拼接字符串的成本。
  • 解决开发阶段 Hidden API 访问限制,可以通过很多种方式绕过,也可以考虑通过 CompileOnly 一个 假工程 来实现,假工程 里面模拟相应的系统源码,从而实现 Observer 类的访问。不过我试过这种方法,同样也需要搞定系统的 hidden api 貌似也是绕不过去的,如果使用这个库 FreeReflection 的话,那么 假工程 的方式也是可以实现,在这里不做赘述,感兴趣的同学可以自己尝试。
  • Observer 机制是在 Anroid 10 开始添加的,因此低版本还是需要用 Printer 的方式进行监听
  • Android 14 版本动态加载 .dex 文件需要设置 file 为只读模式,否则将会抛出安全异常。
相关推荐
深海呐2 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang2 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼2 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss4 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19436 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男7 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽8 小时前
Android 源码集成可卸载 APP
android
码农明明8 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风9 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教10 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python