Update record

  • This article was written on 2019.09.10. The Flutter SDK version is V1.5.4 -hotfix.2
  • Update 2019.09.12 To change the word differential package to incremental package
  • 2019.09.12 Update, –not-hot error, should be –no-hot

preface

This is the second article in the series on Flutter construction. In the last article, WE discussed the different products of Flutter in debug and release mode, how to debug the build_tools source code, etc. We’re not going to go over this again, so if you want to read the first passage.

Hot overloading is one of the big killers of Flutter, and it’s very popular, especially for client developers. As the project gets bigger, there might be a scene where you change a line of code and build for half an hour. The very popular componentization solution is to solve the pain point of long build time. For Flutter, there are two modes that can be quickly modified: Hot Reload and Hot Restart, of which Hot Reload takes only a few hundred milliseconds and is very fast, and Hot Restart is a little slower in seconds. You can only use Hot Restart if you have modified the resource file or need to rebuild the state.

The source code parsing

In the first article, we said that for every Flutter Command, there is a Command class. The Flutter run we use is handled by the RunCommand class.

By default, hot mode is enabled in debug mode and disabled in release mode. You can disable hot mode by adding –no-hot when running the run command.

When Hot Mode is enabled, HotRunner is used to start the Flutter application.

if (hotMode) {                                          
  runner = HotRunner(                                   
    flutterDevices,                                     
    target: targetFile,                                 
    debuggingOptions: _createDebuggingOptions(),        
    benchmarkMode: argResults['benchmark'],             
    applicationBinary: applicationBinaryPath == null    
        ? null                                          
        : fs.file(applicationBinaryPath),               
    projectRootPath: argResults['project-root'],        
    packagesFilePath: globalResults['packages'],        
    dillOutputPath: argResults['output-dill'],          
    saveCompilationTrace: argResults['train'],          
    stayResident: stayResident,                         
    ipv6: ipv6,                                         
  );                                                    
} 
Copy the code

When hot Mode is enabled, initialization is performed first, and the relevant code is shown in HotRunner Run ().

Initialize the

  • Build the application, for example Anroid, where Gradle is called to execute assemble Task to generate the APK file

    if(! prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion ==null) {   
      printTrace('Building APK');                                                                     
      final FlutterProject project = FlutterProject.current();                                        
      await buildApk(                                                                                 
          project: project,                                                                           
          target: mainPath,                                                                           
          androidBuildInfo: AndroidBuildInfo(debuggingOptions.buildInfo,                              
            targetArchs: <AndroidArch>[androidArch]                                                   
          ),                                                                                           
      );                                                                                              
      // Package has been built, so we can get the updated application ID and                         
      // activity name from the .apk.                                                                 
      package = await AndroidApk.fromAndroidProject(project.android);                                 
    }                                                                                                 
    Copy the code
  • When the APK is successfully built, it starts with ADB, establishes sockets connections, and forwards the host’s port to the device.

    The host here refers to the environment in which the Flutter command is run, usually a PC. The device refers to the environment in which the Flutter application is running, in this case the phone.

    The point of the forward port is to communicate with the Dart VM (VIRTUAL Machine) on the device, as discussed later.

    After starting the application with ADB, it listens for log output, uses regular expressions to obtain sockets connection addresses, and sets port forwarding.

    void _handleLine(String line) {                                                                                  
      Uri uri;                                                                                                       
      final RegExp r = RegExp('${RegExp.escape(serviceName)} listening on ((http|\/\/)[a-zA-Z0-9:/=_\\-\.\\[\\]]+)');
      final Match match = r.firstMatch(line);                                                                        
                                                                                                                     
      if(match ! =null) {                                                                                           
        try {                                                                                                        
          uri = Uri.parse(match[1]);                                                                                 
        } catch(error) { _stopScrapingLogs(); _completer.completeError(error); }}if(uri ! =null) {                                                                                             
        assert(!_completer.isCompleted);                                                                             
        _stopScrapingLogs();                                                                                         
        _completer.complete(_forwardPort(uri));                                                                      
      }                                                                                                              
                                                                                                                     
    }
    
    // Forwarding port
    Future<Uri> _forwardPort(Uri deviceUri) async {                                                         
      printTrace('$serviceName URL on device: $deviceUri');                                                 
      Uri hostUri = deviceUri;                                                                              
                                                                                                            
      if(portForwarder ! =null) {                                                                          
        final int actualDevicePort = deviceUri.port;                                                        
        final int actualHostPort = await portForwarder.forward(actualDevicePort, hostPort: hostPort);       
        printTrace('Forwarded host port $actualHostPort to device port $actualDevicePort for $serviceName');
        hostUri = deviceUri.replace(port: actualHostPort);                                                  
      }                                                                                                     
                                                                                                            
      assert(InternetAddress(hostUri.host).isLoopback);                                                     
      if (ipv6) {                                                                                           
        hostUri = hostUri.replace(host: InternetAddress.loopbackIPv6.host);                                 
      }                                                                                                     
                                                                                                            
      return hostUri;                                                                                       
    }                                                                                                       
    Copy the code

    On my device, the matching address is as follows:

    09-08 14:14:12.708  6122  6149 I flutter : Observatory listening on http://127.0.0.1:45093/6p_NsmXILHw=/
    Copy the code
  • Set up RPC communication based on the sockets connection address and forward port established in step 2, using jSON_rpc_2.

    The RPC methods supported by the Dart VM can be found here: Dart VM Service Protocol 3.26

    For jSON-RPC, see here: JSON-RPC 2.0 Specification

    Note: Dart VM only supports WebSocket, not HTTP.

    “The VM will start a webserver which services protocol requests via WebSocket. It is possible to make HTTP (non-WebSocket) requests, but this does not allow access to VM events and is not documented here.”

    static Future<VMService> connect(                                                                            
      Uri httpUri, {                                                                                             
      ReloadSources reloadSources,                                                                               
      Restart restart,                                                                                           
      CompileExpression compileExpression,                                                                       
      io.CompressionOptions compression = io.CompressionOptions.compressionDefault,                              
    }) async {                                                                                                   
      final Uri wsUri = httpUri.replace(scheme: 'ws', path: fs.path.join(httpUri.path, 'ws'));                   
      final StreamChannel<String> channel = await _openChannel(wsUri, compression: compression);                 
      final rpc.Peer peer = rpc.Peer.withoutJson(jsonDocument.bind(channel), onUnhandledError: _unhandledError); 
      final VMService service = VMService(peer, httpUri, wsUri, reloadSources, restart, compileExpression);      
      // This call is to ensure we are able to establish a connection instead of                                 
      // keeping on trucking and failing farther down the process.                                               
      await service._sendRequest('getVersion'.const <String.dynamic> {});return service;                                                                                            
    }                                                                                                            
    Copy the code

    On the Dart VM specific use, we can see FlutterDevice. GetVMs () and FlutterDevice refreshViews () function.

    GetVMs () is used to get the Dart VM instance, and ultimately the RPC method getVM is called:

    @override                                                             
    Future<Map<String.dynamic>> _fetchDirect() => invokeRpcRaw('getVM'); 
    Copy the code

    RefreshVIews () is used to get the latest FlutterView instance, which ultimately calls the RPC method _flutter. ListViews:

    // When the future returned by invokeRpc() below returns,              
    // the _viewCache will have been updated.                              
    // This message updates all the views of every isolate.                
    await vmService.vm.invokeRpc<ServiceObject>('_flutter.listViews');     
    Copy the code

    This method does not belong to the Dart VM definition, but is an additional extension of Flutter, defined in engine-specific-service-protocol-extensions:

  • This is the final step in the initialization. Devfs is used to manage the device files, and when a hot reload is performed, the incremental packages are regenerated and then synchronized to the device.

    First, a directory is generated on the device to store the overloaded resource files and delta packages.

    @override                                                                             
    Future<Uri> create(String fsName) async {                                             
      final Map<String.dynamic> response = await vmService.vm.createDevFS(fsName);       
      return Uri.parse(response['uri']);                                                  
    }                                                                               
    
    /// Create a new development file system on the device.                             
    Future<Map<String.dynamic>> createDevFS(String fsName) {                           
      return invokeRpcRaw('_createDevFS', params: <String.dynamic> {'fsName': fsName}); 
    }                                                                                   
    Copy the code

    The generated Uri looks something like this: file:///data/user/0/com.example.my_app/code_cache/my_appLGHJYJ/my_app/ every FlutterDevice a would would used to encapsulate the device file synchronization. The following directories are created on the device:

    Every time a flutter run is executed, a new my_appXXXX directory is created. The modified resources are synchronized to this directory.

    Notice that I’m using the test project my_app here

    After the directory is generated, the resource files are synchronized once. Fonts, packages, assetmanifest.json, and so on are synchronized to the device.

    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
    Copy the code

Listen to the input

When changing the Dart code, we need to type r or r for our changes to take effect, where r stands for Hot Reload and r stands for Hot restart.

First, you need to register the input handler:

void setupTerminal() {                               
  assert(stayResident);                              
  if (usesTerminalUI) {                              
    if(! logger.quiet) { printStatus(' ');                               
      printHelp(details: false);                     
    }                                                
    terminal.singleCharMode = true; terminal.keystrokes.listen(processTerminalInput); }}Copy the code

When r is typed, the restart(false) method is eventually called:

if (lower == 'r') {                                                             
  OperationResult result;                                                       
  if (code == 'R') {                                                            
    // If hot restart is not supported for all devices, ignore the command.     
    if(! canHotRestart) {return;                                                                   
    }                                                                           
    result = await restart(fullRestart: true);                                  
  } else {                                                                      
    result = await restart(fullRestart: false);                                 
  }                                                                             
  if(! result.isOk) { printStatus('Try again after fixing the above error(s).', emphasis: true); }}Copy the code

The _reloadSources() function does the following:

  • Call the _updateDevFS() method to generate the incremental package and synchronize it to the device. DevFS is used to manage the device file system.

    First, compare the modification time of resource files to determine whether to update them:

    // Only update assets if they have been modified, or if this is the      
    // first upload of the asset bundle.                                     
    if(content.isModified || (bundleFirstUpload && archivePath ! =null)) {  
      dirtyEntries[deviceUri] = content;                                     
      syncedBytes += content.size;                                           
      if(archivePath ! =null&&! bundleFirstUpload) { assetPathsToEvict.add(archivePath); }}Copy the code

    DirtyEntries are used to store content to be updated, and syncedBytes counts the number of bytes to be synchronized.

    Next, generate the code increments package, ending with.increment.dill:

    final CompilerOutput compilerOutput = await generator.recompile(                                              
      mainPath,                                                                                                   
      invalidatedFiles,                                                                                           
      outputPath:  dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation),   
      packagesFilePath : _packagesFilePath,                                                                       
    );                                                                                                            
    Copy the code

    Finally, write to the device via HTTP:

    if (dirtyEntries.isNotEmpty) {                                                        
      try {                                                                               
        await _httpWriter.write(dirtyEntries);                                            
      } on SocketException catch (socketException, stackTrace) {                          
        printTrace('DevFS sync failed. Lost connection to device: $socketException');     
        throw DevFSException('Lost connection to device.', socketException, stackTrace);  
      } catch (exception, stackTrace) {                                                   
        printError('Could not update files on device: $exception');                       
        throw DevFSException('Sync failed', exception, stackTrace); }}Copy the code
  • Call the reloadSources() method to tell the Dart VM to reload the Dart delta package, which is also the RPC method called:

    final Map<String.dynamic> arguments = <String.dynamic> {'pause': pause,                                                                              
    };                                                                                             
    if(rootLibUri ! =null) {                                                                      
      arguments['rootLibUri'] = rootLibUri.toString();                                             
    }                                                                                              
    if(packagesUri ! =null) {                                                                     
      arguments['packagesUri'] = packagesUri.toString();                                           
    }                                                                                              
    final Map<String.dynamic> response = await invokeRpcRaw('_reloadSources', params: arguments); 
    return response;                                                                               
    Copy the code
  • Call flutterReassemble() to refresh the page, in this case the RPC method ext.flutter. Reassemble:

    Future<Map<String.dynamic>> flutterReassemble() {                
      return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble');  
    }                                                                 
    Copy the code

About Incremental packages

Let’s use a very simple DEMO to look at the contents of the generated incremental package. The DEMO has two dart files. The first is main.dart, which is the entry file:

void main() => runApp(MyApp());          
                                         
class MyApp extends StatelessWidget {    
  @override                              
  Widget build(BuildContext context) {   
    return MaterialApp(                  
      title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage(), ); }}Copy the code

Home.dart is also very simple and displays a text:

class HomePage extends StatelessWidget {   
  @override                                
  Widget build(BuildContext context) {     
    return Scaffold(                       
      body: Center(                        
        child: Text('Hello World'),        
      ),                                   
      appBar: AppBar(                      
        title: Text('My APP'),),); }}Copy the code

Here we make two changes. First, change the theme color from color. blue to color. red and change the “Hello World” in HomePage to “Hello Flutter”.

After modification, after the completion of the terminal type r, will be generated in the build directory app. Dill. Incremental. Dill, dill file is what? This is actually our code artifact, which is provided to the Dart VM for execution. Let’s use the strings command to check the contents:

The changes are already included in the incremental package, and increment.dill is synchronized to the device after we execute _updateDevFS().

The names are different, but the content is the same. Now that the device contains the delta package, the next step is to notify the Dart VM that it has refreshed, first calling reloadSources() and then flutterReassemble(), and once that’s done we can see the new interface.

conclusion

The implementation of hot overloading is the implementation of incremental packages, which we won’t go into detail here but will save for later articles. Incremental packages are generated with the suffix increment.dill, and file synchronization is performed over sockets connections established by ADB. Dart VM already defines RPC methods. Flutter extends this function to fetch Dart VM information. Refreshing the Flutter view and so on is done via RPC.

For space reasons, we do not cover the incremental package generation implementation here, as well as the Dart VM and Flutter Engine implementation of RPC methods. This will be saved for later articles.

Writing here, in fact, the goal of dynamic update is becoming clearer and clearer. First, generate incremental packages; Second, reload the refresh delta package when appropriate.