Flutter Stability and Performance Optimization

1. Flutter exception and Crash

Flutter exceptions refer to the exceptions thrown by the Flutter program during runtime:

  • Exceptions that occur when Dart code is running
  • Flutter framework exception
  • Exceptions thrown when native code is running, such as: Java and kotlin for Android, OC and swift for iOS

The vast majority of Dart codes are used to make Flutter applications, so in this article we focus on learning the capture and collection of Dart and framework exceptions in Flutter.

The exceptions that occur when the Dart code is running are different from programming languages ​​​​with multi-threaded models such as Java, kotlin, and OC. Dart is a single-threaded programming language that uses the event loop mechanism to run tasks, so the running status of each task is independent of each other. of. That is to say, when an exception occurs during the running of the program, even if the try-catch mechanism is not used to catch the exception like Java, the Dart program will not exit, it will only cause the subsequent code of the current task to not be executed, while other Functionality can still continue to be used.

exception capture

According to the execution timing of exception codes, Dart exceptions can be divided into two types: synchronous and asynchronous exceptions. First, let's look at how to capture synchronous exceptions:

How to capture synchronous exceptions:

//使用try-catch捕获同步异常
    try {
      throw StateError('There is a dart exception.');
    } catch (e) {
      print(e);
    }

Asynchronous exception capture method:

There are two ways to capture asynchronous exceptions:

  • One is to use the catchError statement provided by Future to capture;
  • The other is to convert asynchronous to synchronous and capture it through try-catch;
    //使用catchError捕获异步异常
    Future.delayed(Duration(seconds: 1))
        .then(
            (e) => throw StateError('This is first Dart exception in Future.'))
        .catchError((e) => print(e));
    try {
      await Future.delayed(Duration(seconds: 1)).then(
          (e) => throw StateError('This is second Dart exception in Future.'));
    } catch (e) {
      print(e);
    }

Focus on catching exceptions

In Android, we can Thread.UncaughtExceptionHandlercollect exceptions centrally through the interface, so how to collect exceptions centrally in Flutter?

The method provided by Flutter Zone.runZonedGuarded(). In the Dart language, Zone represents an environment for code execution. Its concept is similar to a sandbox, and different sandboxes are isolated from each other. If you want to handle exceptions that occur during code execution in the sandbox, you can use the onError callback function provided by the sandbox to intercept those exceptions that are not caught during code execution:

    runZonedGuarded(() {
      throw StateError('runZonedGuarded:This is a Dart exception.');
    }, (e, s) => print(e));
    runZonedGuarded(() {
      Future.delayed(Duration(seconds: 1)).then((e) => throw StateError(
          'runZonedGuarded:sThis is a Dart exception in Future.'));
    }, (e, s) => print(e));

It is not difficult to see from the above code that whether it is a synchronous exception or an asynchronous exception, it can be directly captured by using Zone. At the same time, if you need to focus on capturing unhandled exceptions in the Flutter application, you can also place the runApp statement in the main function in the Zone, so that you can uniformly process the captured exception information when an abnormal code operation is detected:

  runZonedGuarded<Future<Null>>(() async {
    runApp(BiliApp());
  }, (e, s) => print(e));

the case

void main() {
  HiDefend().run(BiliApp());
}
...
class HiDefend {
  run(Widget app) {
    //框架异常
    FlutterError.onError = (FlutterErrorDetails details) async {
      //线上环境,走上报逻辑
      if (kReleaseMode) {
        Zone.current.handleUncaughtError(details.exception, details.stack);
      } else {
        //开发期间,走Console抛出
        FlutterError.dumpErrorToConsole(details);
      }
    };
    runZonedGuarded<Future<Null>>(() async {
      runApp(app);
    }, (e, s) => _reportError(e, s));
  }

  ///通过接口上报
  Future<Null> _reportError(Object error, StackTrace stack) async {
    print('catch error:$error');
  }
}

Exception reporting

After the exception is caught, it can _reportErrorbe reported to the server in the above method. First-tier Internet companies such as BAT have their own Crash monitoring platform. If the company does not have its own Crash platform, it can access third-party platforms such as Buggly.

2. Flutter test

Software testing is an indispensable part of finding program errors and measuring software quality. In enterprises, there will be dedicated software testing engineers to be responsible for software testing and quality failures. As a Flutter developer, understanding the methods and means of Flutter testing will help reduce program bugs and develop higher-quality applications. So how is Flutter tested?

In this tutorial, we will share the mainstream test methods and cases of Flutter. There are three main types of tests in Flutter:

  • unit test
  • Widget test
  • Integration Testing

unit test

Testing a single function, method, or class, unit tests typically do not read/write to disk, render to screen, or receive user actions from outside the process running the test. The goal of unit testing is to verify the correctness of a logical unit under various conditions.

If the object under test has external dependencies, then the external dependencies must be able to be simulated, otherwise unit testing cannot be performed.

required dependencies
dev_dependencies:
  flutter_test:
    sdk: flutter

the case
///单元测试
void main() {
  ///测试HiCache的存储和读取
  test('测试HiCache', () async {
    //fix ServicesBinding.defaultBinaryMessenger was accessed before the binding was initialized.
    TestWidgetsFlutterBinding.ensureInitialized();
    //fix MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)
    SharedPreferences.setMockInitialValues({});
    await HiCache.preInit();
    var key = "testHiCache", value = "Hello.";
    HiCache.getInstance().setString(key, value);
    expect(HiCache.getInstance().get(key), value);
  });
}

In this case, we HiCacheconducted a unit test on the cache module in the project, mainly to test whether its storage and reading functions are normal.

Widget test

Widget tests can be used to test individual classes, functions, and Widgets.

Widget testing has certain limitations. The tested Widget must be able to run independently, or the dependent conditions can be simulated.

required dependencies
dev_dependencies:
  flutter_test:
    sdk: flutter

According to the above requirements, which Widgets in the entire APP can be tested for Widgets?

the case
...
class UnKnownPage extends StatefulWidget {
  @override
  _UnKnownPageState createState() => _UnKnownPageState();
}

class _UnKnownPageState extends State<UnKnownPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Container(
        child: Text('404'),
      ),
    );
  }
}
...
///Widget测试
void main() {
  testWidgets('测试UnKnownPage', (WidgetTester tester) async {
    //UnKnownPage虽然没有Flutter框架之外的依赖,但因为用到了Scaffold所以需要用MaterialApp包裹
    await tester.pumpWidget(MaterialApp(home: UnKnownPage()));
    expect(find.text('404'), findsOneWidget);
  });
}

UnKnownPageIn this case, we tested the Widget in the project , mainly to test whether there is a Text with the content of 404 in the page.

Integration Testing

Integration testing is mainly used when testing the various parts running together or testing the performance of an application running on a real device.

required dependencies
dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter

The main steps
  1. Add test drive
  2. Write test cases
  3. run test case
  4. View Results
Add test drive

The purpose of adding a test driver is to facilitate running integration tests through the flutter drive command:

Create a directory in the project root test_driverand add files integration_test.dart:

import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();

Write test cases

Create a directory in the project root integration_testand add files app_test.dart. Next, let's test the jump function of the login module:

import 'package:flutter/material.dart';
import 'package:flutter_bili_app/main.dart' as app;
import 'package:flutter_bili_app/navigator/hi_navigator.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('Test login jump', (WidgetTester tester) async {
    //构建应用
    app.main();
    //捕获一帧
    await tester.pumpAndSettle();
    //通过key来查找注册按钮
    var registrationBtn = find.byKey(Key('registration'));
    //触发按钮的点击事件
    await tester.tap(registrationBtn);
    //捕获一帧
    await tester.pumpAndSettle();
    await Future.delayed(Duration(seconds: 3));

    //判断是否跳转到了注册页
    expect(HiNavigator.getInstance().getCurrent().routeStatus,
        RouteStatus.registration);

    //获取返回按钮,并触发返回上一页
    var backBtn = find.byType(BackButton);
    await tester.tap(backBtn);
    await tester.pumpAndSettle();
    await Future.delayed(Duration(seconds: 3));
    //判断是返回到登录页
    expect(
        HiNavigator.getInstance().getCurrent().routeStatus, RouteStatus.login);
  });
}

In this case, we obtain the frame on the APP page of the current page, and then find the corresponding control based on the captured frame, and simulate the click. HiNavigatorIn order to be able to judge whether the jump logic of the login module is normal, we judge by obtaining the current page routing status in the above code .

run test case

Running the test cases for the integration tests can be done with the following command:

flutter drive   --driver=test_driver/integration_test.dart   --target=integration_test/app_test.dart

  • –driver: used to specify the path of the test driver;
  • –target: the path used to specify the test case;

The following implements the running effect diagram:

IntegrationTest

If the APP has been logged in before, it needs to be uninstalled first to clear the login cache, so that the APP can normally enter the login page during testing.

3. Flutter performance optimization

In general, the applications we build through Flutter technology are high-performance by default. However, it is inevitable to fall into some performance traps in programming. In this section, I will share with you some ideas for performance optimization of Flutter and based on optimization practices.

The best time for performance optimization is in the coding stage. If you can master some performance optimization skills and best practices, then you can consider how to design for optimal performance and how to implement it will slow down performance; instead of completing development After that, I will start to do performance optimization, so the idea of ​​pollution first and then governance.

In our previous Flutter development process, we have applied a lot of Flutter performance optimization techniques and some best practices, so this section mainly summarizes performance optimization to help you learn and master it.

  • memory optimization
  • build() method optimization
    • Time-consuming operations are performed in the build() method
    • A huge Widget is piled up in the build() method
  • list optimization method
  • Case: Frame Rate Optimization

memory optimization

To optimize memory, we first need to understand the detection methods of memory, so that we can compare the effects before and after memory optimization.

Flutter performance detection tool Flutter Performance

Flutter PerformanceThe tool is provided in the Flutter plugin of the IDE , which is a tool that can be used to detect the sliding frame rate and memory of Flutter.

We can open this tool from the sidebar of the IDE, or use Dart DevTools to view memory usage:

DartDevTools-memory

At this time, you can open a page or perform some operations to observe the changes in memory. If the memory suddenly increases a lot, you must pay special attention to whether it is a reasonable increase. If necessary, check the cause of the increase in memory and consider the optimization plan.

As for how to judge whether the memory has changed after optimization, you can use the Memory tab of Dart DevTools to complete it. When you destroy a FlutterEngine, you can use the button to trigger it GConce GCto view the memory changes.

build() method optimization

When we use Flutter to develop the UI, the method we deal with the most is the method build(). build()There are two common pitfalls when using the method. Let's take a look together:

Time-consuming operations are performed in the build() method

We should try to avoid performing time-consuming operations in build(), because the build() method will be called frequently, especially when the parent Widget is rebuilt; therefore, it is recommended to move time-consuming operations to initState()this In frequently called methods;

In addition, we try not to perform blocking operations in the code. We can convert file reading, database operations, and network requests into asynchronous operations through Future; in addition, for operations with frequent CPU calculations, such as image compression, Isolate can be used. Make full use of multi-core CPU;

A huge Wdiget is piled up in the build() method

Some friends like to use a shuttle when drawing the UI. In fact, this is a particularly bad habit; if the Widget returned in build() is too large, it will cause three problems:

  • Poor code readability: Because of the particularity of Flutter’s layout, we cannot live without a Widiget nested in a Widiget. However, if the Wdiget is nested too deeply, the readability of the code will deteriorate, which is not conducive to the later stage. maintenance and expansion of
  • Difficult to reuse: Since all the codes are in one build() method, it will be impossible to use the public UI codes to other pages or modules;
  • Affecting performance: When we call setState() on State, all Widgets in build() will be rebuilt; therefore, the larger the Widget tree returned in build(), the more Widgets need to be rebuilt, which is more detrimental to performance; See below:

widget-tree

If the above picture is the widget tree we returned in a build() method, then when the widgets in the red box on the left need to be updated, the minimum update cost is to update only the parts that need to be updated, but since they are all in A build method of State. Therefore, when calling setState(), many widgets on the right that do not need to be updated will also need to be rebuilt; the correct way is to transfer the call of setState() to the part of the Widget subtree whose UI actually needs to be changed.

You can recall where similar optimizations have been carried out in our project: hi_flexible_header.dart.

list optimization method

When building a large grid or list, we should try to avoid using ListView(children: [],)or GridView(children: [],)directly, because in this scenario, all the data in the list will be drawn at one time regardless of whether the list content is visible or not. This usage is similar to Android's ScrollView; So when the amount of data in your list is relatively large, it is recommended that you use:

  • ListView.builder(itemBuilder: null)
  • GridView.builder(gridDelegate: null, itemBuilder: null)

These two methods, because these two methods are only created when the visible part of the screen is in the content of the list, this is similar to Android's RecyclerView.

Frame rate optimization

A key factor that determines the performance of the list is the frame rate. Normally, the refresh rate of mobile phones is 60fps. Of course, there are some high-refresh mobile phones that can reach 90 or even 120 fps. Get the frame rate of the application in Flutter We can view the page frame rate through the Flutter Performance tab:

Flutter-Performance

In addition, you can click the Performance overlay button in the upper left corner of the above figure to open the performance layer function:

The-performance-overlay

Through this chart, we can help us analyze whether the UI is stuck. The vertical green bar represents the current frame, and each frame should be created and displayed within 1/60 second (about 16 ms). If a frame times out and cannot be displayed, causing a freeze, the graph above will show a red vertical bar. If the red vertical bars appear on the UI diagram, it means that the Dart code consumes a lot of resources. A red vertical bar indicates that the current frame is taking a lot of time to render and paint.

Frame rate optimization case

With the help of Flutter Performance, we perform frame rate detection on the homepage of the course project:

Because the performance of Flutter in debug mode will be relatively limited, in order to restore the authenticity of the detection, we need to run the APP in analysis mode.

  • In Android Studio and IntelliJ use Run > Flutter Run main.dart in Profile Mode option
  • Or run from the command line with the --profile parameter
    • flutter run --profile

Note: The simulator does not support the analysis mode, you can use the real machine to connect to the computer for analysis

before optimization

Optimized

4. Flutter package size optimization

  • Packet Size Analysis
  • Optimization ideas
  • APK optimization practice

Flutter package size optimization requires the knowledge of integrated packaging. For those who don’t know how to package Flutter applications, you can read the content of integrated packaging at the back of our course, and then come to this lesson.

Packet Size Analysis

Drag and drop the APK to AS to analyze the package size:

apk-size

Optimization ideas

  • image optimization
  • Remove redundant second and third libraries
  • Enable code shrinking and resource shrinking
  • Packages for building single-ABI architectures

image optimization

Images generally account for a large proportion of Flutter resources. There are two commonly used solutions for image optimization:

  • Image compression: For too large images, you can use https://tinypng.com/ to compress them, and use the compressed images
  • Use network pictures: You can also change local pictures to network pictures according to business needs;

Remove redundant second and third libraries

With the increase of business, more and more second- and third-party libraries will be introduced into the project, many of which have duplicate functions, or even no longer used. Removing unused ones and merging those with the same functionality can further reduce package size.

Enable code shrinking and shrinking resources

Compression and resource reduction are enabled by default, and the release package built after enabling code reduction and resource reduction will reduce the size by about 10%, or even more.

If you want to reduce code and resources, you can refer to the following settings:

buildTypes {
    release {
        signingConfig signingConfigs.release
        minifyEnabled false
        shrinkResources false
    }
}

  • minifyEnabled: Whether to enable code reduction
    • If the minifyEnabled property is set to true, R8 code minification is enabled by default. Code shrinking (also known as "tree shaking") refers to the process of removing code that R8 determines is not needed at runtime. This process can greatly reduce the size of your app, for example when your app contains many library dependencies but only uses a small subset of their functionality.
  • shrinkResources: whether to enable shrink resources
    • Resource reduction only works in conjunction with code reduction. After the code reducer removes all unused code, the resource reducer can determine which resources the app still uses.

Packages for building single-ABI architectures

By default, ./gradlew assembleReleasethe Release package built through AS or contains all ABI architectures. By analyzing an optimized Flutter application package, you will find that so is the largest package size:

apk-size

In this case, an installation package with multiple ABI architectures is used, and its so size accounts for 83.5% of the entire package size:

CPU status quo
ARMv8 current mainstream version
ARMv7 some old cell phones
x86 Since 2011, tablets and simulators have been used more
x86_64 From 2014, 64-bit tablets

At present, in the mobile phone market, x86 / x86_64 / armeabi / mips / mips6 should have a small share. As the latest generation architecture, arm64-v8a is the current mainstream, and armeabi-v7a only exists in a small number of old mobile phones.

Therefore, in order to further optimize the package size, we can build a single-architecture installation package. In Flutter, we can build a single-architecture installation package in the following ways:

cd <flutter应用的android目录>
flutter build apk --split-per-abi


  • flutter build: The command will build a release package by default
  • --split-per-abi: Indicates building a single architecture

After running the command successfully, you will see the following input and output:

Removed unused resources: Binary resource data reduced from 518KB to 512KB: Removed 1%
Removed unused resources: Binary resource data reduced from 518KB to 512KB: Removed 1%
Removed unused resources: Binary resource data reduced from 518KB to 512KB: Removed 1%
Running Gradle task 'assembleRelease'...
Running Gradle task 'assembleRelease'... Done                      37.1s
✓ Built build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk (8.5MB).


It is not difficult to see from the running results that the single-architecture installation package is only 8.5MB, which is 66% smaller than the multi-architecture installation package.

final benefit

If you want to become an architect or want to break through the 20-30K salary range, then don't be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of "Advanced Notes on the Eight Major Modules of Android" written by the senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically, so that you can systematically and efficiently Master the various knowledge points of Android development.
insert image description here
Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Full set of video materials:

1. Interview collection

insert image description here
2. Source code analysis collection
insert image description here

3. The collection of open source frameworks
insert image description here
welcomes everyone to support with one click and three links. If you need the information in the article, directly scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓

Guess you like

Origin blog.csdn.net/weixin_43440181/article/details/129570482