监控Android Looper Message调度的另一种姿势

作者:卓_修武
转载地址:https://juejin.cn/post/7139741012456374279

背景

在Android 10版本,系统为Looper类添加了专门的 Observer类用来观测Looper的消息调度。因此除了通过设置Looper对象的 printer属性外,也可以通过设置Looper类的Observer属性来实现监控,然而该功能在设计之初就只是为了观测并统计系统服务的Looper消息调度性能 (其系统使用见LooperStats类 及 LooperStatsService类 ),因此其Looper.Observer类及其相关API都被标记为 @hidden ,对开发者屏蔽其相关API的使用。

本文主要分享使用Looper Observer 进行消息调度观测过程中的遇到的问题及解决方式。

Looper Observer 源码实现

    class Looper{

    // sObserver 为静态成员变量    
    private static Observer sObserver;

    //mLogging 为成员变量
	@UnsupportedAppUsage
    private Printer mLogging;

        /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
    	 final Looper me = myLooper();

    	 final MessageQueue queue = me.mQueue;
    	 for (;;) {
            // 获取消息
    	 	Message msg = queue.next(); // might block
    	 	// This must be in a local variable,in case a UI event sets the logger

            final Printer logging = me.mLogging;
            if (logging != null) {
                // logging 可以通过判断其文本开头为 >>>> 认定为消息开始处理
                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;

            Object token = null;
            if (observer != null) {
                //调用messageDispatchStarting,通知observer 消息开始 某条消息开始处理
                // messageDispatchStarting 需要返回一个唯一标识的token,//在消息处理结束 回调messageDispatched时,会将这个token作为参数,
                // 开发者通过这个token 将Message关联起来
                token = observer.messageDispatchStarting();
            }

            try {
                msg.target.dispatchMessage(msg);
                if (observer != null) {
                    //通知 observer消息 调度完成,并传入 token 和msg
                    observer.messageDispatched(token,msg);
                }
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } catch (Exception exception) {
                if (observer != null) { //通知observer消息处理发生异常
                    observer.dispatchingThrewException(token,msg,exception);
                }
                throw exception;
            } finally {
                ThreadLocalWorkSource.restore(origWorkSource);
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            //设置logging的方式监控消息调度时,通过判断 <<<<字符 判断消息处理结束 
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

    	 }

    }

    /**
     * Set the transaction observer for all Loopers in this process.
     *
     * @hide
     */
    public static void setObserver(@Nullable Observer observer) {
        sObserver = observer;
    }

     /** {@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 接口的API设计,该接口包含三个函数 messageDispatchStarting、messageDispatched、dispatchingThrewException, 分别会在 某条消息被调度前、调度处理后、及消息调度处理过程中发生异常时回调。 注意messageDispatchingStarting 要求返回一个Object对象,对类型不做限制,这样开发者可以返回自己定义的类型,这个Object对象被当做一个token,每个消息的调度过程都应该对应一个单独的token实例。 我们可以参考下 系统的 LooperStats 在这块是如何实现的。

/**
 * Collects aggregated telemetry data about Looper message dispatching.
 *
 * @hide Only for use within the system server.
 */
public class LooperStats implements Looper.Observer {

    // DispatchSesion 缓存池,避免重复创建对象
    private final ConcurrentLinkedQueue<DispatchSession> mSessionPool =
            new ConcurrentLinkedQueue<>();

    // Token实现
    // 其记录了消息开始的 时钟时间(只能精确到秒)、thread cpu time()、及boot elapsed time
    // 并在消息调度结束时,通过计算时间差 统计到消息处理的 cputime、调度延迟、时钟时间差等数据
    private static class DispatchSession {
        static final DispatchSession NOT_SAMPLED = new DispatchSession();
        public long startTimeMicro;
        public long cpuStartMicro;
        public long systemUptimeMillis;
    }

    @Override
    public Object messageDispatchStarting() {
        if (deviceStateAllowsCollection() && shouldCollectDetailedData()) {
            DispatchSession session = mSessionPool.poll();
            session = session == null ? new DispatchSession() : session;
            //记录消息开始处理的微妙时间  SystemClock.elapsedRealtimeNanos/1000
            session.startTimeMicro = getElapsedRealtimeMicro();
            //记录线程时间
            session.cpuStartMicro = getThreadTimeMicro();
            //记录当前时钟时间
            session.systemUptimeMillis = getSystemUptimeMillis();
            return session;
        }

        return DispatchSession.NOT_SAMPLED;
    }

      @Override
    public void messageDispatched(Object token,Message msg) {
        if (!deviceStateAllowsCollection()) {
            return;
        }

        DispatchSession session = (DispatchSession) token;
        Entry entry = findEntry(msg,/* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
        if (entry != null) {
            synchronized (entry) {
                entry.messageCount++;
                if (session != DispatchSession.NOT_SAMPLED) {
                    entry.recordedMessageCount++;
                    //统计消息处理的时间(微妙)
                    final long latency = getElapsedRealtimeMicro() - session.startTimeMicro;
                    //统计cpu时间
                    final long cpuUsage = getThreadTimeMicro() - session.cpuStartMicro;
                    entry.totalLatencyMicro += latency;
                    entry.maxLatencyMicro = Math.max(entry.maxLatencyMicro,latency);
                    entry.cpuUsageMicro += cpuUsage;
                    entry.maxCpuUsageMicro = Math.max(entry.maxCpuUsageMicro,cpuUsage);
                    if (msg.getWhen() > 0) {
                        //统计消息处理延迟
                        final long delay = Math.max(0L,session.systemUptimeMillis - msg.getWhen());
                        entry.delayMillis += delay;
                        entry.maxDelayMillis = Math.max(entry.maxDelayMillis,delay);
                        entry.recordedDelayMessageCount++;
                    }
                }
            }
        }

        recycleSession(session);
    }

    @Override
    public void dispatchingThrewException(Object token,Exception exception) {
        if (!deviceStateAllowsCollection()) {
            return;
        }

        DispatchSession session = (DispatchSession) token;
        Entry entry = findEntry(msg,/* allowCreateNew= */session != DispatchSession.NOT_SAMPLED);
        if (entry != null) {
            synchronized (entry) {
                entry.exceptionCount++;
            }
        }

        recycleSession(session);
    }

}

可以看到其创建了一个专门 DispatchSession类,并通过几个字段(startTimeMicro、cpuStartMicro、systemUptimeMillis)用来记录消息开始的一些时间信息,如时钟时间、cpuTime、等,并在消息调度结束时(回调 messageDispatched),计算其相应的时间差,统计其性能。

另外需要注意的是 mLogging 是成员变量,而 sObserver是静态变量,因此设置 sObserver后 会监听到所有Looper的消息调度信息,如果只想监控主线程的消息调度,还需要判断下线程,我们可以在当前线程为主线程情况下 返回 token,而非主线程直接返回一个null,这样在相应回调函数里(messageDispatched),当发现token为null时,直接返回不做处理即可。 采用Observer的方式 可以拿到调度的Message对象,而 Printer的方式只能拿到系统拼接的字符串信息,因此从性能上来说,Observer的方式会更优一些。

编译问题解决

首先,我们要做的当然是实现一个 Observer实例,然而,由于Observer类被标记为@hidden,由于 android gradle插件的限制 我们无法直接访问该类,IDEA 直接报红了。

这里需要知道一个小知识,hidden api 进行API访问 限制分为两部分,一部分为了避免开发者在编码阶段使用 hidden api 其会在IDEA上下功夫,在开发阶段就限制我们的访问 ,另一部分是在虚拟机层面,在运行时限制访问。 在开发阶段有些时候我们可以通过反射进行相应API的调用,然而,我们目前遇到的问题是 Looper.Observer是个接口,我们需要编写相应的实现类,这可没法通过反射的方式进行调用。 但其实,这些开发阶段的限制都只是个“障眼法”,我们只要通过其他方式能够提前编译出 符合继承 Looper.Observer的类的字节码即可。

归根到底,Looper.Observer也只是一个普通的Java类,我们可以在一个 Java模块中 创建一个同名的类,然后编写相应继承这个类的实现,提前编译好相应的jar(aar) 就可以了。

那么马上实践一下,我们首先创建一个 纯java模块 fake-android,并在其模块中创建 同名的Looper类及Observer抽象接口

要注意,我们并不希望把这个 fake-android最终引入到我们的apk中,因此需要再创建另一个模块,并依赖fake-android模块 (依赖方式为 compileOnly,这样在其maven pom依赖关系上,并不存在 fake-android)。

这里我们创建一个Java模块 free-android,在这个模块中 引入 fake-android模块的依赖,并在该模块中实现 Looper.Observer继承类 LooperObserver。

最后,在我们最终需要使用Looper.Observer的模块中比如 (apm模块) 引入 free-andorid依赖,并使用LooperObserver。

    implementation project(path:":free-android")
复制代码
import com.knightboost.freeandroid.LooperObserver;
public class MyLooperObserver implements LooperObserver {
    @Override
    public Object messageDispatchStarting() {
        return null;
    }

    @Override
    public void messageDispatched(Object token,Message msg) {

    }

    @Override
    public void dispatchingThrewException(Object token,Exception exception) {

    }
}

然而,第一次尝试就失败了,编译时,抛出了了 class file for android.os.Looper$Observer not found** 的异常。

目前的工程结果如下所示:

这里对于编译失败的原因,我并没有深入编译流程进行分析,而是根据以往的经验来判断。我的分析是这样的,目前相关类的继承体系是这样的: MyLooperObserver <- LooperObserver <- android.os.Looper.Observer ,由于MyLooperObserver还是Java源代码,因此模块编译过程需要将MyLooperObserver编译为class类,而MyLooperObserver类所属的模块是android模块,而LooperObserver是java工程模块,虽然free-android模块可以顺利编译通过,但是 APM模块并不能编译通过,因为其模块类型为 android-library,根据类的继承体系,在编译MyLooperObserver过程中,其最终还是需要查找到Looper.Observer类,而因为android工程模块对hidden class的限制,导致最终编译失败了。

那么解决这个问题的方向是,需要保证:我们的apm模块在编译过程中,不直接依赖于任何继承 android.os.Looper.Observer类。 因此我们需要将对hidden class的依赖限定在 free-android模块中,解决方案如下:

首先在 free-andorid模块中,创建一个和 android.os.Looper.Observer 相同函数的接口,但这个接口不继承 之前的 android.os.Looper.Observer

package com.knightboost.freeandroid;

import android.os.Message;

public interface LooperMessageObserver {

    Object messageDispatchStarting();

    void messageDispatched(Object token,Message msg);

    void dispatchingThrewException(Object token,Exception exception);
}

在 free-android模块中,创建一个工具类LooperUtil,及其静态函数 setObserver(LooperMessageObserver observer),这个API 暴露给android项目工程,在这个静态函数内部实现中 实现对系统Looper类 observer的设置

package com.knightboost.freeandroid;

import android.os.Looper;
import android.os.Message;

public class LooperUtil {
    public static void setObserver(final LooperMessageObserver observer) {
        Looper.setObserver(new Looper.Observer() {
            @Override
            public Object messageDispatchStarting() {
                return observer.messageDispatchStarting();
            }

            @Override
            public void messageDispatched(Object token,Message msg) {
                observer.messageDispatched(token,msg);

            }

            @Override
            public void dispatchingThrewException(Object token,Exception exception) {
                observer.dispatchingThrewException(token,exception);
            }
        });
    }

}

可以看到,在这个函数中,我们将Looper.Observer的相应函数回调原封不动的代理给传入的observer参数。

此时整体工程如下所示:

这样 我们的android模块中的MyLooperObserver只依赖了 LooperMessageObserver类,而LooperMessageObserver这个类并不继承android系统的Looper.Observer,从而保证了android-library工程顺利编译通过

hidden API限制

讲过上个小节,我们的监控代码基本实现了,但是由于虚拟机对 hidden API 的限制,在运行时,会抛出NotFound的异常

解决Hidden API的方式有很多种方式,某些方式在Android高版本系统中可能会被官方封堵。 不过由于Android系统是开源的,所以无论怎么封堵,还是有其他的方式可以绕过,毕竟系统不会限制用户修改自身进程的内存。本文使用的三方库为 FreeReflection ,还有一些其他的实现方案,可以学习下各种花式操作,如、AndroidHiddenApiByPassbypassHiddenApiRestrictionRepublic

再绕过 Hidden API限制后,我们的demo 成功运行了

总结

  • Observer的方式相比Printer可以直接拿到Message对象,并且不设置Printer可以避免每个消息调度时额外的字符串拼接成本,性能更优
  • 解决开发阶段 hidden API的访问限制,可以通过 compile only 一个fake-andorid工程来实现,在 fake-andorid工程中模拟相应的系统源代码,从而实现对 Observer类的访问
  • 在 Android模块中 ,间接继承了一个hidden class,android模块项目还是会编译失败,此时我们可以用 代理对象的方式,不直接依赖 hidden class,曲线救国,让工程顺利编译通过
  • Looper的 sObserver (class Observer)是静态成员变量,这和 mLogging (class Printer)不同 ,因此注册Observer后,会接收到所有Looper实例的消息调度,因此在回调中需要进行线程判断
  • Observer机制是在Anroid 10开始添加的,因此低版本还是需要用Printer的方式进行监听

为了帮助到大家更好的掌握 Framework 底层与性能优化相关知识点,这准备了 Android11.0最新Framework解析 与 性能优化知识点汇总和Android 性能监控框架 的学习文档,中间记录了 AMS、PMS、WMS、Hander、Binder、启动优化、内存优化、UI优化……等知识点,可谓是很全面了,↓↓↓

有需要的可以复制下方链接,传送直达!!!
https://qr21.cn/CaZQLo?BIZ=ECOMMERCE

内功心法不是一天两天就可以修炼出来的,而是需要每天的坚持,技术提升也是如此。所以最好的速成修炼方法就是每天学习一点,日积月累后就会发现自己进步的效果。

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具