当一个 APP 随着业务发展,无用功能越堆越多,参与开发的人员队伍越发壮大,代码大爆炸式膨胀,尽管有一系列的例如人工review、代码规范、静态lint检查之类流程上的克制手段,也止不住坍缩的小宇宙往深不可测无法捉摸的黑洞演进。其中,最为令人头疼的问题莫过于“内存泄漏”。
什么是内存泄漏?很常见却也很不起眼,你我随手写段代码就能轻松让一个对象被垃圾回收器(GC)视而不见。
如下面这一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class WidgetA extends StatefulWidget { ... @override _WidgetAState createState() => _WidgetAState(); }
class _WidgetAState extends State<WidgetA> { @override Widget build(BuildContext context) { return GestureDetector( onTap: () async { var r = await fetchResult(); if (mounted) { setState(() { this.result = r; }); } }, child: ... ); } }
|
你能在阅读完这段代码的同时找到那个可能被泄漏的对象吗?
没错,context
可能会短时间泄漏。
但,得益于 Dart 积极高效的 GC 策略,这种泄漏已经算是内存泄漏问题中的可以忽略不计的轻症了。(扩展阅读:Flutter: Don’t Fear the Garbage Collector )
又如这一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class _WidgetAState extends State<WidgetA> { @override void initState() { super.initState(); Provider.of<AuthStateBloc>(context).stream.listen(onAuthStateChanged); } void onAuthStateChanged(AuthState state) { if (mounted) { setState(() { this.state = state; }); } }
|
有着丰富的 Flutter 开发经验的你,还是很轻松地就发现了,stream.listen
的返回对象StreamSubscription
没有被正确地cancel
,导致_WidgetAState
和它的 Element
(context) 可能都泄漏了,直到祖节点的 Provider<AuthStateBloc>
被卸载才可能被 GC 回收。
那么回到标题,有没有工具可以程序化的发现内存泄漏呢?
首先要解决的问题是怎么找到内存泄漏点?
有没有可能在一个对象代码逻辑上被丢弃的时候,设法在不影响引用计数的情况下跟踪它,看它最终有没有被垃圾回收器所收集到即可。
如果你写过 Java,你肯定立马想到了 WeakReference
。
Dart 里也有类似的,只不过它实现的是 Java 里的 WeakHashMap
: Expando
。它使用上和普通的 Map 类似,不过有个特性,不会增加 key 属性的引用计数,亦即不会阻止 key 对象被 GC。
An Expando does not hold on to the added property value after an object becomes inaccessible.
通过 Expando
实现一个 DeadObjectWatcher
,用于跟踪理论上需要被 GC 回收的对象:
1 2 3 4 5 6 7 8 9 10
| class DeadObjectWatcher { static DeadObjectWatcher instance = DeadObjectWatcher._();
DeadObjectWatcher._(); final Expando<Object> weakRef = Expando('weakreferences');
void watch(Object obj) { instance.weakRef[obj] = obj.hashCode; } }
|
DeadObjectWatcher
是一个单例作为一个全局存活的对象,用于跟踪目标对象,如:
1 2 3 4 5 6 7 8 9 10
| mixin LeakStateMixin<T extends StatefulWidget> on State<T> { @override void dispose() { DeadObjectWatcher.instance ..watch(this.context) ..watch(this); super.dispose(); } }
|
如果你在用 flutter_bloc
,则可以像我一样注册一个自动跟踪bloc对象的全局BlocObserver
:
1 2 3 4 5 6 7 8
| class DeadBlocObserver extends BlocObserver { @override void onClose(BlocBase bloc) { super.onClose(bloc); DeadObjectWatcher.instance.watch(bloc); } }
|
接下来的问题是该如何分析 Expando
里对象呢?
作为 Flutter 资深开发的你,一定天天在用 Dart DevTools 分析内存、Timeline等等,其实这个工具本质上是通过 Dart VM Service Protocol 与 Flutter 底层的 Dart VM 服务通信实现的。那么当然可以通过这个协议,写一个简单的内存泄漏分析工具。
Dart 官方已经为我们提供了一个实现 Dart VM Service Protocol 的工具包:vm_service。
ps. Dart DevTools 用的其实是 Dart VM Service Protocol 的扩展实现: Dart Development Service Protocol
通过 vm_service
工具连接 Dart VM Service 对外暴露的 Observatory Uri,通过协议找到 Flutter 程序 main.dart 所在的 Isolate
:
1 2 3 4 5 6 7 8 9 10 11 12
| var observatoryUri = "http://127.0.0.1:36489/g_8M0xR54Rg=/" var wsUri = observatoryUri .replace( scheme: 'ws', path: observatoryUri.path + 'ws', ) .toString(); var vmService = await vmServiceConnectUri(wsUri) var vm = await vmService!.getVM(); var isolate = vm.isolates?.firstWhereOrNull((i) => i.name == "main"); assert(isolate != null); var isolateId = isolate!.id;
|
找到这个 isolateId 很重要,因为静态 DeadObjectWatcher
的对象就存活在这个 Isolate
里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Future<Instance?> _findDeadObjectWatcher(VmService vmService, String isolateId) async { try { var classList = await vmService.getClassList(isolateId); var classRef = classList.classes?.firstWhere((classRef) => classRef.name == 'DeadObjectWatcher'); assert(classRef != null); var deadObjectWatcherClass = await vmService.getObject(isolateId, classRef!.id!) as Class; var instanceFieldRef = deadObjectWatcherClass.fields?.firstWhere((f) => f.isStatic! && f.name == 'instance'); assert(instanceField != null); var instanceField = await vmService.getObject(isolateId, instanceFieldRef!.id!) as Field; var instance = await vmService.getObject(isolateId!, instanceField.staticValue.id); return instance is Instance && instance.kind != InstanceKind.kNull ? instance : null; } catch (_) { return null; } }
|
再找到 DeadObjectWatcher
里的 Expando
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Future<Instance?> _findExpando(VmService vmService, String isolateId, Instance watcher) async { try { var ref = watcher.fields?.firstWhereOrNull((f) => f.decl?.name == 'weakRef')?.value; if (ref == null) return null; var obj = await vmService.getObject(isolateId, ref.id!); assert(obj.kind != InstanceKind.kNull); return obj; } catch (_) { return null; } }
|
通过阅读代码 dart-sdk/lib/_internal/vm/lib/expando_patch.dart 可知,跟踪的对象都会被_WeakProperty
包裹存在一个名为_data
的 List 里。
那么找到这个 _data
,即可找到被跟踪的对象引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Future<List<InstanceRef>> _findWatchingObjects(VmService vmService, String isolateId, Instance expando) async { var dataField = expando.fields?.firstWhereOrNull((f) => f.decl?.name == '_data'); if (dataField == null) return []; final InstanceRef value = dataField.value; final _data = await vmService.getObject(isolateId, ref.id); if (_data == null || _data.kind == InstanceKind.kNull) return []; List<InstanceRef?> refs = await Future.wait( _data.elements! .where((e) => e != null && e.kind == InstanceKind.kWeakProperty) .map((e) async { final instance = await vmService.getObject(isolateId, e.id); InstanceRef? ref = instance?.propertyKey; if (ref == null || ref.kind == InstanceKind.kNull) { return null; } return ref; }), ); return refs.where((e) => e != null).toList().cast(); }
|
找到了需要分析的对象,接下来就是获取这个对象的引用路径 RetainingPath
:
1 2 3 4 5 6 7 8 9 10 11 12
| Future<RetainingPath?> _getRetainPath(VmService vmService, String isolateId, InstanceRef ref) async { try { return await vmService.getRetainingPath(isolateId!, ref.id!, 500); } on SentinelException catch (e) { if (e.sentinel.kind == SentinelKind.kCollected || e.sentinel.kind == SentinelKind.kExpired) { return null; } rethrow; } }
|
能通过InstanceRef
找到 RetainingPath
对象,意味着它还没有被 GC 回收,从 GC Root 根节点到目标对象还有引用路径!亦即,这个在逻辑上应该要被回收的对象所属内存泄漏了!
而RetainingPath
对象里的 elements
,就是引用路径中的各个节点 RetainingObject
。接下来的任务就是分析各个节点,找到内存泄漏点。
分析 RetainingObject
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void _analyzeRetainingObject(RetainingObject ele) { var ref = ele.value!; String name = ref.name!; if (ref is InstanceRef) { if (ref.kind == InstanceKind.kClosure) { List<String?> chain = [ref.closureFunction!.name]; var owner = ref.closureFunction!.owner; while (owner is FuncRef) { chain.add(owner.name); owner = owner.owner; } if (owner != null) { chain.add(owner.name); } name = chain.reversed.join('.'); } } else if (ref is FieldRef) { if (ref.isStatic == true) { name = '${ref.name} (static)'; } } else if (ref is ContextRef) { name = '<Closure Context>'; } final String parentField = ele.parentField ?? ''; print('+ $name $parentField'); }
|
不过,需要注意一个小细节,找到 Expando
里跟踪对象的引用路径,并不意味着它一定泄漏了,也有可能只是 GC 任务还没来得及执行而已。
所以分析引用路径前最好先强制触发一下GC:
1 2 3 4 5 6 7 8
| void triggerGC(VmService vmService, String isolateId) async { var allocation = await vmService.getAllocationProfile(isolateId, gc: true); var usage = allocation.memoryUsage!; print('Memory Usage:\n' ' externalUsage: ${usage.externalUsage! >> 20}MB\n' ' heapCapacity: ${usage.heapCapacity! >> 20}MB\n' ' heapUsage: ${usage.heapUsage! >> 20}MB'); }
|
结合起来就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void main() async { var wsUri = ... var vmService = await vmServiceConnectUri(wsUri) var vm = await vmService!.getVM(); var isolate = vm.isolates?.firstWhereOrNull((i) => i.name == "main"); assert(isolate != null); var isolateId = isolate!.id!;
var watcher = await _findDeadObjectWatcher(vmService!, isolateId); assert(watcher != null); var expando = _findExpando(vmService!, isolateId, watcher!); assert(expando != null); var objs = await _findWatchingObjects(vmService!, isolateId, expando!); await triggerGC(vmService!, isolateId); for (var obj in objs) { var path = await _getRetainPath(vmService!, isolateId, obj); if (path == null) { print('${obj.id} has been collected!'); } else { print('${obj.id} leaks!'); print('GC ROOT: ${path.gcRootType}'); path.elements?.forEach(_analyzeRetainingObject); } } await vmService.dispose(); }
|
这样,通过一个简单的对象跟踪类 DeadObjectWatcher
,再通过一个简单的工具与目标 Flutter 程序的 Dart VM Service 通信,获取Expando
所跟踪的“该死的”对象,即可无需大海捞针似地 dump 所有内存,直接分析指定目标对象是否会内存泄漏。
那么如何自动化地定位内存泄漏问题呢?下一篇文章再细说。
PS. 注意一下本文的 _findWatchingObjects
在 Flutter web 环境不可用,至于原因留给你自己发现咯~