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…

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;面向连接的协议…

[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;不能…

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;也让游戏语音成为了“游戏少年”闲暇生活的…

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

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

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处…

VUE v-for 数据引用

VUE 的数据引用有多种方式。 直接输出数据 如果我们希望页面中直接输出数据就可以使用&#xff1a; {{ pageNumber }}双括号引用的方式即可。 在 JavaScript 中引用 如果你需要直接在代码中使用&#xff0c;直接使用变量名就可以了。 上面这张小图&#xff0c;显示了引用的…

linux 调试-kdb 调试内核-1

目标&#xff1a;打印bcm2835_spi_transfer_one 是如何从用户空间开始调用的 1. kernel 配置 KDB配置选项 添加 spi 控制器驱动 和 spi 设备驱动 2. 调试流程 调试内核-系统启动之后 1. 开发板进入kdb,等待pc 连接 rootraspberrypi:~# echo "ttyS0,115200"…

找不到模块“vue-router”。你的意思是要将 moduleResolution 选项设置为 node,还是要将别名添加到 paths 选项中?

在tsconfig.app.json中添加&#xff0c;记得一定是 tsconfig.app.json 中&#xff0c;如添加到 tsconfig.node.json 还是会报错的 哈哈哈哈&#xff0c;不瞒你们&#xff0c;我就添加错了&#xff0c;哈哈哈。所以这也算写一个demo提醒自己 "compilerOptions": {&qu…

C语言 动态内存管理

目录 1. C/C程序的内存分配2. 动态内存分配的作用3. malloc - 分配内存4. free - 释放内存5. calloc - 分配并清零内存6. realloc - 调整之前分配的内存块7. 常见的动态内存的错误7.1 对空指针解引用7.2 对动态开辟空间的越界访问7.3 对非动态开辟内存使用free7.4 使用free释放…

发电机组远程管理,提升管控力,降低运维成本

发电机组是指发电机发动机以及控制系统的总称&#xff0c;用来把发动机提供的动能转化为电能。它通常由动力系统、控制系统、消音系统、减震系统、排气系统组成。发电机组远程管理系统利用物联网技术与PLC远程控制模块集成解决方案&#xff0c;在提高发电机组的运行效率、降低运…

【计算机科学速成课】笔记三——操作系统

文章目录 18.操作系统问题引出——批处理设备驱动程序多任务处理虚拟内存内存保护Unix 18.操作系统 问题引出—— Computers in the 1940s and early 50s ran one program at a time. 1940,1950 年代的电脑&#xff0c;每次只能运行一个程序 A programmer would write one at…

Django框架四-项目

一、项目准备 1.流程与人员 2.需求分析 项目主要页面 归纳项目主要模块 3.架构设计 项目开发模式 项目架构设计

【C++STL详解(八)】--------stack和queue的模拟实现

目录 前言 一、stack模拟实现 二、queue的模拟实现 前言 前面也介绍了stack和queue的常见接口&#xff0c;我们也知道stack和queue实际上是一种容器适配器&#xff0c;它们只不过是对底层容器的接口进行封装而已&#xff0c;所以模拟实现起来比较简单&#xff01;一起来看看是…