30分钟打造属于自己的Flutter内存泄漏检测工具---FlutterLeakCanary

30分钟打造属于自己的Flutter内存泄漏检测工具

  • 思路
    • 检测
    • Dart 也有弱引用-----WeakReference
    • 如何执行Full GC?
    • 如何知道一个引用他的文件路径以及类名?
  • 代码实践
    • 第一步,实现Full GC
    • 第二步,如何根据对象引用,获取出他的类名,路径等信息。
    • 第三步,定义工具接口
    • 第四步,添加代理类,隔离实现类
    • 第五步, 提供State的mixin监听类
    • 第六步,提供其他类的mixin监听类
    • 第七步,实现具体的管理类
  • 运行测试
    • 环境配置 --disable-dds
    • 检验成果

思路

检测

通过借鉴Android的内存泄漏检测工具LeakCanary的原理,使用弱引用持有引用,当这个引用执行释放动作的时候,执行Full GC后,如果弱引用的持有还在,那么就代表这个引用泄漏了。

Dart 也有弱引用-----WeakReference

关于Dart弱引用WeakReference怎么使用,我的这篇文章2分钟教你Flutter怎么避免引用内存泄漏>>会对你有帮助.

如何执行Full GC?

通过使用vm_service这个插件,在Dev可以执行Full GC请求,通过获取VmService的引用后,调用执行

vms.getAllocationProfile(isolate!.id!, gc: true)

就可以请求Full GC

如何知道一个引用他的文件路径以及类名?

vm_service这个插件里面有Api支持反射获取ClassRef读取引用里面的属性名,类名,以及路径等。

代码实践

有了以上的思路,我们就可以通过代码方式来实现检测内存泄漏,然后把泄漏的引用通知到UI展示出来。
代码我已经写好在 flutter_leak_canary: ^1.0.1,可做参考修改

第一步,实现Full GC

  1. 添加vm_service插件,获取VmService引用
 Future<VmService?> getVmService() async {if (_vmService == null && debug) {ServiceProtocolInfo serviceProtocolInfo = await Service.getInfo();_observatoryUri = serviceProtocolInfo.serverUri;if (_observatoryUri != null) {Uri url = convertToWebSocketUrl(serviceProtocolUrl: _observatoryUri!);try {_vmService = await vmServiceConnectUri(url.toString());} catch (error, stack) {print(stack);}}}return _vmService;}
  1. 执行GC的时候,flutter的无效引用回收是每个Isolate线程独立的,因为内存独立,相互不受影响。由于我们几乎所有代码都在UI线程执行的,所以我们需要进行筛选出UI线程,也就是’main’线程。
Future<VM?> getVM() async {if (!debug) {return null;}return _vm ??= await (await getVmService())?.getVM();
}//获取ui线程
Future<Isolate?> getMainIsolate() async {if (!debug) {return null;}IsolateRef? ref;final vm = await getVM();if (vm == null) return null;//筛选出ui线程的索引var index = vm.isolates?.indexWhere((element) => element.name == 'main');if (index != -1) {ref = vm.isolates![index!];}final vms = await getVmService();if (ref?.id != null) {return vms?.getIsolate(ref!.id!);}return null;
}

3.根据上面方法,落实Full GC

//请求执行Full GC
Future try2GC() async {if (!debug) {return;}final vms = await getVmService();if (vms == null) return null;final isolate = await getMainIsolate();if (isolate?.id != null) {await vms.getAllocationProfile(isolate!.id!, gc: true);}
}

第二步,如何根据对象引用,获取出他的类名,路径等信息。

  1. 思路大概是这样,通过一个文件的路径能获取当前LibraryRef对象,通过这个LibraryRef对象可以调用这个文件里面的顶级函数,返回值可以加工得到刚才提过的ClassRef。
  2. 利用这个特性,我们可以先把需要检测的对象,丢到一个Map里面,然后写一个高级函数返回这个map保存的对象。然后通过api获取这个对象id后,可以得到Obj, 根据Obj可以得到对应Instance,这个Instance里面就有ClassRef

具体实现如下:

const String vmServiceHelperLiraryPath ='package:flutter_leak_canary/vm_service_helper.dart';
//dont remove this method, it's invoked by getObjectId
String getLiraryResponse() {return "Hello LeakCanary";
}
//dont remove this method, it's invoked by getObjectId
dynamic popSnapObject(String objectKey) {final object = _snapWeakReferenceMap[objectKey];return object?.target;
}//
class VmServiceHelper {
//....    //根据文件获取getLiraryByPath
Future<LibraryRef?> getLiraryByPath(String libraryPath) async {if (!debug) {return null;}Isolate? mainIsolate = await getMainIsolate();if (mainIsolate != null) {final libraries = mainIsolate.libraries;if (libraries != null) {final index =libraries.indexWhere((element) => element.uri == libraryPath);if (index != -1) {return libraries[index];}}}return null;
}//通过顶部函数间接获取这个对象的objectId
Future<String?> getObjectId(WeakReference obj) async {if (!debug) {return null;}final library = await getLiraryByPath(vmServiceHelperLiraryPath);if (library == null || library.id == null) return null;final vms = await getVmService();if (vms == null) return null;final mainIsolate = await getMainIsolate();if (mainIsolate == null || mainIsolate.id == null) return null;Response libRsp =await vms.invoke(mainIsolate.id!, library.id!, 'getLiraryResponse', []);final libRspRef = InstanceRef.parse(libRsp.json);String? libRspRefVs = libRspRef?.valueAsString;if (libRspRefVs == null) return null;_snapWeakReferenceMap[libRspRefVs] = obj;try {Response popSnapObjectRsp = await vms.invoke(mainIsolate.id!, library.id!, "popSnapObject", [libRspRef!.id!]);final instanceRef = InstanceRef.parse(popSnapObjectRsp.json);return instanceRef?.id;} catch (e, stack) {print('getObjectId $stack');} finally {_snapWeakReferenceMap.remove(libRspRefVs);}return null;
}//根据objectId获取Obj
Future<Obj?> getObjById(String objectId) async if (!debug) {return null;}final vms = await getVmService();if (vms == null) return null;final mainIsolate = await getMainIsolate();if (mainIsolate?.id != null) {try {Obj obj = await vms.getObject(mainIsolatereturn obj;} catch (e, stack) {print('getObjById>>$stack');}}return null;
}//根据objectId获取Instance.  
Future<Instance?> getInstanceByObjectId(String objectId) async {if (!debug) {return null;}Obj? obj = await getObjById(objectId);if (obj != null) {var instance = Instance.parse(obj.json);return instance;}return null;
}//根据objectId获取出具体的类名,文件名,类在文件的第几行,第几列
//顶级函数>objectId>Obj>Instance
Future<LeakCanaryWeakModel?> _runQuery(objectId) async {final vmsh = VmServiceHelper();Instance? instance = await vmsh.getInstanceByObjectId(objectId!);if (instance != null &&instance.id != 'objects/null' &&instance.classRef is ClassRef) {ClassRef? targetClassRef = instance.classRef;final wm = LeakCanaryWeakModel(className: targetClassRef!.name,line: targetClassRef.location?.line,column: targetClassRef.location?.column,classFileName: targetClassRef.library?.uri);print(wm.className);return wm;}return null;
}}//泄漏信息模型
class LeakCanaryWeakModel {//泄漏时间late int createTime;//类名final String? className;
//所在文件名final String? classFileName;//所在列final int? line;//所在行数final int? column;LeakCanaryWeakModel({required this.className,required this.classFileName,required this.column,required this.line,}) {createTime = DateTime.now().millisecondsSinceEpoch;}
}

第三步,定义工具接口

定义一个接口,里面有添加监听,检测是否泄漏,获取当前泄漏的引用列表,通知当前有泄漏的引用

abstract class LeakCanaryMananger {//具体实现管理类,这个后面会介绍factory LeakCanaryMananger() => _LeakCanaryMananger();//监听当前引用,初始化时候调用void watch(WeakReference obj);//生命周期结束的以后,检测引用有没有泄漏void try2Check(WeakReference wr);//当前的泄漏列表List<LeakCanaryWeakModel> get canaryModels;//当前内存有新泄漏引用通知ValueNotifier get leakCanaryModelNotifier;
}

第四步,添加代理类,隔离实现类


class FlutterLeakCanary implements LeakCanaryMananger {final _helper = LeakCanaryMananger();static final _instance = FlutterLeakCanary._();FlutterLeakCanary._();factory() => _instance;static FlutterLeakCanary get() {return _instance;}void watch(obj) {_helper.watch(obj);}void try2Check(WeakReference wr) {_helper.try2Check(wr);}void addListener(VoidCallback listener) {_helper.leakCanaryModelNotifier.addListener(listener);}void removeListener(VoidCallback listener) {_helper.leakCanaryModelNotifier.removeListener(listener);}List<LeakCanaryWeakModel> get canaryModels => List.unmodifiable(_helper.canaryModels);ValueNotifier get leakCanaryModelNotifier => _helper.leakCanaryModelNotifier;
}

第五步, 提供State的mixin监听类

我们最不希望看到的泄漏类,一定是state。他泄漏后,他的context,也就是element无法回收,然后它里面持有所有的渲染相关的引用都无法回收,这个泄漏非常严重。
通过WeakReference来持有这个对象以来可以用来检测,二来避免自己写的工具导致内存泄漏。
initState的时候,把它放到检测队列,dispose以后进行检测

mixin LeakCanaryStateMixin<T extends StatefulWidget> on State<T> {late WeakReference _wr;String? objId;void initState() {super.initState();_wr = WeakReference(this);FlutterLeakCanary.get().watch(_wr);}void dispose() {super.dispose();FlutterLeakCanary.get().try2Check(_wr);}
}

第六步,提供其他类的mixin监听类


mixin LeakCanarySimpleMixin {late WeakReference _wr;String? objId;void watch()  {_wr = WeakReference(this);FlutterLeakCanary.get().watch(_wr);}void try2Check() {FlutterLeakCanary.get().try2Check(_wr);}
}

第七步,实现具体的管理类

对于引用的检测,是把引用包装到GCRunnable,使用消费者设计模型来做,3秒轮询检测一次。尽量用线程去分担检测,避免影响UI线程性能开销的统计。

class _LeakCanaryMananger implements LeakCanaryMananger {static final vmsh = VmServiceHelper();//objId:instancefinal _objectWeakReferenceMap = HashMap<int, WeakReference?>();List<GCRunnable> runnables = [];Timer? timer;bool isDetecting = false;//3秒轮训loopRunnables() {timer ??= Timer.periodic(Duration(seconds: 3), (timer) {if (isDetecting) {return;}if (runnables.isNotEmpty) {isDetecting = true;final trunnables = List<GCRunnable>.unmodifiable(runnables);runnables.clear();//使用线程去GCcompute(runGc, null).then((value) async {await Future.forEach<GCRunnable>(trunnables, (runnable) async {if (runnable.objectId == "objects/null") {return;}try {final LeakCanaryWeakModel? wm = await runnable.run();//如果非空,就是泄漏了,然后对泄漏的进行class信息获取,发送到订阅的地方,一般是ui,进行刷新if (wm != null) {canaryModels.add(wm);leakCanaryModelNotifier.value = wm;}} catch (e, s) {print(s);} finally {_objectWeakReferenceMap.remove(runnable.wkObj.hashCode);}});isDetecting = false;});}});}void watch(WeakReference wr) async {bool isDebug = false;assert(() {isDebug = true;return true;}());if (!isDebug) {return;}_objectWeakReferenceMap[wr.hashCode] = wr;loopRunnables();}ValueNotifier leakCanaryModelNotifier = ValueNotifier(null);//添加到待检测执行队列里,轮询扫描的时候执行,这样可以避免检测瓶颈void _check(WeakReference? wr) {assert(() {WeakReference? wkObj = _objectWeakReferenceMap[wr.hashCode];runnables.add(GCRunnable(wkObj: wkObj));return true;}());}void try2Check(WeakReference wr) async {bool isDebug = false;assert(() {isDebug = true;return true;}());if (!isDebug) {return;}if (wr.target != null) {_check(wr);}}List<LeakCanaryWeakModel> canaryModels = [];
}class GCRunnable {String? objectId;final WeakReference? wkObj;GCRunnable({required this.wkObj});Future<LeakCanaryWeakModel?> run() async {if (wkObj?.target != null) {final vmsh = VmServiceHelper();//cant quary objectId with isolate, but quary instanceobjectId = await vmsh.getObjectId(wkObj!);LeakCanaryWeakModel? weakModel = await compute(_runQuery, objectreturn weakModel;}}
}

运行测试

环境配置 --disable-dds

VsCode需要配置.vscode

“configurations”: [
{

“args”: [
“–disable-dds”
],
“type”: “dart”
},

]

Android Studio

在这里插入图片描述

检验成果

读下面的代码,看看那些会泄漏,然后在看看结果。

class WeakPage extends StatefulWidget {const WeakPage({super.key});State<WeakPage> createState() => _WeakPageState();
}class TestModel with LeakCanarySimpleMixin {Timer? timer;int count = 0;init() {watch();timer = Timer.periodic(Duration(seconds: 1), (timer) {count++;print("TestModel $count");});}void dispose() {// timer?.cancel();try2Check();}
}class TestModel2 with LeakCanarySimpleMixin {Timer? timer;int count = 0;init() {watch();}void dispose() {timer?.cancel();timer = null;try2Check();}
}class _WeakPageState extends State<WeakPage> with LeakCanaryStaTestModel? test = TestModel();TestModel2? test2 = TestModel2();Timer? timer;int count = 0;void initState() {super.initState();test?.init();test2?.init();timer = Timer.periodic(Duration(seconds: 1), (timer) {count++;print("_WeakPageState ${count}");});}void dispose() {// TODO: implement disposesuper.dispose();//timer.cancel();test?.dispose();test2?.dispose();test = null;test2 = null;}Widget build(BuildContext context) {return Material(child: Center(child: Container(child: InkWell(onTap: () {Navigator.of(context).pop();},child: Text('back')),),),);}

泄漏结果:

在这里插入图片描述

需要获取源码的同学,到这里获取,点击>>flutter_leak_canary: ^1.0.1<<

是不是很赞?如果这篇文章对你有帮助,请关注🙏,点赞👍,收藏😋三连哦

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

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

相关文章

HarmonyOS实战开发教程-如何开发一个2048游戏

今天为大家分享的是2048小游戏&#xff0c;先看效果图&#xff1a; 这个项目对于新手友友来说可能有一点难度&#xff0c;但是只要坚持看完一定会有收获。因为小编想分享的并不局限于ArkTs语言&#xff0c;而是编程思想。 这个游戏的基本逻辑是初始化一个4乘4的数组&#xff…

直接显示二进制图片

Option Explicit Private Type GUID Data1 As Long Data2 As Integer Data3 As Integer Data4(7) As Byte End Type Private Declare Function CreateStreamOnHGlobal Lib “ole32.dll” (ByRef hGlobal As Any, ByVal fDeleteOnResume As Long, ByRef ppstr As Any) As Long P…

LeetCode 每日一题 Day 144-157

2385. 感染二叉树需要的总时间 给你一棵二叉树的根节点 root &#xff0c;二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟&#xff0c;感染 将会从值为 start 的节点开始爆发。 每分钟&#xff0c;如果节点满足以下全部条件&#xff0c;就会被感染&#xf…

网络基础(1)网络编程套接字TCP,守护进程化

TCP协议 下面我们来学习一下TCP套接字的使用。 也就是使用一下基本的接口。首先TCP套接字的使用和UDP套接字的使用是大同小异的&#xff0c;但是多了一些步骤。 这里回顾一下&#xff1a;UDP是不可靠的&#xff0c;无连接的协议。而TCP则是可靠的&#xff0c;面向连接的协议…

【Linux】CAN相关命令:ip、ifconfig、can-utils

1、配置CAN波特率 ip link set can0 type can bitrate 500002、启动CAN设备 ip link set can0 up 或者 ifconfig can0 up3、显示CAN设备信息 ip -d -s link show can0ip -d -s link show can0 2: can0: <NOARP,UP,LOWER_UP,ECHO> mtu 16 qdisc fq_codel state UNKNOWN…

[HUBUCTF 2022 新生赛]checkin

数组反序列化弱比较 <?php $info array(username>true,password>true); echo serialize($info); ?> //?infoa:2:{s:8:"username";b:1;s:8:"password";b:1;}1.构造不能用类&#xff0c;因为$data_unserialize只是一个变量&#xff0c;不能…

谷歌确认:链接并不那么重要

谷歌的 Gary Illyes 在最近的一次搜索营销会议上证实&#xff0c;谷歌只需要很少的链接&#xff0c;这为出版商需要关注其他因素提供了越来越多的证据。Gary 在推特上证实了他确实说过这些话。 排名链接的背景 20 世纪 90 年代末&#xff0c;搜索引擎发现链接是验证网站权威性…

vivado Versal ACAP 可编程器件镜像 (PDI) 设置

Versal ACAP 可编程器件镜像 (PDI) 设置 下表所示 Versal ACAP 器件的器件配置设置可搭配 set_property <Setting> <Value> [current_design] Vivado 工具 Tcl 命令一起使用。 注释 &#xff1a; 在 Versal ACAP 架构上 &#xff0c; 原先支持将可编程器…

游戏技术人福音!当游戏语音碰到网易云信 ,我服了!

“开黑吗&#xff1f;五黑的那种” 少年时代&#xff0c;放假后偷偷溜进网吧&#xff0c;一边打着游戏&#xff0c;一边连麦吐槽对手的惬意岁月&#xff0c;不仅承载了无数 80 后、90 后&#xff0c;甚至 00 后的青春记忆&#xff0c;也让游戏语音成为了“游戏少年”闲暇生活的…

JavaScript函数和对象

函数&#xff1a; 定义语法&#xff1a; function functionName&#xff08;参数1&#xff0c;参数2&#xff09;{ } 注意 形式参数不需要类型&#xff0c;因为JavaScript是弱语言 返回值也不需要定义&#xff0c;可以在函数内部直接使用return 调用 函数名称&#xff…

6W 1.5KVDC. 单、双输出 DC/DC 电源模块——TP2L-6W 系列

TP2L-6W系列是一款高性能、超小型的电源模块&#xff0c;2:1电压输入&#xff0c;输出有稳压和连续短路保护功能&#xff0c;隔离电压为1.5KVDC、作温度范围为–40℃到85℃。特别适合对输出电压的精度有严格要求的地方&#xff0c;外部遥控功能对您的设计又多一项选择&#xff…

[链表专题]力扣206, 203, 19

1. 力扣206 : 反转链表 (1). 题 : 图略 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。示例 1&#xff1a;输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a;输入&#xff1a;head [1,2] 输出&#x…

C# Winform代码

功能1&#xff1a; TextBox控件&#xff0c;只允许输入正负数字和小数&#xff0c;且小数点只能后两位 功能2&#xff1a; winform控件启动线程&#xff0c;执行任务 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; u…

多实例任务在工作流中的高级配置与应用

前言 在现代业务流程管理中&#xff0c;多实例任务是一项强大功能&#xff0c;它允许一个任务被多个参与者并行或顺序处理&#xff0c;这在需要会签、集体评审等场景下尤为重要。Activiti作为一款流行的开源工作流引擎&#xff0c;提供了丰富的配置选项来满足多样化的多实例任…

面试分享——描述一个复杂网络问题如何解决

描述一个你成功解决的复杂网络问题 1.问题背景 在我之前的一家网络服务提供商公司工作期间&#xff0c;我们面临了一个复杂的网络故障问题。该公司为客户提供云主机托管服务&#xff0c;其中一个关键客户的应用服务器突然无法与数据库服务器通信&#xff0c;导致其核心业务受…

HackMyVM-Slowman

目录 信息收集 arp nmap whatweb WEB web信息收集 gobuster FTP匿名登录 hydra mysql爆破 mysql登录 fcrackzip爆破 hashcat爆破 ssh登录 提权 系统信息收集 python Capabilities提权 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interf…

类加载器aa

一&#xff0c;关系图及各自管辖范围 &#xff08;不赘述&#xff09; 二&#xff0c;查看关系 package com.jiazai;public class Main {public static void main(String[] args) {ClassLoader appClassLoader ClassLoader.getSystemClassLoader();//默认System.out.println…

关于在Conda创建的虚拟环境中安装好OpenCV包后,在Pycharm中依然无法使用且import cv2时报错的问题

如果你也掉进这个坑里了&#xff0c;请记住opencv-python&#xff01;opencv-python&#xff01;&#xff01;opencv-python&#xff01;&#xff01;&#xff01; 不要贪图省事直接在Anaconda界面中自动勾选安装libopencv/opencv/py-opencv包&#xff0c;或者在Pycharm中的解…

Linux搭建http发布yum源

1、搭建http源yum仓库 &#xff08;1&#xff09;在yum仓库服务端安装httpd yum -y install httpd &#xff08;2&#xff09;修改配置文件 我们httpd 中默认提供web 界面的位置是我们/var/www/html 目录&#xff0c;如果我们yum 源想指定目录&#xff0c;就需要修改蓝框2处…