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
- 添加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;
}
- 执行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);
}
}
第二步,如何根据对象引用,获取出他的类名,路径等信息。
- 思路大概是这样,通过一个文件的路径能获取当前LibraryRef对象,通过这个LibraryRef对象可以调用这个文件里面的顶级函数,返回值可以加工得到刚才提过的ClassRef。
- 利用这个特性,我们可以先把需要检测的对象,丢到一个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(mainIsolate
return 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:instance
final _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();
//使用线程去GC
compute(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 instance
objectId = await vmsh.getObjectId(wkObj!);
LeakCanaryWeakModel? weakModel = await compute(_runQuery, object
return 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 LeakCanarySta
TestModel? 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 dispose
super.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<<
是不是很赞?如果这篇文章对你有帮助,请关注🙏,点赞👍,收藏😋三连哦