从LeakCanary看Service生命周期监控

news2024/9/30 11:31:17

作者:小海编码日记

大家都知道使用LeakCanary可以监控项目中存在的 内存泄漏 问题,那么LeakCanary是怎么实现的呢?LeakCanary通过检测程序中对象的引用关系,收集应该被回收的对象并标记,随后等待GC后,检查该对象是否按预期回收即可,目前LeakCanary支持Service,Activity,Fragment,ViewModel以及View的泄漏检测,接下来我们一起来看下Service的关联部分。

首先,我们考虑如果要认定一个Service对象可以被回收,前提条件是什么?没错,当然是这个Service执行了onDestroyed方法,也就意味着我们要检测Service的内存泄漏情况,首先要实现Service的生命周期监控,这样的话当Service执行了onDestroyed方法后,我们就可以对该Service对象进行标记和观察。那么如何监控Service的生命周期呢?

Service的启动过程

上图中描述的Service启动过程包含进程创建,流程很清晰,不做解释,有兴趣的同学可以跟源码看下。

Service的销毁过程

如上图当Service回调onDestroyed完成后,会通知AMS Service已经销毁。

Service生命周期监控

从前文中,我们已经基本了解了Service的启动和销毁过程,可以看出不论是Service的创建还是Service的销毁,在整个流程中都涉及到一个非常中要的Handler角色,这个Handler在ApplicationThread和ActivityThread中间充当桥梁作用,当有Service创建时,会接受并处理CREATE_SERVICE消息,当有Service销毁时,会接受并处理STOP_SERVICE消息,回调Service onDestroy方法。

没错,就是我们的mH对象,这个对象定义在ActivityThread中,如果我们能监听其内部的消息处理,自然可以实现Setvice生命周期监控的能力

监听mH接收到的STOP_SERVEICE消息(Service即将销毁)

如何监听mH Handler对象接收到的STOP_SERVICE消息呢?首先我们来看下Handler内部是如何进行消息分发的?

可以看到当Handler中的mCallback对象不为空时,消息会首先分发给mCallback对象执行,如果该函数返回false,则继续分发。这也就意味着我们可以修改mH Handler对象中的mCallback成员,通过该成员对象完成Handler中消息分发的监听,而不影响原始的消息分发逻辑。

反射mH Handler对象,设置mCallback成员

结合上文以及反射相关知识,我们可以得到下面反射并设置mH Handler对象的mCallback成员的实现,代码如下:

 try {
     Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
     Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
     currentActivityThreadMethod.setAccessible(true);
     // currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
     Object currentActivityThread = currentActivityThreadMethod.invoke(null);
 ​
     // 获取mH Handler对象
     Field mH = activityThreadClass.getDeclaredField("mH");
     mH.setAccessible(true);
     Handler handler = (Handler) mH.get(currentActivityThread);
 ​
     // 获取Handler中Callback对象并赋值
     Field callBack = Handler.class.getDeclaredField("mCallback");
     callBack.setAccessible(true);
 ​
     callBack.set(handler,new Handler.Callback(){
 ​
         @Override
         public boolean handleMessage(Message msg) {
             Log.d(TAG, "handleMessage: msg " + msg);
 ​
             return false;
         }
     });
 } catch (ClassNotFoundException | NoSuchFieldException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) {
     e.printStackTrace();
 }

编写一个TestService,前后分别调用startService和stopService,验证日志输出如下:

可以看到我们确实监听到了msg.what = 116的Service销毁的消息。

mH Handler对象中STOP_SERVICE取值为116

从日志可以看出,在STOP_SERVICE消息中并没有被销毁的Service信息,此时我们应该如何匹配那个Service被销毁了呢?

找出即将被销毁的Service

在STOP_SERVICE消息中没有携带被销毁的Service相关信息,那么系统是如何知道那个Service被销毁了呢?我们查看源码一探究竟:

可以看到在ActivityThread中是通过msg.obj来索引标记Service的,msg.obj是一个IBinder对象,在handleStopService中,通过Service s = mServices.remove(token);获取msg.obj对应的Service对象,并依次调用Service的onDestroy和deatchAndCleanUp方法,结合这段代码,我们不难联想到handleStopService这里引用的mServices是一个类Map类型的数据结构,当调用其remove方法时会依据key将数据结构中的数据删除,并返回该key值对应的value对象,下面我们来看下mServices的声明和初始化:

从源码上可以看到mServices是一个以IBinder对象为key,Service为值的ArrayMap,也就意味着我们只要能访问到mServices成员,就可以通过监听到的Message.obj来获取对应的Service对象。

怎么获取mServices对象呢?自然也是通过反射,相关代码如下:

 private void hookServicesInActivityThread(){
     try {
         Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
         Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
         currentActivityThreadMethod.setAccessible(true);
         // currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
         Object currentActivityThread = currentActivityThreadMethod.invoke(null);
 ​
         Field mServices = activityThreadClass.getDeclaredField("mServices");
         mServices.setAccessible(true);
         mActivityThreadServices = (Map<IBinder, Service>) mServices.get(currentActivityThread);
 ​
     } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
              InvocationTargetException | NoSuchFieldException e) {
         e.printStackTrace();
     }
 }
 ​
 private Service findServiceFromActivityThreadServices(IBinder token) {
     if (mActivityThreadServices == null) {
         hookServicesInActivityThread();
     }
     return mActivityThreadServices.get(token);
 }

运行后日志输出如下,可以看到我们确实找到了即将被销毁的Service对象

结合Service销毁过程一节中流程图和handleStopService代码可知,在我们监听到消息时,Service实际上还没有调用onDestroy方法,也就意味着mH中的STOP_SERVICE消息仅代表Service即将开始销毁,那么什么时候销毁完成呢?

没错,在流程图和handleStopService代码中均可以看出,当调用ActivityManager.getService().serviceDoneExecuting()方法时,代表Service已经销毁完成。

监听Service销毁完成

从流程图和handleStopService代码可知,如果要监听Service销毁完成,也就是要监听serviceDoneExecuting方法的调用,怎么做呢?

反射+代理,将ActivityManager.getService返回的对象使用代理包装一层后重新设置回去即可.

在Android 8.0及以后,IActivityManager是从IActivityManagerSingleton中获取的对象,代码如下:

在Android 8.0以前,IActivityManager是从ActivityManagerNative的成员gDefault中获取的,代码如下:

代码如下:

 try {
     Object defaultSingleton = null;
     if (Build.VERSION.SDK_INT >= 26) {
         Class<?> activityManageClazz =
                 Class.forName("android.app.ActivityManager");
         Field field = activityManageClazz.getDeclaredField("IActivityManagerSingleton");
         field.setAccessible(true);
         //获取activityManager中的IActivityManagerSingleton字段
         defaultSingleton = field.get(null);
     } else {
         Class<?> activityManagerNativeClazz =
                 Class.forName("android.app.ActivityManagerNative");
         //获取ActivityManagerNative中的gDefault字段
         Field field = activityManagerNativeClazz.getDeclaredField("gDefault");
         field.setAccessible(true);
         defaultSingleton = field.get(null);
     }
     
     Class<?> singletonClazz = Class.forName("android.util.Singleton");
     Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
     mInstanceField.setAccessible(true);
     
     //获取iActivityManager
     Object iActivityManager = mInstanceField.get(defaultSingleton);
     Class<?> iActivityManagerClazz =
             Class.forName("android.app.IActivityManager");
     
     Object proxy = Proxy.newProxyInstance(
             Thread.currentThread().getContextClassLoader(),
             new Class<?>[]{iActivityManagerClazz},
             new IActivityManagerProxy(iActivityManager));
     
     mInstanceField.set(defaultSingleton, proxy);
 } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
     e.printStackTrace();
 }

 public class IActivityManagerProxy implements InvocationHandler {
     private Object mActivityManager;
 ​
     public IActivityManagerProxy(Object activityManager) {
         mActivityManager = activityManager;
     }
 ​
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         Log.d("ServiceWatcher", "proxy receive method:" + method.getName());
         return method.invoke(mActivityManager, args);
     }
 }

运行,我们可以看到在IActivityManagerProxy中监听到了serviceDoneExecuting方法调用,日志如下:

综上,我们也就完成了Service销毁的监听。

虽然文中只是重点介绍了Service销毁过程的监听,但是基于文中代码结构,我们不难实现自己的全局Service生命周期监听,用于监听进程中Service的生命周期变化。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/852534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

线上电影票票务系统开发--APP、H5小程序、网站

一、系统架构 后端架构&#xff1a;采用微服务架构&#xff0c;包括API接口、业务逻辑层和数据访问层。使用云服务器进行数据存储&#xff0c;保证数据的安全性和稳定性。 前端架构&#xff1a;APP采用原生开发方式&#xff0c;前端与后端通过API接口进行数据交互。 二、功能…

司空见惯 - Realwear公司及产品介绍

基于安卓系统的头显设备&#xff0c;Android based headset display&#xff0c;集成了摄像头&#xff0c;还有个小显示屏。 设备价格很贵哦&#xff1a; 显示屏在前方&#xff1a; 官方网站&#xff1a;https://www.realwear.com/ RealWear head-mounted displays. Built to …

直播平台的秘密武器:揭秘流行直播实时美颜SDK的背后技术

近年来&#xff0c;随着社交媒体和直播平台的崛起&#xff0c;实时美颜成为了许多用户在分享自己生活的过程中的一项重要需求。无论是个人的自拍照片&#xff0c;还是主播在直播中的形象展示&#xff0c;美颜效果都直接影响着观众的视觉感受。而支撑这种实时美颜效果背后的技术…

uniapp 实现滑动视图切换 顶部滚动导航栏

无论小程序的时候一般有这个功能,在页面处于首页时候,滑动视图,切换视图顶部滚动导航也跟着切换 1.想要实现这个功能就需要实现顶部导航栏,首先实现顶部滚导航栏 点击高亮颜色显示 模板代码 <scroll-view scroll-x"true" class"scroll-content" > …

MySQL不知道密码,直接修改密码

很简单&#xff0c;我们跳过验证&#xff0c;直接进去修改就好 修改配置文件 vim /etc/my.cnf在[mysqld]下直接添加配置 skip-grant-tables如图&#xff1a; 保存&#xff0c;退出即可。 重启服务 service mysqld restart进入MySQL #(直接点击回车&#xff0c;密码为空)…

20230809在WIN10下使用python3将DOCX文件转换为TXT文件

20230809在WIN10下使用python3将DOCX文件转换为TXT文件 2023/8/9 11:38 python docx txt https://blog.51cto.com/u_16175446/6620474 如何实现Python读取word内容转为TXT的具体操作步骤 如何实现Python读取word内容转为TXT的具体操作步骤 原创 mob649e81576de12023-07-04 14:0…

伪原创文章生成器软件【php源码】

这篇文章主要介绍了python怎么做gui界面&#xff0c;具有一定借鉴价值&#xff0c;需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获&#xff0c;下面让小编带着大家一起了解一下。 火车头采集ai伪原创插件截图&#xff1a; Author&#xff1a;Runsen 现在极少有人会用…

Python入门【串行、并行与并发的区别、 进程、线程、协程的区别、线程是什么? 、协程是什么?、同步和异步介绍、线程Thread 、守护线程】(二十三)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…

小红书运营 从入门到精通

大家好&#xff0c;我是网媒智星&#xff0c;今天跟大家分享一下小红书运营的经验&#xff0c;从入门到精通&#xff0c;一文读懂&#xff0c;全篇干货输出&#xff0c;非常实用。 一、注册账号 首先要说明一点&#xff0c;小红书与其他平台有所不同&#xff0c;因此具有特殊性…

mysql二进制方式升级8.0.34

一、概述 mysql8.0.33 存在如下高危漏洞&#xff0c;需要通过升级版本修复漏洞 Oracle MySQL Cluster 安全漏洞(CVE-2023-0361) mysql/8.0.33 Apache Skywalking <8.3 SQL注入漏洞 二、查看mysql版本及安装包信息 [rootlocalhost mysql]# mysql -V mysql Ver 8.0.33 fo…

做外贸受伤的并不总是你

外贸群里的小伙伴们经常吐槽&#xff1a; 小伙伴A 说&#xff1a;我前两天做一个PI&#xff0c;是新开发成功的一个客户。客户让我再次降价&#xff0c;我也同意了&#xff0c;刚刚客户反过来说&#xff0c;一再同意降价&#xff0c;是不是品质同时也变差了&#xff0c;应该怎…

ORACLE19.8 RAC搭建ADG-主备都在原主机上 静态监听

ORACLE19.8 RAC搭建ADG-主备都在原主机上配置关于PDB连接 标签&#xff1a; oracle19c 我们知道多租户环境下&#xff0c;pdb中建立的普通用户连接pdb需要通过tnsnames.ora或ezconnect的方式连接。而pdb的连接需要通过IP、端口和PDB服务名来连接&#xff0c;那么相同主机adg的…

轻辙视觉引擎以多种AI算法工具,助力纺织行业断线检测智能识别

近年来&#xff0c;人工智能技术在各行各业的应用愈发广泛&#xff0c;机器视觉作为人工智能的重要分支&#xff0c;成为当下的研究热点。机器视觉技术的发展&#xff0c;大幅提升了工业、农业、医疗等领域的效率和精度。尤其在工业领域&#xff0c;随着智能制造的进一步发展&a…

如何更改或伪装浏览器指纹?

跨境出海经常会出现被某些网站“禁止访问”的情况&#xff0c;为什么呢&#xff1f;其中一部分原因就是因为你的浏览器制备被网站和在线平台识别到&#xff0c;从而得出设备和网络详细信息&#xff0c;从而禁止你的访问。这种独特的配置文件称为“浏览器指纹”&#xff0c;使网…

艺术创作的新纪元:如何训练Lora模型打造令人惊叹的AI绘画

目录 前言一、&#x1f981; 选择合适的云端平台1-1、云端平台的优势1-2、选择适合的云端平台 二、&#x1f981; 账号注册三、&#x1f981; 开始炼丹3-1、购买算力并创建工作空间3-2、启动工作空间3-3、应用市场一键安装 四、&#x1f981; 使用Stable-Diffusion作图4-1、国风…

软考高项(八)项目整合管理 ★重点集萃★

&#x1f451; 个人主页 &#x1f451; &#xff1a;&#x1f61c;&#x1f61c;&#x1f61c;Fish_Vast&#x1f61c;&#x1f61c;&#x1f61c; &#x1f41d; 个人格言 &#x1f41d; &#xff1a;&#x1f9d0;&#x1f9d0;&#x1f9d0;说到做到&#xff0c;言出必行&am…

循环结构进阶

二重循环 import java.util.Scanner;public class Demo01 {public static void main(String[] args) {Scanner scanner new Scanner(System.in);// 二重循环 外循环班级 内循环学生for (int i1; i<3; i) { // 外循环班级System.out.println("请输入第" i "…

WebDAV之π-Disk派盘+麻雀记

麻雀记是一款专注个人记录的优秀软件。正所谓麻雀虽小五脏俱全,麻雀记app亦是如此,虽然这款软件非常的小巧,但是它的功能却非常的丰富强大。全新的Focus页面功能,可以将你置顶的笔记整合在一个页面中,然后结合番茄钟功能来实现专注功能,提高你的专注力与效率。同时还提供…

原型设计工具大盘点:Figma VS 蓝湖 VS Axure VS 摹客

从事互联网行业以来&#xff0c;我在很多平台都可以看到这样的问题&#xff1a;原型设计工具推荐有哪些&#xff1f;产品经理有什么好用的原型设计工具&#xff1f; 的确&#xff0c;原型设计工具在产品设计领域扮演着至关重要的角色&#xff0c;一款高效简单的原型工具对于产…

基于低代码和数字孪生技术的电力运维平台设计

电力能源服务商在为用能企业提供线上服务的时候&#xff0c;不可避免要面对用能企业的各种个性化需求。如果这些需求和想法都要靠平台厂家研发人员来实现&#xff0c;那在周期、成本、效果上都将是无法满足服务运营需要的&#xff0c;这也是目前很多线上能源云平台应用效果不理…