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.hqwc.cn/news/674731.html

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

相关文章

通信录的动态版本

一. 增加需求 在学习了动态开辟内存之后 我们对于通讯录产生了新的需求 要求我们做出一个动态增长的版本 即 随着我们储存联系人的增加 储存的空间增加 要求 &#xff1a; 1 初始空间为3 2 每次达到上限之后 扩容两个内存 二. 动手实施 我们首先要创建一个结构体 结构体…

基于FPGA的多路彩灯控制器VHDL代码Quartus仿真

名称&#xff1a;基于FPGA的多路彩灯控制器VHDL代码Quartus仿真&#xff08;文末获取&#xff09; 软件&#xff1a;Quartus 语言&#xff1a;VHDL 代码功能&#xff1a; 多路彩灯控制器 综合训练内容要求 设计一台基于FPGA的多路彩灯控制器的设计。要求如下 1.彩灯从左…

简易录制视频做3D高斯

系统环境 ubuntu20 &#xff0c;cuda11.8&#xff0c;anaconda配置好了3D高斯的环境。 具体参考3D高斯环境配置&#xff1a;https://blog.csdn.net/Son_of_the_Bronx/article/details/138527329?spm1001.2014.3001.5501 colmap安装&#xff1a;https://blog.csdn.net/Son_of…

[CISCN2019 华北赛区 Day1 Web2]ikun

看到提示说一定要找到lv6 这要写脚本来爆破了&#xff0c;用bp是爆破不出来的 发现LV等级都是有参数挂着的 写个脚本看一下 import requests for i in range(1,1000):payload"http://node4.anna.nssctf.cn:28150/shop?page%d"%(i)resrequests.get(payload)if "…

SVM直观理解

https://tangshusen.me/2018/10/27/SVM/ https://www.bilibili.com/video/BV16T4y1y7qj/?spm_id_from333.337.search-card.all.click&vd_source8272bd48fee17396a4a1746c256ab0ae SVM是什么? 先来看看维基百科上对SVM的定义: 支持向量机&#xff08;英语&#xff1a;su…

SparkSQL与Hive整合 、SparkSQL函数操作

SparkSQL与Hive整合 SparkSQL和Hive的整合&#xff0c;是一种比较常见的关联处理方式&#xff0c;SparkSQL加载Hive中的数据进行业务处理&#xff0c;同时将计算结果落地回Hive中。 整合需要注意的地方 1)需要引入hive的hive-site.xml&#xff0c;添加classpath目录下面即可…

15.计算机网络

1.物理层的互联设备 中继器 和 集线器 2.集线器可以看做特殊的多路中继器 集线器 不可以做到自动寻址的功能 3.数据链路层 网桥 和 交换机 4.交换机是多端口网桥 5.网络层 路由器 6.应用层 网关 7.广播域 网络层 可以形成多个广播域 冲突域 网络层数据链路层 可以形成多个冲突域…

【数据结构】C/C++ 带头双向循环链表保姆级教程(图例详解!!)

目录 一、前言 二、链表的分类 &#x1f95d;单链表 &#x1f95d;双链表 &#x1f95d;循环链表 &#x1f95d;带头双向循环链表 &#x1f34d;头节点&#xff08;哨兵位&#xff09;的作用 ✨定义&#xff1a; ✨作用&#xff1a; &#x1f347;总结 三、带头双向循环链表 …

Mybatis进阶4-权限管理

权限管理 1.权限 //相当于 职责 2.用户 //相当于 职员&#xff08;职员就职于一个职位&#xff09; 3.角色 //相当于 职位&#xff08;有多个职责&#xff09; 权限管理基础表&#xff1a;权限表&#xff0c;用户表&#xff0c;角色表 问题1&#xff1a;…

RK3576芯片规格,以及与RK3588对比

瑞芯微RK3576是一款高性能、低功耗的SoC&#xff08;系统级芯片&#xff09;处理器&#xff0c;适用于基于ARM的PC、边缘计算设备、个人移动互联网设备等多种应用场景。它采用Arm架构的八核心CPU&#xff0c;集成了GPU、MCU、NPU、VPU等多种计算核心&#xff0c;并具有丰富的外…

KAN: Kolmogorov–Arnold Networks

KAN: Kolmogorov–Arnold Networks 论文链接&#xff1a;https://arxiv.org/abs/2404.19756 代码链接&#xff1a;https://github.com/KindXiaoming/pyKAN 项目链接&#xff1a;https://kindxiaoming.github.io/pyKAN/intro.html Abstract 受Kolmogorov-Arnold表示定理的启…

【Linux网络】网络文件共享

目录 一、存储类型 二、FTP文件传输协议 2.1 FTP工作原理 2.2 FTP用户类型 2.3 FTP软件使用 2.3.1 服务端软件vsftpd 2.3.2 客户端软件ftp 2.4 FTP的应用 2.4.1 修改端口号 2.4.2 匿名用户的权限 2.4.3 传输速率 三、NFS 3.1 工作原理 3.2 NFS软件介绍 3.3 NFS配…