We wanted to use Flutter to unify mobile App development and did some practice. With limited resources on mobile devices, memory usage is often a major concern in our daily development. So how does Flutter use memory and how does it affect the memory of Native App? This article will briefly introduce the Flutter memory mechanism, combine testing with our development practice, and explore the daily concerns of Bitmap memory usage and View drawing memory usage.
The Dart RunTime profile
Just like Android Art, The Dart source code is AOT compiled directly into the local bytecode. Improved execution performance by eliminating the process of explaining execution. Here we focus on the parts related to Dart VM memory allocation and GC.
Unlike Java, Dart’s “threads “(Isolate) do not share memory. Their respective Heap and Stack are isolated and communicate with each other through message channels. Dart does not have the problem of data contention and variable state synchronization. The entire rendering process of the Flutter Framework widgets runs in one ISOLATE.
The Dart VM divides memory management into New Generation and Old Generation.
- New Generation: Usually the first allocated objects are located in the New Generation. This area mainly stores objects with small memory and short life cycle, such as local variables. The new generation frequently performs memory collection (GC), which uses a copy-clean algorithm that divides memory into two parts (from and to) and uses only one of them at a time (FROM) and the other one (TO). When GC occurs, surviving objects in the currently used memory block are copied to the standby memory block, then the currently used memory block is cleared, and finally, the roles of the two memory blocks are swapped.
- Old Generation: Objects that “survive” in the New Generation’s GC are transferred to the Old Generation. Older generations store objects with long life cycles and large memory. The old age is usually much larger than the new. In the old days, GC collection used a “mark-clean” algorithm, which was divided into two stages: mark and clean. In the marking stage, all threads participate in the concurrent completion of the marking of the reclaimed object, reducing the marking stage time. During the cleanup phase, the GC thread is responsible for cleaning up the reclaimed objects, and the application thread executes simultaneously without affecting the application.
As you can see, the Dart VM borrows a lot of ideas from the JVM, and the way Dart generates memory leaks is similar to the way Java uses to detect and prevent memory leaks.
A Probe into Image memory
The proper use and optimization of images is an important part of UI programming. Flutter provides Image widgets that can be easily used:
New image. asset("images/xxxx.jpg"); New Image.network("https://xxxxxx");Copy the code
As we know, Android divides the memory into Java VIRTUAL machine memory and Native memory. Major manufacturers have an upper limit on the memory of Java virtual machine, and OOM exception will be triggered when the upper limit is reached. However, there is no strict limit on the use of Native memory. Generally, there is a large spare Native memory. Does the Android ImageView use Java virtual machine memory or Native memory?
Here’s a test: On an interface, each click adds an image to the stack. To prevent optimization by completely overwriting the previous image, zoom out a few pixels at a time so that complete coverage does not occur.
Open the Android Profiler, add images one by one, and observe the in-memory data. We tested Android 6.0, 7.0 and 8.0 respectively, and the results are as follows:
The Android 6.0 (Google Nextus5)
The Android 7.0 (Meizu pro5)
The Android 8.0 (Google pixel)
In testing, Android 6.0 and 7.0 both showed memory growth in the Java portion, while Android 8.0 showed memory growth in the Native portion, as image after image was added. In conclusion, The Android Native ImageView uses Java VIRTUAL machine memory in versions 6.0 and 7.0, and Native memory in Android 8.0.
Which part of memory does the Flutter Image Widget use? We did the same test with the Flutter interface. There is a big performance difference between the Debug and Release versions of The Flutter Engine, so it is better to use the Release version in our tests. However, the Apk of the Release version cannot use the Android profiler to observe the memory. We need to package a Release version of the Flutter Engine in the Debug Apk. We can modify the Flutter tool to do this:
// Do not judge, Engine Private Static String buildModeFor(buildType) {// if (buildType.name == "profile") {// return "profile" // } else if (buildType.debuggable) { // return "debug" // } return "release" }Copy the code
Similarly, we added images to the Flutter interface and used the Android Profiler to observe memory, testing the DART code used:
class StackImageState extends State<StackImages> { var images = <String>[]; var index = 0; @override Widget build(BuildContext context) { var widgets = <Widget>[]; for (int i = 0; i <= index; i++) { var pos = i - (i ~/ 103) * 103; widgets.add(new Container( child: new Image.asset("images/${pos}.jpg", fit: BoxFit.cover), padding: New EdgeInsets. Only (top: I * 2.0)); } widgets. Add (new Center(child: new GestureDetector(child: new Container(child: new Text(" add image (${index})", style: New TextStyle(color: color.red), color: color.green, paddING-color: const EdgeInsets. All (8.0)), onTap: () { setState(() { index++; }); }))); return new Stack( children: widgets, alignment: AlignmentDirectional.topCenter); }}Copy the code
The result is:
The Android 6.0
The Android 8.0
You can see that the memory used by the Flutter Image is neither Java VIRTUAL machine memory nor Native memory. But Graphics memory (not belonging to Graphics on the Meizu Pro5 device, in fact, the Meizu Pro5 device cannot categorize the memory used by the Flutter Image). The official description of Graphics memory is:
At least the memory used by Flutter Image will not be Java VIRTUAL machine memory, which is good news for many Android devices. This means that Flutter Image can use Native memory without OOM risk.
The Flutter Framework provides an ImageCache to Cache loaded images. However, unlike the Android Lru Cache, the Flutter Framework does not use the exact memory size to set the buffer pool capacity. Instead, you can only roughly specify the maximum number of cached images.
Preliminary study on FlutterView memory
Flutter was originally designed to unify Android and IOS interface programming, so ideally a Flutter based APK would only need to provide a MainActivity entry, and all subsequent page jumps would be managed in FlutterView. However, if an existing app is connected to Flutter development, it is impossible for us to re-implement all existing Activity pages with Flutter. At this time, we need to consider the jump interaction between local pages and Flutter pages. IOS can easily manage the page stack, but Android is very complex (Android has a task stack mechanism, low memory Activity recycling mechanism, etc.), so we usually use the Activity as the page container to display the flutter page. There are two options: you can start a new FlutterView each time you start an Activity, or you can reuse the existing FlutterView when you start the Activity.
Do not reuse FlutterView
Reuse FlutterView
FlutterView is used by binding activities in the Flutter Framework. To reuse the FlutterView, you must be able to use the FlutterView independently. Fortunately, the coupling degree between FlutterNativeView and Activity is not very deep. The most important point is that the FlutterNativeView must attach an Activity:
. / / the attach to the current Activity mNativeView attachViewAndActivity (this, the Activity);Copy the code
You must pass in an Activity to initialize the FlutterView, and then call the Attach method when other activities reuse the FlutterView. The problem is that the FlutterView must save a reference to the Activity. This is a memory leak. We can pass MainActivity in when FluterView detach. Because MainActivity is usually present throughout the App interaction, it can prevent other activities from leaking.
To better balance the pros and cons of the two methods, let’s first test the memory changes as pages increase with empty pages:
Memory changes when pages are added when FlutterView is not multiplexed
When reusing FlutterView, memory changes as pages increase
When FlutterView is not reused, one page (empty page) is opened on average, and The Java memory increases by 0.02m and Native memory increases by 0.73m. In the multiplexing of FlutterView, one page (empty page) was opened on average, and Java memory increased by 0.019m and Native memory increased by 0.65m. It can be seen that reusing FlutterView has advantages in memory usage, but it mainly reuses the memory of Native part. Reuse of FlutterView will inevitably bring some additional complex logic. Sometimes, it is worthwhile to sacrifice some relatively less precious Native memory for the sake of simple logic and convenience in later maintenance.
Reuse of a single FlutterView sometimes causes some “accidents”. For example, when an Activity switches, the current FlutterView detach to the newly created Activity, and the current interface will go blank and flash. One idea is to take screenshots of the current screen to block out subsequent screen changes, which sometimes creates additional adaptation issues.
The reuse of FlutterView is not absolute, and sometimes some comprehensive compromise schemes can be used. For example, we can establish a FlutterViewProvider, which maintains N reusable FlutterViews, as shown in the figure:
The advantage of this is that there can be a certain degree of reuse, and some embarrassing problems can be avoided when only one FlutterView occurs.
The rendering time of the first frame of FlutterView was relatively high, which was obviously felt in the Debug version with a black screen of about 2 seconds, which was much better in the Release version. But when we look at the Cpu curve, it’s still a time-consuming process. An idea of experience optimization is that we can pre-load the first frame of FlutterView to be used, so that it will be quickly used. We can first build a window with only 1 pixel and complete the rendering of the first frame of FlutterView in this window. The code is as follows:
final WindowManager wm = mFakeActivity.getWindowManager(); final FrameLayout root = new FrameLayout(mFakeActivity); LayoutParams = new Framelayout.layoutParams (1, 1); root.addView(flutterView,params); WindowManager.LayoutParams wlp = new WindowManager.LayoutParams(); wlp.width = 1; wlp.height = 1; wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; wlp.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; wm.addView(root,wlp); final FlutterView.FirstFrameListener[] listenerRef = new FlutterView.FirstFrameListener[1]; ListenerRef [0] = new FlutterView. FirstFrameListener () {@ Override public void onFirstFrame () {/ / after the first frame render cancellation window wm.removeView(root); flutterView.removeFirstFrameListener(listenerRef[0]); }}; flutterView.addFirstFrameListener(listenerRef[0]); String appBundlePath = FlutterMain.findAppBundlePath(mFakeActivity.getApplicationContext()); flutterView.runFromBundle(appBundlePath, null, "main", true);Copy the code
These are some of the practices that the Idle Fish team has applied to Flutter. If you want to try out new technologies and technical challenges, please contact us in the comments below. Your resume is sent to [email protected]