当一个 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 环境不可用,至于原因留给你自己发现咯~