Hinweise zu Flutter | Routing, Pakete, Ressourcen, Ausnahmen und Debuggen in Flutter

Routing-Management

Im Allgemeinen handelt es sich beim Routing in Flutter um einen Seitensprung. Die Routing-Navigation wird über Komponenten in Flutter Navigatorverwaltet . Und stellt Methoden zum Verwalten des Stapels bereit. Wie zum Beispiel: Navigator.pushundNavigator.pop

Flutter bietet uns zwei Möglichkeiten, Routing-Sprünge zu konfigurieren: 1. Grundlegendes Routing , 2. Benanntes Routing

gemeinsame Streckennutzung

Wir wollen nun beispielsweise von der HomePage- Komponente zur SearchPage- Komponente springen .

1. SearchPage.dart muss in HomPage eingeführt werden

import '../SearchPage.dart';

2. Gehen Sie auf der Startseite die folgende Methode durch

Center(
	child: ElevatedButton( 
		onPressed: () {
    
    
			Navigator.push(context,
				MaterialPageRoute(builder: (context) {
    
    
						return const SearchPage();
				}));
		},
		child: const Text("跳转到搜索页面"),
	),
)

MaterialPageRoute

MaterialPageRouteDie von der PageRouteKlasse PageRoutegeerbte Klasse ist eine abstrakte Klasse, die eine modale Routing-Seite darstellt, die den gesamten Bildschirmbereich einnimmt. Außerdem definiert sie die zugehörigen Schnittstellen und Eigenschaften der Übergangsanimation während des Routenaufbaus und -wechsels. MaterialPageRouteEs handelt sich um eine Komponente, die von der Materialkomponentenbibliothek bereitgestellt wird. Sie kann Routing-Switching-Animationen implementieren, die dem Animationsstil für den Plattform-Seitenwechsel für verschiedene Plattformen entsprechen:

  • Wenn bei Android eine neue Seite geöffnet wird, wird die neue Seite vom unteren Bildschirmrand zum oberen Bildschirmrand verschoben. Wenn die Seite geschlossen wird, wird die aktuelle Seite vom oberen Bildschirmrand zum unteren Bildschirmrand verschoben Bildschirm und verschwindet dann, und die vorherige Seite wird auf dem Bildschirm angezeigt.
  • Wenn Sie unter iOS eine Seite öffnen, wird die neue Seite vom rechten Bildschirmrand zum linken Bildschirmrand verschoben, bis alle neuen Seiten auf dem Bildschirm angezeigt werden, während die vorherige Seite vom aktuellen Bildschirm nach links verschoben wird Wenn Sie eine Seite schließen, ist das Gegenteil der Fall: Die aktuelle Seite wird auf der rechten Seite des Bildschirms herausgeschoben, während die vorherige Seite auf der linken Seite des Bildschirms eingeblendet wird.
    Lassen Sie uns die Bedeutung jedes Parameters des MaterialPageRoute-Konstruktors vorstellen:
  MaterialPageRoute({
    
    
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • builderEs handelt sich um eine WidgetBuilderArt Rückruffunktion. Ihre Funktion besteht darin, den spezifischen Inhalt der Routing-Seite zu erstellen, und der Rückgabewert ist ein Widget. Normalerweise implementieren wir diesen Rückruf und geben eine Instanz der neuen Route zurück.
  • settingsEnthält die Konfigurationsinformationen der Route , z. B. den Routennamen und ob es sich um die ursprüngliche Route handelt (Startseite).
  • maintainState: Wenn eine neue Route auf den Stapel verschoben wird, wird standardmäßig die ursprüngliche Route weiterhin im Speicher gespeichert . Wenn Sie alle von der Route belegten Ressourcen freigeben möchten, wenn sie unbrauchbar ist, können Sie sie auf maintainStatefestlegen false.
  • fullscreenDialogGibt an, ob die neue Routing-Seite ein modaler Vollbilddialog ist . Wenn in iOS -fullscreenDialog wahr ist, wird die neue Seite vom unteren Bildschirmrand her eingeblendet (statt horizontal).

Navigator

NavigatorEs handelt sich um eine Routing-Management-Komponente, die Methoden zum Öffnen und Verlassen von Routing-Seiten bereitstellt. NavigatorEin Stapel wird verwendet , um den Satz aktiver Routen zu verwalten. Normalerweise ist die auf dem aktuellen Bildschirm angezeigte Seite die Route oben im Stapel. NavigatorBietet eine Reihe von Methoden zum Verwalten des Routing-Stacks. Hier stellen wir nur die beiden am häufigsten verwendeten Methoden vor:

1.Future push(BuildContext context, Route route)

Schieben Sie die angegebene Route auf den Stapel (d. h. öffnen Sie eine neue Seite), und der Rückgabewert ist ein FutureObjekt, das die Rückgabedaten empfängt, wenn die neue Route geöffnet (d. h. geschlossen) wird.

2.bool pop(BuildContext context, [ result ])

Entfernen Sie die oberste Route aus dem Stapel. resultDabei handelt es sich um die Daten, die beim Schließen der Seite an die vorherige Seite zurückgegeben werden.

NavigatorEs gibt viele andere Methoden wie Navigator.replaceusw. Navigator.popUntilWeitere Informationen finden Sie in der API-Dokumentation oder in den Kommentaren zum SDK-Quellcode. Daher werde ich sie hier nicht wiederholen.

Instanzmethode

NavigatorJede statische Methodecontext in der Klasse , deren erster Parameter ist, entspricht einer Instanzmethode mit derselben Funktion , z. B. äquivalent zu .Navigator.push(BuildContext context, Route route)Navigator.of(context).push(Route route)

Gewöhnlicher Routing-Jump-Pass-Wert

Beim Routing von Sprüngen können Sie Werte direkt über den Konstruktor der Komponente übergeben. Beispielsweise möchten Sie Parameter von HomePage an SearchPage unten übergeben.

1. Definieren Sie eine Suchseite, um den übergebenen Wert zu empfangen

import 'package:flutter/material.dart';
class SearchPage extends StatefulWidget {
    
    
    final String title;
    const SearchPage({
    
    
        super.key, this.title = "Search Page"
    });
    
    State < SearchPage > createState() => _SearchPageState();
}
class _SearchPageState extends State < SearchPage > {
    
    
    
    Widget build(BuildContext context) {
    
    
        return Scaffold(
            appBar: AppBar(
                title: Text(widget.title),
                centerTitle: true,
            ),
            body: const Center(
                child: Text("组件居中"),
            ),
        );
    }
}

2. Implementieren Sie die Wertübergabe auf der Sprungseite

Center(
  child: ElevatedButton(
      onPressed: () {
    
    
        Navigator.of(context).push(
            MaterialPageRoute(builder: (context) {
    
    
              return const SearchPage(title: "我是标题",);
            })
        );
      },
      child: const Text("跳转到搜索页面")
   ),
)

Benannter Routenübergabewert

Offizielle Dokumentation: Navigieren mit Argumenten

1. Konfigurieren Sie onGenerateRoute

import 'package:flutter/material.dart';
import './pages/tabs.dart';
import './pages/search.dart';
import './pages/form.dart';

void main() {
    
    
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
    
    
  MyApp({
    
    Key? key}) : super(key: key);
  
  // 1、配置路由, 定义Map类型的routes, Key为String类型,value为Function类型
  final Map<String, WidgetBuilder> routes = {
    
    
    '/':(context)=>const Tabs(), 
    '/search':(context,{
    
    arguments})=> SearchPage(arguments:arguments),
    '/login':(context)=>const LoginPage(), 
  };

  // 2. 固定写法 统一处理
  Route? onGenerateRoute(RouteSettings settings) {
    
    
    final String? name = settings.name;
    final Function? pageContentBuilder = routes[name];
    if (pageContentBuilder != null) {
    
    
      if (settings.arguments != null) {
    
    
        return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
      } else {
    
    
        return MaterialPageRoute(builder: (context) => pageContentBuilder(context));
      }
    }
    return null;
  }
  
  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue,),
      initialRoute: '/',
      //2、调用onGenerateRoute处理
      onGenerateRoute: onGenerateRoute,
    );
  }
}

2. Definieren Sie die Seite, um Argumente zur Übergabe von Parametern zu empfangen

import 'package:flutter/material.dart';

class SearchPage extends StatefulWidget {
    
    
  final Map arguments;
  const SearchPage({
    
    super.key, required this.arguments}); // 构造函数接受参数
  
  
  State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
    
    
  
  void initState() {
    
    
    super.initState();
    print(widget.arguments); // 打印接受到的参数
  }
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("我是搜索页面"),
      ),
    );
  }
}

3. Implementieren Sie die Parameterübergabe auf der Sprungseite

ElevatedButton(
   onPressed: () {
    
    
     Navigator.pushNamed(context, '/search', arguments: {
    
    
       "title": "搜索页面",
     });
   },
   child: const Text("打开搜索页面")
)

NavigatorNeben pushNamedMethoden gibt es auch pushReplacementNamedandere Möglichkeiten, benannte Routen zu verwalten. Sie können die API-Dokumentation selbst überprüfen.

RouteSetting ruft Routing-Parameter ab

Es ist auch möglich, settings.argumentsRouting-Parameter abzurufen, und der Komponentenkonstruktor muss keine zusätzlichen Parameter hinzufügen

class EchoRoute extends StatelessWidget {
    
    

  
  Widget build(BuildContext context) {
    
    
    //获取路由参数  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略无关代码
  }
}

Übergeben Sie Parameter beim Öffnen der Route:

Navigator.of(context).pushNamed("new_page", arguments: "hi");

Routing-Tabelle

Die Routing-Tabelle ist wie folgt definiert:

Map<String, WidgetBuilder> routes;

Es handelt sich um eine Zeichenfolge Map, keydie den Namen der Route darstellt. valueEs handelt sich um eine builderRückruffunktion, die zum Generieren der entsprechenden Route verwendet wird widget. Wenn wir über den Routennamen eine neue Route öffnen, findet die Anwendung die entsprechende WidgetBuilderRückruffunktion in der Routing-Tabelle entsprechend dem Routennamen und ruft dann die Rückruffunktion auf, um die Route zu generieren widgetund zurückzukehren.

Routing-Tabelle registrieren

Schauen Sie sich den Code direkt an:

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,),
  // home:Tabs(),
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  //注册路由表
  routes:{
    
    
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  } 
);

Es ist ersichtlich, dass wir, wenn wir die Root-Routing-Seite konfigurieren möchten, nur die Route in der Routing-Tabelle routesregistrieren MyHomePageund dann ihren Namen als Attributwert verwenden MaterialAppmüssen initialRoute, der bestimmt, welche benannte Route die anfängliche Routing-Seite der Anwendung ist . Dadurch wird homeder Parameter in der Standardbeispielvorlage überschrieben, um die Startseite anzugeben.

Routengenerierungs-Hook

MaterialAppEs gibt eine onGenerateRouteEigenschaft, die beim Öffnen einer benannten Route aufgerufen werden kann. Der Grund dafür ist, dass beim Aufruf der benannten Route die Funktion in der Routing-Tabelle Navigator.pushNamed(...)aufgerufen wird, wenn der angegebene Routenname in der Routing-Tabelle registriert ist zum builderGenerieren von Routing-Komponenten. Wenn keine Registrierung in der Routing-Tabelle vorhanden ist, wird sie onGenerateRoutezum Generieren von Routing aufgerufen. onGenerateRouteDie Rückrufsignatur lautet wie folgt:

Route<dynamic> Function(RouteSettings settings)

Mit onGenerateRouteRückrufen ist es sehr einfach, die obige Funktion zur Steuerung von Seitenberechtigungen zu implementieren: Anstatt die Routing-Tabelle zu verwenden, stellen wir einen onGenerateRouteRückruf bereit und führen dann im Rückruf eine einheitliche Berechtigungssteuerung durch, wie zum Beispiel:

MaterialApp(
  ... //省略无关代码
  onGenerateRoute:(RouteSettings settings){
    
    
	  return MaterialPageRoute(builder: (context){
    
    
		   String routeName = settings.name;
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 引导用户登录;其他情况则正常打开路由。
     }
   );
  }
);

Diese Funktion kann als Seiteninterceptor, Benutzerberechtigungsbeurteilung usw. verwendet werden.

Beachten Sie, dass onGenerateRoutedies nur für benannte Routen wirksam wird.

Einheitliche Routing-Tabellenkonfiguration in einer separaten Datei

Wir können die Funktionen der Routing-Tabelle und des Routing-Hooks einheitlich in einer separaten Dart-Datei konfigurieren, um die Verwaltung und Verwendung zu vereinfachen.

1. Erstellen Sie neue routers/routers.dart, um das Routing zu konfigurieren

import 'package:flutter/material.dart';

// 1.配置路由
final Map<String, WidgetBuilder> routes = {
    
    
  '/': (context) => const Tabs(),
  '/form': (context) => const FormPage(),
  '/product': (context) => const ProductPage(),
  '/productinfo': (context, {
    
    arguments}) => ProductInfoPage(arguments: arguments),
  '/search': (context, {
    
    arguments}) => SearchPage(arguments: arguments),
  '/login': (context) => const LoginPage(),
  '/registerFirst': (context) => const RegisterFirstPage(),
  '/registerSecond': (context) => const RegisterSecondPage(),
  '/registerThird': (context) => const RegisterThirdPage(),
};

// 2.onGenerateRoute
Route? onGenerateRoute(RouteSettings settings) {
    
    
  // 统一处理
  final String? name = settings.name;
  final Function? pageContentBuilder = routes[name];
  if (pageContentBuilder != null) {
    
    
    if (settings.arguments != null) {
    
    
      return MaterialPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
    } else {
    
    
      return MaterialPageRoute(
          builder: (context) => pageContentBuilder(context));
    }
  } else {
    
    
    // 可以在这里添加全局跳转错误拦截处理页面
    print("路由不存在");
    return null;
  }
}

Dann können Sie es so verwenden:

import 'package:flutter/material.dart';
import 'routes/Routes.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return const MaterialApp( 
        initialRoute: '/', //初始化的时候加载的路由
        onGenerateRoute: onGenerateRoute,
    );
  }
}

Dies ist der Fall bei der Verwendung von Routing-Hooks. Wenn Sie keine Routing-Hooks verwenden, können Sie so schreiben:

MaterialApp( 
  // ...
  initialRoute: "/",   
  routes: routes
);

Route zurück

Navigator.of(context).pop();

Die Route gibt den Wert zur vorherigen Seite zurück

Erstens wird es auf der Startseite hauptsächlich verwendet, um await/asyncauf das Rückgabeergebnis der zu öffnenden Seite zu warten, da es ein Objekt Navigator.pushNamedzurückgibt .Future

class RouterTestRoute extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: ElevatedButton(
        onPressed: () async {
    
    
          // 打开`TipRoute`,并等待返回结果
          var result = await Navigator.pushNamed(context, "tip_page", arguments: "初始参数");
          var result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
    
    
                return TipRoute(text: "我是提示xxxx"); // 路由参数
              },
            ),
          ); 
          print("路由返回结果: $result");
        },
        child: Text("打开提示页"),
      ),
    );
  }
}

// MaterialApp 配置
MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,), 
  initialRoute:"/",  
  routes:{
    
     
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), 
   "tip_page": (context) =>
            TipRoute(title: '${
      
      ModalRoute.of(context)?.settings.arguments}'),
  } 
);        

Verwenden Sie dann auf der geöffneten Routing-Seite Navigator.pop(context, result)den Wert, um ihn zurückzugeben.

class TipRoute extends StatelessWidget {
    
    
  final String title;

  const TipRoute({
    
    Key? key, required this.title}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(
        title: const Text("提示"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(title),
              ElevatedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),
                child: const Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Alternativroute

Beispielsweise sind wir von der User-Center-Seite zur registerFirstnächsten gesprungen und dann von registerFirstder Seite zur
pushReplacementNamednächsten registerSecond. Wenn wir zu diesem Zeitpunkt auf registerSeconddie Schaltfläche „Zurück“ klicken, kehren wir direkt zum Benutzercenter zurück.

Navigator.of(context).pushReplacementNamed('/registerSecond');

Root-Route zurückgeben

Wir sind zum Beispiel vom User Center zur registerFirstSeite gesprungen, dann von registerFirstder Seite zur registerSecondSeite und dann von registerSecondder Seite zur registerThirdSeite. Zu diesem Zeitpunkt möchten wir registerThirdnach erfolgreicher Registrierung zum Benutzercenter zurückkehren. Zu diesem Zeitpunkt wird die Methode der Rückkehr zur Root-Route verwendet.

Navigator.of(context).pushAndRemoveUntil(
        MaterialPageRoute(builder: (BuildContext context) {
    
    
          return const Tabs();
        }), (route) => false);

Fügen Sie hier eine Bildbeschreibung ein

Android und iOS verwenden den gleichen Routing-Jump-Stil

Die Materialkomponentenbibliothek stellt eine MaterialPageRoute-Komponente bereit, die die Routing-Umschaltanimation im Einklang mit dem Plattformstil verwenden kann, z. B. das Schieben nach links und rechts unter iOS und das Schieben nach oben und unten unter Android. CupertinoPageRoute ist ein iOS-Stil, der von der Cupertino-Komponente bereitgestellt wird Bibliothek Wenn Sie den Links-Rechts-Umschaltstil unter Android verwenden möchten, können Sie CupertinoPageRoute verwenden.

1. Fügen Sie cupertino.dart in routers.dart ein

import 'package:flutter/cupertino.dart';

2. Ändern Sie MaterialPageRoute in CupertinoPageRoute

import 'package:flutter/cupertino.dart';
import '../pages/tabs.dart';
import '../pages/shop.dart';
import '../pages/user/login.dart';
import '../pages/user/registerFirst.dart';
import '../pages/user/registerSecond.dart';
import '../pages/user/registerThird.dart';

//1、配置路由
Map routes = {
    
    
  "/": (contxt) => const Tabs(),
  "/login": (contxt) => const LoginPage(),
  "/registerFirst": (contxt) => const RegisterFirstPage(),
  "/registerSecond": (contxt) => const RegisterSecondPage(),
  "/registerThird": (contxt) => const RegisterThirdPage(),
  "/shop": (contxt, {
    
    arguments}) => ShopPage(arguments: arguments),
};

//2、配置onGenerateRoute 固定写法 这个方法也相当于一个中间件,这里可以做权限判断
var onGenerateRoute = (RouteSettings settings) {
    
    
  final String? name = settings.name;  
  final Function? pageContentBuilder = routes[name];  
  Function = (contxt) {
    
     return const NewsPage()}
  if (pageContentBuilder != null) {
    
    
    if (settings.arguments != null) {
    
    
      return CupertinoPageRoute(builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
    } else {
    
    
      return CupertinoPageRoute(builder: (context) => pageContentBuilder(context));
    }
  }
  return null;
};

Streckenbeobachter

Der Routenbeobachter kann alle Routensprungaktionen überwachen. Erstellen Sie zunächst eine Klassenvererbung, NavigatorObserverum die Routenüberwachung zu implementieren:

class MyObserver extends NavigatorObserver {
    
    
  
  void didPush(Route route, Route? previousRoute) {
    
    
    super.didPush(route, previousRoute);
    var currentName = route.settings.name;
    var previousName =
        previousRoute == null ? 'null' : previousRoute.settings.name;
    if (kDebugMode) {
    
    
      print('MyObserver-didPush-Current:$currentName  Previous:$previousName');
    }
  }

  
  void didPop(Route route, Route? previousRoute) {
    
    
    super.didPop(route, previousRoute);
    var currentName = route.settings.name;
    var previousName =
        previousRoute == null ? 'null' : previousRoute.settings.name;
    if (kDebugMode) {
    
    
      print('MyObserver-didPop--Current:$currentName  Previous:$previousName');
    }
  }
}

MaterialAppKonfigurieren Sie dann navigatorObserversdie Eigenschaft in:

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,), 
  initialRoute:"/",  
  routes:routes, 
  navigatorObservers: [MyObserver()], // 可以配多个  
);    

Achten Sie auf nicht registrierte Routen

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(primarySwatch: Colors.blue,), 
  initialRoute:"/",  
  routes:routes, 
  // 在打开一个不存在的命名路由时会被调用, 调用顺序为onGenerateRoute ==> onUnknownRoute
  onUnknownRoute: (RouteSettings settings){
    
    
   	String routeName = settings.name;
    print('未注册的路由:$routeName');
   }, 
);    

Paketverwaltung

Neben der Suche nach Bibliotheksdateien auf pub.dev und pubspec.yamldem Hinzufügen dieser Dateien unter der Datei gibt es mehrere Möglichkeiten:

  • Verlassen Sie sich auf lokale Pakete : Wenn wir ein Paket lokal entwickeln, lautet der Paketname pkg1, wir können uns auf folgende Weise darauf verlassen
dependencies:
	pkg1:
        path: ../../code/pkg1
  • Git-Abhängigkeiten : Sie können sich auch auf Pakete verlassen, die in Git-Repositorys gespeichert sind. Wenn sich das Paket im Stammverzeichnis des Repositorys befindet, verwenden Sie die folgende Syntax
dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git

Das Obige geht davon aus, dass sich das Paket im Stammverzeichnis des Git-Repositorys befindet. Ist dies nicht der Fall, kann über den Pfadparameter ein relativer Ort angegeben werden, zum Beispiel:

dependencies:
  package1:
    git:
      url: git://github.com/flutter/packages.git
      path: packages/package1        

Die oben beschriebenen Abhängigkeitsmethoden werden häufig in der Flutter-Entwicklung verwendet, es gibt jedoch auch andere Abhängigkeitsmethoden. Leser können den vollständigen Inhalt selbst überprüfen: https://www.dartlang.org/tools/pub/dependencies .

Achten Sie darauf, es nicht an der falschen Stelle hinzuzufügen, dependenciesspäter hinzuzufügen und nicht später hinzuzufügen dev_dependencies(dies ist das Toolkit, von dem die Konfigurationsentwicklungsumgebung abhängt, nicht das Paket, von dem die Flatteranwendung selbst abhängt).

Führen Sie nach der Konfiguration die Eingabeaufforderung „Pub get“ auf der Schnittstelle aus, um abhängige Pakete automatisch zu aktualisieren und herunterzuladen, oder führen Sie sie manuell in der Befehlszeile aus flutter packages get.

Tipps:

Wenn die von uns verwendeten Plug-Ins, insbesondere die nativen Plug-Ins, Fehler melden (dank der verwirrenden SDK-Version von Android und der schwindelerregenden AGP-Version), ist es am besten, auf pub.dev nach der neuesten Version zu suchen .

Wenn Sie jedoch die Plug-in-Bibliothek finden, öffnen Sie die Seite und sehen Sie: Vor 24 Monaten veröffentlicht... Guter Kerl, sie wurde seit mehr als 2 Jahren nicht mehr aktualisiert. . .

Zu diesem Zeitpunkt wird es sehr peinlich sein. Was soll ich tun? Es gibt zwei Möglichkeiten:

  • 1) Sie können auf pub.dev nach ähnlichen Plug-Ins suchen . Wenn Sie es beispielsweise schon einmal verwendet haben flutter_webview_plugin, hat der Autor der Bibliothek es noch nicht aktualisiert. Wir können nach Schlüsselwörtern suchen webview, um ähnliche Bibliotheken zu finden. Zunächst müssen wir uns nur zwei Indikatoren ansehen: die Veröffentlichungszeit und/ POPULARITYoder LIKESden Index. Je näher die Veröffentlichung rückt, POPULARITYdesto höher der Index, desto besser, da häufig die neueste Bibliothek die vorherigen Fehler behebt, und je höher der Index, desto weniger Probleme und bessere Kompatibilität.
  • 2) Wenn die erste Methode Ihr Problem nicht löst oder die gefundene Bibliothek neue Kompatibilitätsprobleme aufweist, können Sie auf der Github-Homepage der ursprünglichen Bibliothek suchen, in der das Problem aufgetreten ist, und die Antworten mit weiteren Kommentaren sehen, einige davon im Ausland. Das issuesTolle Götter hinterlassen oft ihre eigene forkVersionsadresse, die das Problem vorübergehend löst. Sie können versuchen, sie zu verwenden (über die Konfigurationsmethode der Git-Abhängigkeit oben).
  • 3) Wenn Sie auf der ganzen Welt suchen, aber nicht finden, was Sie suchen, aber professionelle Android- und iOS-Entwickler in Ihrem Team haben , können Sie den Quellcode der Originalbibliothek herunterladen, um eine Version zu reparieren und zu warten Ihr eigenes Team.
  • 4) Keines der oben genannten Probleme wurde gelöst, daher kann ich das nur zutiefst bedauern. Überlassen Sie es Gott!

Resourcenmanagement

pubspec.yamlIn der Dateikonfiguration können Ressourcendateien wie Bilder und Schriftarten hinterlegt werden

  assets:
    - assets/ 
    - images/ic_timg.jpg 
    - images/avatar.png 
    - images/bg.jpeg 
  fonts:
    - family: myIcon #指定一个字体名
      fonts:
        - asset: fonts/iconfont.ttf

Das Verzeichnis befindet sich hier auf pubspec.yamlderselben Ebene wie die Datei, normalerweise das Stammverzeichnis.

Es gibt viele Bilder, die im Projekt verwendet werden müssen. Manchmal möchten Sie sie nicht einzeln hinzufügen, da dies zu mühsam ist. Sie können sie mit der folgenden Methode stapelweise hinzufügen:

assets: [images/]

Anlagevariante

Der Build-Prozess unterstützt das Konzept der „Asset-Varianten“: Verschiedene Versionen eines Assets assetkönnen in unterschiedlichen Kontexten erscheinen. Wenn im Abschnitt der Datei ein Pfad angegeben wird , pubspec.yamlsucht der Erstellungsprozess in benachbarten Unterverzeichnissen nach Dateien mit demselben Namen. Diese Dateien werden dann mit der angegebenen Datei in die Datei eingebunden .assetsassetassetasset bundle

Wenn Sie beispielsweise die folgenden Dateien in Ihrem Anwendungsverzeichnis haben:

…/pubspec.yaml
…/graphics/background.png
…/graphics/dark/background.png

Dann pubspec.yamlenthält die Datei einfach:

flutter:
  assets:
    - graphics/background.png

Dann werden sowohl graphics/background.pngund in Ihre . Ersteres gilt als (Master-Ressource) und Letzteres als Variante ( ).graphics/dark/background.pngasset bundlemain assetvariant

Flutter verwendet assetdie Variante bei der Auswahl eines Bildes, das der aktuellen Geräteauflösung entspricht.

Vermögenswerte laden

Ihre App kann AssetBundleüber das Objekt darauf zugreifen asset. Es gibt zwei Hauptmethoden, die das Laden von String- oder Bilddateien (Binärdateien) Asset bundleaus .

1. Text-Assets laden

  • Laden über rootBundleObjekt: Jede Flutter-Anwendung verfügt über ein rootBundleObjekt, über das Sie einfach auf das Hauptressourcenpaket zugreifen können, indem Sie zum Laden direkt das package:flutter/services.dartglobale statische rootBundleObjekt verwenden asset.
  • Laden über DefaultAssetBundle: Es wird empfohlen, DefaultAssetBundlezum Abrufen des BuildContextStroms zu verwenden AssetBundle. Anstatt den Standard zu verwenden, mit dem die Anwendung erstellt wird asset bundle, macht dieser Ansatz das übergeordnete Element widgetzu einem anderen, das zur Laufzeit dynamisch ersetzt wird AssetBundle, was für Lokalisierungs- oder Testszenarien nützlich ist.

Normalerweise kann das indirekte Laden (z. B. JSON-Dateien) verwendet werden DefaultAssetBundle.of(), während die Anwendung ausgeführt wird , während das direkte Laden dieser Dateien außerhalb des Kontexts oder wenn andere Handles nicht verfügbar sind , verwendet werden kann , zum Beispiel:assetwidgetAssetBundlerootBundleasset

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
    
    
  return await rootBundle.loadString('assets/config.json');
}

2. Laden Sie das Bild

1) Deklarieren Sie auflösungsbezogene Bildressourcen

AssetImageassetDie angeforderte Logik kann dem nächstgelegenen Pixelverhältnis (dpi) des aktuellen Geräts zugeordnet werden asset. Damit diese Zuordnung funktioniert, muss sie gemäß einer bestimmten Verzeichnisstruktur geführt werden asset:

…/image.png
…/Mx/image.png
…/Nx/image.png

其中 MN 是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。

主资源默认对应于1.0倍的分辨率图片。看一个例子:

…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png

在设备像素比率为1.8的设备上,.../2.0x/my_icon.png 将被选择。对于2.7的设备像素比率,.../3.0x/my_icon.png将被选择。

如果未在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。 也就是说,如果.../my_icon.png72pxx72px,那么.../3.0x/my_icon.png应该是216pxx216px; 但如果未指定宽度和高度,它们都将渲染为72pxx72px(以逻辑像素为单位)。

pubspec.yamlasset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。

2)加载图片

要加载图片,可以使用 AssetImage 类。例如,我们可以从上面的asset声明中加载背景图片:

Widget build(BuildContext context) {
    
    
  return DecoratedBox(
    decoration: BoxDecoration(
      image: DecorationImage(
        image: AssetImage('graphics/background.png'),
      ),
    ),
  );
}

Beachten Sie, dass AssetImagees sich nicht um eins handelt widget, sondern tatsächlich um eins ImageProvider. Manchmal können Sie erwarten, direkt ein Anzeigebild zu erhalten widget. Dann können Sie Image.asset()Methoden wie die folgenden verwenden:

Widget build(BuildContext context) {
    
    
  return Image.asset('graphics/background.png');
}

Wenn Sie die Standardeinstellung asset bundlezum Laden von Ressourcen verwenden, werden die Auflösung usw. automatisch intern verarbeitet, und diese Prozesse sind für Entwickler nicht wahrnehmbar. ImageStream(Wenn Sie eine Klasse niedrigerer Ebene wie oder verwenden, ImageCachewerden Sie feststellen, dass es Parameter gibt, die sich auf die Skalierung beziehen.)

3) Verlassen Sie sich auf Ressourcenbilder im Paket

Um ein Bild aus einem abhängigen Paket zu laden, muss ein Argument AssetImageangegeben werden package.

Angenommen, Ihre Anwendung hängt von einem my_iconsPaket namens „ “ ab, das die folgende Verzeichnisstruktur aufweist:

…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png

Um das Bild dann zu laden, verwenden Sie:

AssetImage('icons/heart.png', package: 'my_icons')

oder:

Image.asset('icons/heart.png', package: 'my_icons')

Hinweis: Das Paket sollte auch durch Hinzufügen des Paketparameters abgerufen werden, wenn eigene Ressourcen verwendet werden.

Paketressourcen

Wenn pubspec.yamldie gewünschte Ressource in der Datei deklariert ist, wird sie in die entsprechende gepackt package. Insbesondere müssen die vom Paket selbst verwendeten Ressourcen pubspec.yamlangegeben werden.

Pakete können auch libRessourcen in ihre Ordner aufnehmen, die nicht pubspec.yamlin ihren Dateien deklariert sind. In diesem Fall muss die Anwendung pubspec.yamlangeben, welche Bilder in das Paket aufgenommen werden sollen, damit ein Bild gepackt werden kann. Ein Paket mit dem Namen „ “ kann beispielsweise fancy_backgroundsdie folgenden Dateien enthalten:

…/lib/backgrounds/background1.png

/lib/backgrounds/background2.png …/lib/backgrounds/background3.png

Um das erste Bild einzubinden, muss es pubspec.yamlim assetsAbschnitt deklariert werden:

flutter:
  assets:
    - packages/fancy_backgrounds/backgrounds/background1.png

lib/ist implizit und sollte daher nicht im Asset-Pfad enthalten sein.

Plattformspezifische Asset-Nutzung

Die oben genannten Ressourcen befinden sich alle in der Flutter-Anwendung. Diese Ressourcen können nur verwendet werden, nachdem das Flutter-Framework ausgeführt wird. Wenn wir das APP-Symbol festlegen oder ein Startbild zu unserer Anwendung hinzufügen möchten, müssen wir plattformspezifische Assets verwenden.

1) Legen Sie das APP-Symbol fest

Das Aktualisieren eines Startsymbols einer Flutter-App funktioniert auf die gleiche Weise wie das Aktualisieren eines Startsymbols in einer nativen Android- oder iOS-App.

  • Android

    Navigieren Sie im Stammverzeichnis des Flutter-Projekts zu ../android/app/src/main/resdem Verzeichnis, das verschiedene Ressourcenordner enthält (falls mipmap-hdpidas Platzhalterbild „ ic_launcher.png“ bereits enthalten ist, siehe Abbildung unten). Befolgen Sie einfach die Anweisungen im Android-Entwicklerhandbuch und ersetzen Sie sie durch die von Ihnen benötigten Ressourcen. Beachten Sie dabei die empfohlenen Symbolgrößenstandards für die jeweilige Bildschirmdichte (dpi). HINWEIS: Wenn Sie die Datei
    Fügen Sie hier eine Bildbeschreibung ein
    umbenennen , müssen Sie auch den Namen in den Eigenschaften Ihres Tags aktualisieren ..pngAndroidManifest.xml<application>android:icon

  • iOS

    Navigieren Sie im Stammverzeichnis Ihres Flutter-Projekts zu ../ios/Runner. Dieses Verzeichnis Assets.xcassets/AppIcon.appiconsetenthält bereits Platzhalterbilder (siehe Bild unten). Ersetzen Sie sie einfach durch Bilder in der entsprechenden Größe und behalten Sie dabei die ursprünglichen Dateinamen bei.
    Fügen Sie hier eine Bildbeschreibung ein

2) Startseite aktualisieren

Wenn das Flutter-Framework geladen ist, verwendet Flutter den nativen Plattformmechanismus, um die Begrüßungsseite zu zeichnen. Diese Begrüßungsseite bleibt bestehen, bis Flutter den ersten Frame der App rendert.

Hinweis: Das bedeutet, dass der Begrüßungsbildschirm für immer angezeigt wird, wenn Sie die Funktion nicht in der main()Methode Ihrer Anwendung aufrufen (oder genauer gesagt, wenn Sie die Antwort runAppnicht aufrufen ).window.renderwindow.onDrawFrame

  • Android

    Um Ihrer Flutter-App einen Begrüßungsbildschirm hinzuzufügen, navigieren Sie zu ../android/app/src/main. Realisieren Sie nun die benutzerdefinierte Startoberfläche res/drawable/launch_background.xmldurch Anpassen (Sie können ein Bild auch direkt ändern).drawable

  • iOS

    Um ein Bild in der Mitte des Begrüßungsbildschirms hinzuzufügen, navigieren Sie zu ../ios/Runner. Ziehen Sie das Bild hinein Assets.xcassets/LaunchImage.imagesetund benennen Sie es LaunchImage.png、[email protected][email protected]. Wenn Sie einen anderen Dateinamen verwenden, müssen Sie auch Contents.jsondie Datei im selben Verzeichnis aktualisieren. Die spezifische Größe des Bildes entnehmen Sie bitte dem offiziellen Standard von Apple.

    Sie können es auch Xcodevollständig anpassen, indem Sie öffnen storyboard. Project NavigatorNavigieren Sie zu einem Bild und Runner/Runnerziehen Sie es dann hinein, indem Sie es öffnen Assets.xcassets, oder passen Sie es an , indem LaunchScreen.storyboardSie es wie gezeigt in verwenden .Interface Builder
    Fügen Sie hier eine Bildbeschreibung ein

Von der Plattform gemeinsam genutzte Ressourcen

Offizielle Dokumentation: Vermögenswerte mit der zugrunde liegenden Plattform teilen

Wenn wir das Flutter + native-Entwicklungsmodell übernehmen, kann es Situationen geben, in denen Flutter und Native Ressourcen gemeinsam nutzen müssen. Beispielsweise gibt es im Flutter-Projekt bereits ein Bild A. Wenn A auch im nativen Code verwendet wird, können wir es kopieren A Ein spezifisches Verzeichnis für das native Projekt. Auf diese Weise kann die Funktion zwar realisiert werden, das endgültige Anwendungspaket wird jedoch größer, da es doppelte Ressourcen enthält. Um dieses Problem zu lösen, stellt Flutter eine gemeinsam genutzte Ressource zwischen Flutter und Native bereit.

1. Laden Sie die Flutter-Ressourcendatei in Android

Lesen Sie auf der Android-Plattform die API assetsdurch AssetManager. Verwenden Sie die Methode PluginRegistry.Registrarvon lookupKeyForAssetoder, um den Dateipfad abzurufen, und rufen Sie dann den Dateideskriptor entsprechend dem Dateipfad ab. Es kann bei der Entwicklung von Plug-Ins verwendet werden und ist die beste Wahl bei der Entwicklung von Anwendungen mit Plattformansichten .FlutterViewgetLookupKeyForAssetAssetManageropenFdPluginRegistry.RegistrarFlutterView

Angenommen, Sie geben dies beispielsweise pubspec.yamlin Ihrem an:

flutter:
  assets:
    - icons/heart.png

Entspricht der folgenden Struktur in Ihrer Flutter-Anwendung.

.../pubspec.yaml
.../icons/heart.png
...etc.

Möchten Sie Javaim Plugin zugreifen icons/heart.png:

AssetManager assetManager = registrar.context().getAssets();
String key = registrar.lookupKeyForAsset("icons/heart.png");
AssetFileDescriptor fd = assetManager.openFd(key);

2. Laden Sie die Flutter-Ressourcendatei in iOS

Auf der iOS-Plattform assetswerden Ressourcendateien mainBundleüber gelesen. Rufen Sie den Dateipfad über pathForResource:ofType:die Methode „ lookupKeyForAssetoder “ ab. Ebenso kann die Methode „oder “ auch den Dateipfad abrufen. Es kann bei der Entwicklung von Plug-Ins verwendet werden und ist die beste Wahl bei der Entwicklung von Anwendungen mit Plattformansichten .lookupKeyForAsset:fromPackage:FlutterViewControllerlookupKeyForAsset:lookupKeyForAsset:fromPackage:FlutterPluginRegistrarFlutterViewController

Angenommen, Ihre Flutter-Konfiguration ist dieselbe wie oben.

So greifen Sie in einem Objective-C-Plugin zu icons/heart.png:

NSString* key = [registrar lookupKeyForAsset:@"icons/heart.png"];
NSString* path = [[NSBundle mainBundle] pathForResource:key ofType:nil];

Hier ist ein umfassenderes Beispiel zum Verständnis der Anwendung von Flutter: video_player-Plugin .

Das Plugin auf pub.devios_platform_images kapselt diese Logik in praktische Klassen. Es erlaubt zu schreiben:

Ziel c:

[UIImage flutterImageWithName:@"icons/heart.png"];

Schnell:

UIImage.flutterImageNamed("icons/heart.png")

Verwenden Sie Icons-Symbolressourcen von Drittanbietern

Hier sind zwei benutzerfreundliche Ressourcen-Websites für Icons-Icons, die denjenigen, die sich mit Front-End-Entwicklung befassen, möglicherweise bekannt sind. Sie sind:

  • iconfont : https://www.iconfont.cn/ Sie können nach dem gewünschten Symbol suchen und es dann herunterladen (Anmeldung erforderlich), das verschiedene Formate enthält, und Sie können es zur Verwendung in Flutter importieren.
  • fluttericon : https://fluttericon.com/ Dies wurde speziell für Flutter entwickelt und bietet viele Symbole im Material Design-Stil. Wählen Sie einfach das gewünschte Symbol auf der Seite aus und klicken Sie oben auf die Schaltfläche DOWNLOAD, um es herunterzuladen. Der Download Die Datei wird gleichzeitig angezeigt. Enthält DartBeispielcode mit:

Beachten Sie, dass diese beiden Websites zwar Symbole in anderen Formaten bereitstellen. Versuchen Sie jedoch, TTF- Schriftartsymbole auszuwählen, da die Farbe der Schriftartsymbole dynamisch festgelegt werden kann. Andernfalls wird es peinlich, wenn Sie die Farbe des Anwendungsthemas ändern müssen . Wenn Sie sich dafür entscheiden, normale PNG-Bilder als Symbole zu verwenden, machen Sie sich natürlich nicht die Mühe und lassen Sie die Bilder vom Designer zuschneiden.

Laden Sie Android-Ressourcen in Flutter

AssetsFür die Integration von Flutter in ein bestehendes Android-Geschäft besteht die offizielle Lösung von Flutter darin, Ressourcen zu integrieren und AssetImageüber dieses Widget zu verwenden, wenn Bildressourcen (z. B. Symbole) verwendet werden müssen . Der obige Ansatz hat ein Problem mit der Transformation des Aktiengeschäfts: Da viele Bildressourcen in drawableoder in assetsbenutzerdefinierten Verzeichnissen abgelegt werden können, werden die Bildressourcen von Flutter in assets/flutter_assetsVerzeichnissen gespeichert. Sofern die ursprünglichen Bildressourcen nicht verschoben werden, müssen sie im Verzeichnis gespeichert werden Neues Verzeichnis zum Speichern derselben Bildressourcen. Gibt es also eine Lösung, die es uns ermöglicht, frei auf Bildressourcen in verschiedenen Verzeichnissen in Android zu drawableverweisen assets?

Wir wissen, dass ImageProviderdie Bilddatenquelle beispielsweise NetworkImageüber eine HTTP-Verbindung bereitgestellt werden kann. Ist es also möglich, Bildressourcen im Android-Apk-Installationspaket anzufordern, beispielsweise durch das Anfordern von Netzwerkbildern? Wir wissen, dass Platform ChannelDaten zwischen Flutter und Platform übertragen werden können, daher sollten auch Bildressourcen möglich sein. Daher können wir ImageProvidereinen neuen Datenkanal bereitstellen, und die Datenquelle des Datenkanals wird Platform Channelvon bereitgestellt.

Implementieren Sie zunächst die logische Kapselung des Flutter-Seiten-Plug-Ins. Der Code lautet wie folgt:

enum AndroidPlatformImageType {
    
     drawable, assets, }
class AndroidPlatformImage extends ImageProvider<AndroidPlatformImage> {
    
    
  const AndroidPlatformImage( // 对外暴露的接口和参数
      this.id, {
    
    
        this.scale = 1.0,
        this.quality = 100,
        this.type = AndroidPlatformImageType.drawable
      });
  static const MethodChannel _channel = MethodChannel('plugins.flutter.io/android_platform_images'); // 用于传输图片数据 
  final String id;
  final int quality;
  final double scale;
  final AndroidPlatformImageType type;
  
  ImageStreamCompleter load(AndroidPlatformImage key,DecoderCallback decode) {
    
    
    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, decode),  
      scale: key.scale,
      debugLabel: key.id,
      informationCollector: () sync* {
    
    
        yield ErrorDescription('Resource: $id');
      },
    );
  } 
}

Das Obige ist ImageProviderdie herkömmliche Vererbungslogik. Der Kern liegt darin, _loadAsyncwie die Methode die Datenquelle bereitstellt. Der Code lautet wie folgt:

class AndroidPlatformImage extends ImageProvider<AndroidPlatformImage> {
    
    

   // 定义Key规则
  Future<AndroidPlatformImage> obtainKey(ImageConfiguration configuration) {
    
    
    return Future<AndroidPlatformImage>.value(this);
  }
  Future<ui.Codec> _loadAsync( // 异步请求图片数据
      AndroidPlatformImage key, DecoderCallback decode) async {
    
    
    assert(key == this);
    final Uint8List? bytes = await _channel.invokeMethod<Uint8List>( // Platform Channel调用
        describeEnum(type), // 资源类型
        <String, dynamic>{
    
    
          'id' : id, // 图片的唯一id
          'quality': quality, // 图片编码的质量
        }
    );
    if (bytes == null) {
    
    
      throw StateError('$id does not exist and cannot be loaded as an image.');
    }
    return decode(bytes);
  }}

Die obige Logik MethodChannelfordert die binären Informationen des Bildes von Android an, und die Erläuterung der relevanten Parameter wurde im Code angegeben. Die Logik auf der Android-Seite wird unten analysiert. Um Wiederverwendbarkeit und eine klare Architektur zu gewährleisten, sollten Sie zunächst die Implementierung eines Plug-Ins in Betracht ziehen. Der Code lautet wie folgt:

public class AndroidPlatformImagesPlugin implements FlutterPlugin, MethodCallHandler {
    
    
  static final String TAG = "AndroidPlatformImages"; // 各种字段的定义
  private static final String CHANNEL_NAME = "plugins.flutter.io/android_platform_images";
  private static final String DRAWABLE = "drawable";
  private static final String ASSETS = "assets";
  public static final HashMap<String, Integer> resourceMap = new HashMap<>();
  DrawableImageLoader drawableImageLoader;
  AssetsImageLoader assetsImageLoader;
  private MethodChannel channel;
  private ExecutorService fixedThreadPool;
  private Handler mainHandler;
  private static final String ARG_ID = "id"; // 对应Framework中定义的参数
  private static final String ARG_QUALITY = "quality";
  @Override // 绑定FlutterEngine
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    
    
    channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL_
        NAME);
    channel.setMethodCallHandler(this);
    drawableImageLoader = 
new DrawableImageLoader(flutterPluginBinding.getApplicationContext());
    assetsImageLoader = new AssetsImageLoader
        (flutterPluginBinding.getApplicationContext());
    mainHandler = new Handler(flutterPluginBinding.getApplicationContext().
        getMainLooper());
    int THREAD_POOL_SIZE = 5; // 用于解码图片的线程池数目
    fixedThreadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
  }
  @Override // 响应Flutter中的_loadAsync数据请求
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    
    
    final MethodCall methodCall = call;
    final Result finalResult = result;
    fixedThreadPool.submit(new Runnable() {
    
    
      @Override // 异步加载,避免主线程阻塞
      public void run() {
    
    
        asyncLoadImage(methodCall, finalResult); // 开始异步加载图片数据
      }
    });
  }
  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    
    
    channel.setMethodCallHandler(null); // FlutterEngine销毁,相关资源销毁
    drawableImageLoader.dispose();
    assetsImageLoader.dispose();
    fixedThreadPool.shutdown();
    fixedThreadPool = null;
    mainHandler = null;
  }
 
}

Die obige Logik bezieht sich nicht auf die spezifischen Details des Bildladens, sondern zeigt eine typische Programmstruktur von FlutterPlugin, die hauptsächlich die Initialisierung onAttachedToEngineverwandter Felder in abschließt und onDetachedFromEngineRessourcen in freigibt, was für das Schreiben von qualitativ hochwertigem Code von entscheidender Bedeutung ist.

AndroidPlatformImageDarüber hinaus verwendet die obige Logik einen Thread-Pool beim Laden von Bildern, hauptsächlich um ein asynchrones gleichzeitiges Laden zu erreichen, was die Benutzererfahrung verbessern kann, insbesondere wenn die Benutzeroberfläche mehrere Typen enthält Image. Der endgültige Logikcode zum Laden von Bildern lautet wie folgt:

public class AndroidPlatformImagesPlugin implements FlutterPlugin, MethodCallHandler {
    
    
 
  private void asyncLoadImage(final MethodCall call, final Result result) {
    
    
    String id = call.argument(ARG_ID); // 参数解析
    int quality = call.argument(ARG_QUALITY);
    byte[] ret = null;
    long start = 0L;
    if (DRAWABLE.equals(call.method) && drawableImageLoader != null) {
    
    
      ret = drawableImageLoader.loadBitmapDrawable(id, quality); // 加载drawable资源
    } else if (ASSETS.equals(call.method) && assetsImageLoader != null) {
    
    
      ret = assetsImageLoader.loadImage(id); // 加载assets资源
    }
    if (ret == null) {
    
     return; } // 加载失败
    final byte[] finalRet = ret;
    mainHandler.post(new Runnable() {
    
    
      @Override
      public void run() {
    
     // 在主线程中返回数据
        result.success(finalRet);
      }
    });
  }
}

Die obige Logik wird entsprechend dem Laden dem entsprechenden Bildressourcentyp zugewiesen ImageLoader. Es ist zu beachten, dass die Rückgabe von Daten dennoch erfolgen muss, da der Plattformkanal in der obigen Logik im Hauptthread ausgeführt werden muss, obwohl das Laden des Bildes asynchron erfolgt im Hauptthread sein.

Analysieren Sie als Nächstes die Ladelogik des Bildes. Analysieren Sie zunächst drawabledie Art des Bildladens. Der Code lautet wie folgt:

class DrawableImageLoader extends ImageLoader {
    
    
  DrawableImageLoader(Context context) {
    
    
    this.appContext = context;
  }
  public byte[] loadBitmapDrawable(String name, int quality) {
    
    
    byte[] buffer = null;
    Drawable drawable = null;
    try {
    
     // 用户是在Embedder中通过resourceMap注册的
      Integer id = AndroidPlatformImagesPlugin.resourceMap.get(name); // 第1步,通过resourceMap获取资源id
      if (id == null) {
    
    
        String type = "drawable"; // 第2步,通过系统API接口进行查询
        id = appContext.getResources().getIdentifier( // 系统API查询
            name, type, appContext.getPackageName());
      }
      if (id <= 0) {
    
      return buffer; } // 找不到有效的资源id
      drawable = ContextCompat.getDrawable(appContext, id);
    } catch (Exception ignore) {
    
    }
    if (drawable instanceof BitmapDrawable) {
    
    
      Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); // 转换为Bitmap
      if (bitmap != null) {
    
     // 第3步,通过继承的手段重写buffer方法
        ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, quality, stream); // 序列化
        buffer = stream.buffer();
      }
    }
    return buffer;
  }
  static final class ExposedByteArrayOutputStream extends ByteArrayOutputStream {
    
    
    byte[] buffer() {
    
     return buf; }
  }
}
 
// 基类代码 
abstract class ImageLoader {
    
    
  protected Context appContext;
  public void dispose() {
    
    
    appContext = null;
  }
}  

In der obigen Logik sind noch viele Details enthalten, und es gibt insgesamt 3 Stellen. Der erste Schritt besteht darin , zuerst resourceMapRessourcen abzurufen id. Da res/drawbledas Verzeichnis möglicherweise verwirrt und komprimiert ist, kann der Entwickler hier den Index des Namens der Ressource anpassen id. Schritt 2: getIdentifierWenn die Abfrage über die System-API-Schnittstelle fehlschlägt, kehren Sie direkt zurück. Da ByteArrayOutputStreamdie bufferMethode in Schritt 3 eine tiefe Kopie durchführt, wird die Methode direkt durch Vererbung neu geschrieben, wodurch eine tiefe Kopie vermieden wird, eine Technik, die auch im Quellcode von Embedder verwendet wird.

Schließlich assetsist der Code für das Laden der Bildressource vom Analysetyp wie folgt:

class AssetsImageLoader extends ImageLoader {
    
    
  AssetsImageLoader(Context context) {
    
    
    this.appContext = context;
  }
  public byte[] loadImage(String path) {
    
    
    byte[] buffer = null;
    AssetManager assetManager = appContext.getAssets();
    InputStream inputStream;
    try {
    
    
      inputStream = assetManager.open(path);
      buffer = new byte[inputStream.available()];
      inputStream.read(buffer);
    } catch (IOException ignored){
    
     }
    return buffer;
  }
}

Die obige Logik ist relativ einfach, hauptsächlich für den Aufruf AssetManagerverwandter APIs, daher werde ich hier nicht auf Details eingehen.

Obwohl der oben gezeigte Fall einfach ist, nämlich die Wiederverwendung der ursprünglichen Android-Bildressourcen in der Flutter-Benutzeroberfläche, umfasst er viele Details, einschließlich der Codeorganisation des FlutterPlugins, des effizienten Ladens von Bildern über den Thread-Pool, der Vermeidung von Verwirrung und der Vermeidung von resourceMapVererbung ByteArrayOutputStreamViele Techniken wie Deep Copying müssen in der Praxis vertieft werden.

Debuggen Sie die Flutter-App

Zusammenfassung herkömmlicher Debugging-Methoden:

  • Breakpoint-Debugging geschickt nutzen
  • Debugger-Panel
  • Nutzen Sie die Fenster „Variablen“ und „Beobachter“ sinnvoll aus
  • Nutzen Sie die Konsole (Console) sinnvoll für die Protokollanalyse
  • Verwendung von Dart DevTools
  • Diagnostizieren Sie Layoutprobleme mit dem Flutter Inspector (unklar über vorhandene Layouts).
  • Verwendung von Flutter Outline

Protokollierung und Haltepunkte

1. debugger()-Anweisung

Bei Verwendung von Dart Observatory(oder einem anderen DartDebugger, z. B. dem in der IntelliJ-IDE) können Sie diese debugger()Anweisung verwenden, um programmgesteuerte Haltepunkte einzufügen. Um dies zu nutzen, müssen Sie import 'dart:developer';oben in der entsprechenden Datei hinzufügen.

debugger()Die Anweisung benötigt einen optionalen whenParameter, den wir so angeben können, dass er nur unterbrochen wird, wenn eine bestimmte Bedingung wahr ist, etwa so:

void someFunction(double offset) {
    
    
  debugger(when: offset > 30.0);
  // ...
}

2. Drucken、DebugDrucken、Flatterprotokolle

Die Dart- print()Funktion wird an die Systemkonsole ausgegeben, und wir können Flatterprotokolle verwenden, um sie anzuzeigen (im Grunde ein Wrapper um adb logcat).

Android löscht manchmal einige Protokollzeilen, wenn Sie zu viel auf einmal ausgeben. Um diese Situation zu vermeiden, können wir die Flutter- foundationBibliothek debugPrint()verwenden (das Flutter/Foundation-Paket muss importiert werden), die den Druck kapselt und die Länge des Inhalts einer Ausgabe auf eine Ebene begrenzt (wenn zu viel Inhalt vorhanden ist, wird dies der Fall sein). werden in Stapeln ausgegeben), wodurch vermieden wird, dass es zu Android-Kernel-Drops kommt.

Sie können auch entscheiden, ob Protokolle nur im Modus gemäß kDebugModeund ausgegeben werden sollen :kReleaseModedebug/release

import 'package:flutter/foundation.dart';

if (kDebugMode) print("只在Debug模式下输出");
if (kReleaseMode) print("只在Release模式下输出");
 debugPrint("AAA");

Viele Klassen im Flutter-Framework verfügen über toStringImplementierungen. Konventionell umfassen die Ausgabeinformationen Informationen wie den Laufzeittyp des Objekts, den Klassennamen und Schlüsselfelder. Einige Klassen im Baum verfügen auch über toStringDeepImplementierungen, die von diesem Punkt an eine mehrzeilige Beschreibung des gesamten Teilbaums zurückgeben. Einige Klassen mit Details toStringimplementieren eine toStringShort, die lediglich den Typ oder eine andere sehr kurze Beschreibung (ein oder zwei Wörter) des Objekts zurückgibt.

3. Debug-Modus-Behauptungen

Während des Debuggens der Flutter-App sind Dart- assertAnweisungen aktiviert und das Flutter-Framework verwendet sie, um eine Reihe von Laufzeitprüfungen durchzuführen, um zu überprüfen, ob einige unveränderliche Regeln nicht verletzt werden. Wenn eine bestimmte Regel verletzt wird, wird ein Fehlerprotokoll mit einigen Kontextinformationen auf der Konsole ausgegeben, um die Ursache des Problems zu ermitteln.

Um den Debug-Modus zu deaktivieren und den Release-Modus zu verwenden, führen Sie flutter run --releaseunsere App aus. Dadurch wird auch Observatoryder Debugger geschlossen. Ein Zwischenmodus kann Observatoryalle anderen Debugging-Hilfsmittel namens „ profile mode“ schließen und einfach --profileersetzen --release.

4. Haltepunkte

Während des Entwicklungsprozesses stellen Haltepunkte eines der praktischsten Debugging-Tools dar. Nehmen wir Android Studio als Beispiel:

Fügen Sie hier eine Bildbeschreibung ein
Wir setzen einen Haltepunkt in Zeile 93. Sobald der Code bis zu dieser Zeile ausgeführt wird, wird er angehalten. Zu diesem Zeitpunkt können wir die Werte aller Variablen im aktuellen Kontext sehen und dann auswählen, den Code schrittweise auszuführen Schritt. Um den Punkt durch die IDE zu brechen, können Sie selbst nach vielen Online-Tutorials suchen.

Bedauernsmedizin beim Flutter-Debugging:

  • Fallback über Frames
  • Fallback über Drop Frame

Fügen Sie hier eine Bildbeschreibung ein

Debuggen Sie die Anwendungsschicht

Jede Ebene des Flutter-Frameworks bietet Funktionen zum Ausgeben ( dump) ihres aktuellen Status oder ihrer aktuellen Ereignisse an die Konsole (use ).debugPrint

1. Debugging des Widget-Baums debugDumpApp()

Um Widgetsden Zustand des Baums zu sichern, rufen Sie auf debugDumpApp(). Wir können diese Methode ( nach dem Aufruf) jederzeit aufrufen build(), wenn sich die Anwendung nicht in der Build-Phase befindet (d. h. nicht innerhalb der Methode aufgerufen wird), solange die Anwendung mindestens einmal erstellt wurde (d. h. jederzeit nach dem Aufruf).build()runApp()

Zum Beispiel diese Anwendung:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(
    MaterialApp(
      home: AppHome(),
    ),
  );
}

class AppHome extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Material(
      child: Center(
        child: TextButton(
          onPressed: () {
    
    
            debugDumpApp();
          },
          child: Text('Dump App'),
        ),
      ),
    );
  }
}

würde etwa so etwas ausgeben:

I/flutter ( 6559): WidgetsFlutterBinding - CHECKED MODE
I/flutter ( 6559): RenderObjectToWidgetAdapter<RenderBox>([GlobalObjectKey RenderView(497039273)]; renderObject: RenderView)
I/flutter ( 6559):MaterialApp(state: _MaterialAppState(1009803148))
I/flutter ( 6559):ScrollConfiguration()
I/flutter ( 6559):AnimatedTheme(duration: 200ms; state: _AnimatedThemeState(543295893; ticker inactive; ThemeDataTween(ThemeData(Brightness.light Color(0xff2196f3) etc...) → null)))
I/flutter ( 6559):Theme(ThemeData(Brightness.light Color(0xff2196f3) etc...))
I/flutter ( 6559):WidgetsApp([GlobalObjectKey _MaterialAppState(1009803148)]; state: _WidgetsAppState(552902158))
I/flutter ( 6559):CheckedModeBanner()
I/flutter ( 6559):Banner()
I/flutter ( 6559):CustomPaint(renderObject: RenderCustomPaint)
I/flutter ( 6559):DefaultTextStyle(inherit: true; color: Color(0xd0ff0000); family: "monospace"; size: 48.0; weight: 900; decoration: double Color(0xffffff00) TextDecoration.underline)
I/flutter ( 6559):MediaQuery(MediaQueryData(size: Size(411.4, 683.4), devicePixelRatio: 2.625, textScaleFactor: 1.0, padding: EdgeInsets(0.0, 24.0, 0.0, 0.0)))
I/flutter ( 6559):LocaleQuery(null)
I/flutter ( 6559):Title(color: Color(0xff2196f3))
... #省略剩余内容

Dies ist ein „abgeflachter“ Baum, der alles zeigt, was durch die verschiedenen Build-Funktionen projiziert wird ( dies ist der Baum, den Sie erhalten widget, wenn Sie die Wurzel des Widget-Baums aufrufen ). toStringDeepwidgetSie werden viele Dinge sehen, die nicht im Quellcode Ihrer Anwendung erscheinen, widgetweil sie durch Funktionen widgetim Framework eingefügt werden. „Ja“ ist build()beispielsweise ein Implementierungsdetail.InkFeatureMaterial widget

debugDumpApp()Wird aufgerufen , wenn die Taste vom Drücken zum Loslassen wechselt . TextButtonDas Objekt ruft gleichzeitig auf setState()und markiert sich selbst als „ dirty“. Wir können auch sehen, welche Gesten-Listener registriert wurden; in diesem Fall wird ein einzelner GestureDetectoraufgelistet, der auf die „ tap“-Geste wartet („Tippen“ ist die Ausgabe TapGestureDetectorder toStringShortFunktion).

Wenn wir unsere eigenen schreiben widget, können wir debugFillProperties()durch Überschreiben Informationen hinzufügen. Verwenden Sie DiagnosticsPropertydas Objekt als Methodenparameter und rufen Sie die Methode der Oberklasse auf. Diese Funktion wird von dieser Methode verwendet, toStringum die Beschreibungsinformationen des Widgets einzugeben.

2. Debuggen des Renderbaums debugDumpRenderTree()

Wenn wir versuchen, Layoutprobleme zu beheben, ist der Widget-Baum möglicherweise nicht detailliert genug. In diesem Fall können wir debugDumpRenderTree()den Renderbaum durch Aufrufen sichern Wie in debugDumpApp()können wir diese Funktion jederzeit aufrufen, außer während der Layout- oder Zeichnungsphase. Als allgemeine Regel gilt, dass frameder Aufruf über einen Callback- oder Event-Handler die beste Lösung ist.

Um aufzurufen , müssen wir etwas zu unserer Quelldatei debugDumpRenderTree()hinzufügen .import'package:flutter/rendering.dart';

Die Ausgabe des kleinen Beispiels oben würde so aussehen:

I/flutter ( 6559): RenderView
I/flutter ( 6559):  │ debug mode enabled - android
I/flutter ( 6559):  │ window size: Size(1080.0, 1794.0) (in physical pixels)
I/flutter ( 6559):  │ device pixel ratio: 2.625 (physical pixels per logical pixel)
I/flutter ( 6559):  │ configuration: Size(411.4, 683.4) at 2.625x (in logical pixels)
I/flutter ( 6559):I/flutter ( 6559):  └─child: RenderCustomPaint
I/flutter ( 6559):    │ creator: CustomPaintBannerCheckedModeBannerI/flutter ( 6559):WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)]I/flutter ( 6559):ThemeAnimatedThemeScrollConfigurationMaterialAppI/flutter ( 6559):[root]
I/flutter ( 6559):    │ parentData: <none>
I/flutter ( 6559):    │ constraints: BoxConstraints(w=411.4, h=683.4)
I/flutter ( 6559):    │ size: Size(411.4, 683.4)
... # 省略

Dies ist die Ausgabe der Funktion des RenderObjectStammobjekts toStringDeep.

sizeBeim Debuggen von Layoutproblemen sollten Sie vor allem auf Summenfelder achten constraints. Einschränkungen werden im Baum nach unten und Dimensionen nach oben weitergegeben.

Wenn wir unsere eigenen Renderobjekte schreiben, können wir debugFillProperties()durch Überschreiben Informationen zum Dump hinzufügen. Verwenden Sie DiagnosticsPropertydas Objekt als Parameter der Methode und rufen Sie die Methode der Oberklasse auf.

3. Debuggen des Ebenenbaums debugDumpLayerTree()

Der Renderbaum kann geschichtet sein, und die endgültige Zeichnung muss verschiedene Ebenen kombinieren, aber Layeres ist die Ebene, die beim Zeichnen synthetisiert werden muss. Wenn wir versuchen, das Syntheseproblem zu debuggen, können wir es verwenden debugDumpLayerTree(). Für das obige Beispiel würde es Folgendes ausgeben:

I/flutter : TransformLayer
I/flutter :  │ creator: [root]
I/flutter :  │ offset: Offset(0.0, 0.0)
I/flutter :  │ transform:
I/flutter :[0] 3.5,0.0,0.0,0.0
I/flutter :[1] 0.0,3.5,0.0,0.0
I/flutter :[2] 0.0,0.0,1.0,0.0
I/flutter :[3] 0.0,0.0,0.0,1.0
I/flutter :I/flutter :  ├─child 1: OffsetLayer
I/flutter :  │ │ creator: RepaintBoundary ← _FocusScope ← SemanticsFocus-[GlobalObjectKey MaterialPageRoute(560156430)] ← _ModalScope-[GlobalKey 328026813] ← _OverlayEntry-[GlobalKey 388965355]StackOverlay-[GlobalKey 625702218]Navigator-[GlobalObjectKey _MaterialAppState(859106034)]Title ← ⋯
I/flutter :  │ │ offset: Offset(0.0, 0.0)
I/flutter :  │ │
I/flutter :  │ └─child 1: PictureLayer
I/flutter :I/flutter :  └─child 2: PictureLayer

Dies ist die Ausgabe Layervon root toStringDeep.

Die Transformation an der Wurzel ist eine Transformation, die das Gerätepixelverhältnis anwendet; in diesem Fall repräsentiert jedes logische Pixel 3,5 Gerätepixel.

RepaintBoundaryDas Widget erstellt eine Ebene im Renderbaum RenderRepaintBoundary. Dies wird verwendet, um die Anzahl der erforderlichen Neuanstriche zu reduzieren.

4. Semantischer Baum-Debugging debugDumpSemanticsTree()

Wir können auch aufrufen debugDumpSemanticsTree(), um einen Dump des semantischen Baums (des Baums, der der System-Barrierefreiheits-API präsentiert wird) abzurufen. Um diese Funktion nutzen zu können, müssen Sie zunächst Barrierefreiheitsfunktionen aktivieren, z. B. Systembarrierefreiheit aktivieren oder SemanticsDebugger.

Für das obige Beispiel würde es Folgendes ausgeben:

I/flutter : SemanticsNode(0; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :SemanticsNode(1; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :  │ └SemanticsNode(2; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4); canBeTapped)
I/flutter :SemanticsNode(3; Rect.fromLTRB(0.0, 0.0, 411.4, 683.4))
I/flutter :SemanticsNode(4; Rect.fromLTRB(0.0, 0.0, 82.0, 36.0); canBeTapped; "Dump App")

5. Terminplanung

Um herauszufinden, wo relativ zum Start-/Ende des Frames Ereignisse auftreten, können Sie den booleschen Wert debugPrintBeginFrameBannerund umschalten debugPrintEndFrameBanner, um den Start und das Ende des Frames auf der Konsole auszugeben.

Zum Beispiel:

I/flutter : ▄▄▄▄▄▄▄▄ Frame 12         30s 437.086ms ▄▄▄▄▄▄▄▄
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : Debug print: Am I performing this work more than once per frame?
I/flutter : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

debugPrintScheduleFrameStacksKann auch zum Drucken des Aufrufstapels verwendet werden, der die Aussendung des aktuellen Frames verursacht hat.

6. Visuelles Debuggen

Wir können Layoutprobleme auch visuell beheben debugPaintSizeEnabled, indem wir auf festlegen. trueDies ist renderingein boolescher Wert aus der Bibliothek. Es kann jederzeit aktiviert werden und wirkt sich am trueEnde . Die einfachste Möglichkeit zum Einrichten finden Sie void main()oben im .

Wenn es aktiviert ist, erhalten alle Kästchen einen hellgrünen Rand, padding( widgetz. B. von Padding) werden als hellblau angezeigt, untergeordnete widgetElemente werden von einem dunkelblauen Kästchen umgeben und Ausrichtungen (von widgetz. B. Centerund Align) werden als gelbe Pfeile angezeigt. Leer (falls nicht vorhanden). alle untergeordneten Knoten Container) ist ausgegraut.

debugPaintBaselinesEnabledmacht etwas Ähnliches, aber bei Objekten mit Grundlinien wird die wörtliche Grundlinie in Grün und die ideografische ( ideographic) Grundlinie in Orange angezeigt.

debugPaintPointersEnabledDie Flagge aktiviert einen speziellen Modus, in dem jedes angeklickte Objekt blaugrün hervorgehoben wird. Dies kann uns helfen festzustellen, ob ein Objekt auf eine falsche Art und Weise getestet wird hit(Flutter erkennt Klicks, bei denen es etwas gibt, das auf Benutzeraktionen reagiert widget), z. B. wenn es sich tatsächlich außerhalb des Gültigkeitsbereichs seines übergeordneten Objekts befindet, wird dies zunächst nicht der Fall sein Ort Erwägen Sie, hitden Test zu bestehen.

Wenn wir versuchen, zusammengesetzte Ebenen zu debuggen, um beispielsweise festzustellen, ob und wo RepaintBoundaryWidgets hinzugefügt werden, können wir debugPaintLayerBordersEnabledFlags verwenden, die die Grenzen jeder Ebene in Orange oder Umrissen markieren, oder debugRepaintRainbowEnabledFlags, die bewirken, dass die Ebene überlagert wird eine Reihe gedrehter Farben.

Alle diese Flags funktionieren nur im Debug-Modus. debug...Im Allgemeinen funktioniert alles im Flutter-Framework, das mit „“ beginnt, nur im Debug-Modus.

7. Debug-Animation

Der einfachste Weg, Animationen zu debuggen, besteht darin, sie zu verlangsamen. Setzen Sie dazu timeDilationeine Variable (in schedulerder Bibliothek) auf 1.0eine Zahl größer als, z. B. 50.0. Am besten legen Sie es nur einmal beim Anwendungsstart fest. Wenn wir es im laufenden Betrieb ändern, insbesondere wenn wir es während der Animation verkleinern, kann es beim Beobachten zu Regressionen kommen, die dazu führen können, dass Behauptungen treffen, und oft unsere Entwicklungsbemühungen beeinträchtigen.

8. Beheben Sie Leistungsprobleme

Um zu verstehen, warum unsere Anwendung ein Layout oder eine Neulackierung verursacht, können wir die Flags debugPrintMarkNeedsLayoutStacksund setzen. debugPrintMarkNeedsPaintStacksDiese protokollieren einen Stack-Trace in der Konsole, wenn die Renderbox zum Layouten und Neuzeichnen aufgefordert wird. Wenn dieser Ansatz für uns funktioniert, können wir Methoden servicesin der Bibliothek verwenden, um Stack-Traces bei Bedarf zu drucken.debugPrintStack()

9. Startzeit der Statistikanwendung

Um detaillierte Informationen darüber zu erhalten, wie lange es dauert, eine Flutter-App zu starten, können Sie zur Laufzeit die Optionen und flutter runverwenden .trace-startupprofile

$ flutter run --trace-startup --profile

start_up_info.jsonDie Trace-Ausgabe wird als im Flutter-Projektverzeichnis unter dem Verzeichnis gespeichert build. Die Ausgabe listet die verstrichene Zeit vom Anwendungsstart bis zu diesen Trace-Ereignissen auf (in Mikrosekunden erfasst):

  • Beim Einstieg in die Flutter-Engine.
  • Wenn der erste Frame der Anwendung angezeigt wird.
  • Bei der Initialisierung des Flutter-Frameworks.
  • Wenn die Initialisierung des Flutter-Frameworks abgeschlossen ist.

wie :

{
    
    
  "engineEnterTimestampMicros": 96025565262,
  "timeToFirstFrameMicros": 2171978,
  "timeToFrameworkInitMicros": 514585,
  "timeAfterFrameworkInitMicros": 1657393
}

10. Verfolgen Sie die Leistung des Dart-Codes

Um benutzerdefinierte Leistungsverfolgungen durchzuführen und das Timing Dartbeliebiger Codesegmente zu messen wall/CPU(ähnlich Androiddenen, die bei verwendet werden systrace). Verwenden Sie das dart:developerTool Timeline, um den Codeblock einzubinden, den Sie testen möchten, zum Beispiel:

Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();

Öffnen Sie dann die Seite Ihrer Anwendung Observatory timeline, Recorded Streamsaktivieren Sie das DartKontrollkästchen „“ und führen Sie die Funktion aus, die Sie messen möchten.

Durch das Aktualisieren der Seite werden die chronologischen timelineAufzeichnungen der App im Tracking-Tool von Chrome angezeigt.

Bitte stellen Sie sicher, dass Sie flutter runmit --profileFlags arbeiten, um eine minimale Abweichung der Laufzeitleistungsmerkmale von unserem Endprodukt sicherzustellen.

Verwendung von Dart DevTools

Tools->Flutter->Open Dart DevTools wird zum ersten Mal automatisch installiert. Nachdem Sie zum Ausführen geklickt haben , wird debugin der Konsole ein kleines blaues Symbol von Open DevTools angezeigt. Durch Klicken wird Dart DevTools im Browser geöffnet .

  • Klicken Sie nach der Auswahl select widget modeauf das Steuerelement auf dem Telefon, um die Debugging-Seite des ausgewählten Steuerelements aufzurufen
  • Wenn ein Steuerelement ausgewählt und angeklickt wird layout explorer, kann es debuggt werden
  • Wenn es sich um flexein Layout wie etc. handelt, ändert die mobile Schnittstelle Row Columnin der Ausrichtung der anklickbaren Hauptachse und Querachse den Effekt in Echtzeit entsprechend dem ausgewählten Ergebnislayout explorer
  • Wenn ein Fehler gemeldet wird, kann es sein, dass die Widget-Komponente nicht angegeben ist textDirection: TextDirection.ltr.

main.dartWie führt Flutter andere dartDateien (einschließlich mainFunktionen) in Android Studio aus? Führen Sie den folgenden Befehl aus:

flutter run lib/animated_list.dart

Flutter führt Verknüpfungsbefehle aus:

  • r : Hot-Reload.
  • R : Warmstart.
  • h : Wiederholen Sie diese Hilfemeldung.
  • d : trennen (beendet „Flutter Run“, lässt die App aber weiter laufen).
  • c : Bildschirm löschen.
  • q : Beenden (beendet die Anwendung auf dem Gerät).

Im Allgemeinen wird am häufigsten die Hot-Loading-Eingabe verwendet:r

Flutter erstellt die App:

  • Im Terminal ausführen: flutter create xxxErstellen Sie schnell eine Flutter-Anwendungsvorlage

Oben im Flutter-Outline-Bedienfeld befindet sich eine Reihe von Schaltflächen, mit denen Sie das angeklickte Steuerelement schnell in einen gemeinsamen Layout-Container einbinden oder mit der rechten Maustaste auf das Steuerelement klicken können.

DevTools bietet viele sehr umfassende Funktionen. Weitere Informationen finden Sie auf der offiziellen Website: DevTools (klicken Sie auf die Kategorie auf der linken Seite, um mehr zu erfahren)

Vier Betriebsmodi von Flutter

Flutter verfügt über vier Betriebsmodi: Debug , Release , Profile und Test . Diese vier Modi sind während der Erstellung völlig unabhängig .

  • Debug : Der Debug-Modus kann sowohl auf der realen Maschine als auch auf dem Emulator ausgeführt werden: Alle Assertionen werden aktiviert, einschließlich Debugging-Informationen, Debugger-Hilfsmittel (z. B. Observables) und Diensterweiterungen. Optimiert für schnelle Entwicklungs-/Ausführungsschleifen, jedoch nicht für Ausführungsgeschwindigkeit, Binärgröße und Bereitstellung. Führen Sie den Befehl aus: flutter run, pass sky/tools/gn --androidoder sky/tools/gn --ioscome build. checkedManchmal auch „ Modus“ oder „ slowMuster“ genannt .

  • Release : Der Release-Modus kann nur auf der realen Maschine ausgeführt werden, nicht auf dem Emulator: Alle Assertionen und Debugging-Informationen werden deaktiviert, und alle Debugger-Tools werden deaktiviert. Optimiert für schnellen Start, schnelle Ausführung und reduzierte Paketgröße. Deaktivieren Sie alle Debugging-Hilfsmittel und Serviceerweiterungen. Dieser Modus ist für die Bereitstellung für Endbenutzer gedacht. Führen Sie den Befehl aus: flutter run --release, pass sky/tools/gn --android --runtime-mode=releaseoder sky/tools/gn --ios --runtime-mode=releasecome build.

  • Profil : Der Profilmodus kann nur auf der realen Maschine ausgeführt werden, nicht auf dem Emulator: Er ist im Grunde dasselbe wie der Release-Modus, mit der Ausnahme, dass Diensterweiterung und Ablaufverfolgung aktiviert sind und einige Dinge, die die Ablaufverfolgung zumindest unterstützen (z. B. Observables). können mit Prozessen Der Befehl flutter run --profile wird in diesem Modus über sky/tools/gn --android --runtime-mode=profile oder sky/tools/gn --ios --runtime-mode=profile``` build ausgeführt. Da der Simulator die reale Szene nicht darstellen kann, kann er nicht auf dem Simulator ausgeführt werden.

  • Test : Der Headless-Testmodus kann nur auf dem Desktop ausgeführt werden : Er ist im Grunde dasselbe wie der Debug- Modus, außer dass er Headless ist und Sie ihn auf dem Desktop ausführen können. Befehle flutter testwerden in diesem Modus ausgeführt, indem Sie sky/tools/gnkommen build.

In unserer tatsächlichen Entwicklung sollten die vier oben genannten Modi verwendet und in zwei Typen unterteilt werden: Der eine ist ein nicht optimierter Modus für Entwickler zum Debuggen, der andere ist ein optimierter Modus für die endgültige Verwendung durch Entwickler. Standardmäßig handelt es sich um den nicht optimierten Modus. Wenn Sie den optimierten Modus aktivieren möchten, fügen Sie beim Erstellen Parameter nach der Befehlszeile hinzu --unoptimized.

Beachten Sie, dass im releaseAndroid-Modus möglicherweise Berechtigungen manuell hinzugefügt werden müssen (andernfalls stellen Sie möglicherweise fest, dass Sie das Netzwerk nicht verwenden können).Androidmanifest.xmlINTERNET

Generierung von Flatter-Hotkeys/Shortcut-Codes

Android Studio kann die zustandsbehafteten und zustandslosen Komponenten von Flutter schnell generieren, indem es Hotkey-Eingabeschlüsselwörter in Einstellungen->Editor->Live-Vorlagen konfiguriert:

Fügen Sie hier eine Bildbeschreibung ein
Fügen Sie hier eine Bildbeschreibung ein

Auf diese Weise werden Vorlagen schnell generiert, wenn wir dartdie Datei eingeben , und Vorlagen werden schnell generiert, wenn wir eingeben .stlessStatelessWidgetstfulStatefulWidget

Jeder andere Codeausschnitt, den Sie nicht erneut eingeben möchten, kann auf die gleiche Weise erfolgen.

Wenn Sie faul sind und die Konfiguration einer anderen Person verwenden möchten, können Sie natürlich direkt unter Einstellungen->Plugins in Android Studio nach dem Flutter Snippets- Plugin suchen und es installieren:

Fügen Sie hier eine Bildbeschreibung ein

Nach Abschluss der Installation können Sie Einstellungen->Editor->Live-Vorlagen öffnen, um die Flutter- Gruppe zu finden und zu sehen, welche Tastenkombinationen sie hat. Darüber hinaus können Sie direkt die Beschreibung ihres offiziellen Dokuments anzeigen: Flutter Snippets

Benutzer, die Visual Studio Code verwenden, können auch im Plug-in-Markt nach ähnlichen Plug-ins suchen.

Erfassung von Flutter-Ausnahmen

Dart-Single-Thread-Modell

In Javaund Objective-C(im Folgenden als „OC“ bezeichnet) wird das Programm beendet, wenn eine Ausnahme auftritt und nicht abgefangen wird. Dies ist jedoch in Dartoder nicht der Fall ! Der Grund dafür hat etwas mit ihrem Betriebsmechanismus zu tun. Bei beiden handelt es sich um Multithread-Programmiersprachen. Wenn ein Thread eine Ausnahme auslöst und die Ausnahme nicht abgefangen wird, wird der gesamte Prozess beendet. Aber und nein, es handelt sich bei allen um Single-Threaded-Modelle und der Betriebsmechanismus ist sehr ähnlich (aber es gibt Unterschiede). Werfen wir einen Blick auf das allgemeine Funktionsprinzip anhand eines Bildes, das vom Dart-Beamten bereitgestellt wurde:JavaScriptJavaOCDartJavaScriptDart

Fügen Sie hier eine Bildbeschreibung ein

Dart wird mit einem Nachrichtenschleifenmechanismus in einem einzelnen Thread ausgeführt , der zwei Aufgabenwarteschlangen enthält, eine ist die Mikrotask-Warteschlange Mikrotask -Warteschlange “ und die andere wird als „ Ereigniswarteschlange “ bezeichnet . Aus der Abbildung ist ersichtlich, dass die Ausführungspriorität der Mikrotask-Warteschlange höher ist als die der Ereigniswarteschlange .

Lassen Sie uns nun den Dart-Thread-Ausführungsprozess vorstellen. Wie in der Abbildung oben gezeigt, wird main()nach Ausführung der Eingabefunktion der Nachrichtenschleifenmechanismus gestartet. Zunächst werden die Aufgaben in der Mikrotask-Warteschlange nacheinander in der Reihenfolge „ First In First Out“ ausgeführt. Nachdem die Ereignisaufgabe ausgeführt wurde, wird das Programm beendet. Während der Ausführung können jedoch auch neue Mikrotasks und Ereignisaufgaben eingefügt werden Die Ereignisaufgabe . In diesem Fall befindet sich der Ausführungsprozess des gesamten Threads immer in einer Schleife und wird nicht beendet. In Flutter ist der Ausführungsprozess des Hauptthreads genau so und wird nie beendet .

In Dart befinden sich alle externen Ereignisaufgaben wie E/A, Timer, Klicks und Zeichnungsereignisse usw. in der Ereigniswarteschlange . Mikrotasks kommen normalerweise aus Dart , und es gibt nur sehr wenige Mikrotasks. Der Grund dafür sind Mikrotasks Die Priorität der Aufgabenwarteschlange ist hoch . Wenn zu viele Mikroaufgaben vorhanden sind, ist die Gesamtausführungszeit länger und die Verzögerung der Aufgaben in der Ereigniswarteschlange ist länger . Es ist erwähnenswert, dass wir über die Methode eine Aufgabe in die Mikrotask- Warteschlange einfügen können.Future.microtask(…)

Wenn in der Ereignisschleife eine Ausnahme in einer Aufgabe auftritt und nicht abgefangen wird, wird das Programm nicht beendet, und das direkte Ergebnis ist, dass der nachfolgende Code der aktuellen Aufgabe nicht ausgeführt wird , d. h. die Ausnahme in a Die Aufgabe hat keinen Einfluss auf die Ausführung anderer Aufgaben .

Ausnahmeerfassung des Flutter-Frameworks

try/catch/finallyCodeblockausnahmen können in Dart abgefangen werden , ähnlich wie in anderen Programmiersprachen.

Das Flutter-Framework bietet uns die Ausnahmeerfassung in vielen Schlüsselmethoden. Hier ist ein Beispiel. Wenn unser Layout außerhalb der Grenzen oder Spezifikationen liegt, öffnet Flutter automatisch eine Fehlerschnittstelle. Dies liegt daran, dass Flutter buildbeim Ausführen der Methode die Ausnahmeerfassung hinzugefügt hat. Der endgültige Quellcode lautet wie folgt:


void performRebuild() {
    
    
 ...
  try {
    
    
    //执行build方法  
    built = build();
  } catch (e, stack) {
    
    
    // 有异常时则弹出错误提示  
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
  } 
  ...
}      

Es ist ersichtlich, dass die Standardverarbeitungsmethode von Flutter beim Auftreten einer Ausnahme darin besteht, eine Ausnahme zu platzieren ErrorWidget. Was sollten wir jedoch tun, wenn wir die Ausnahme selbst abfangen und an die Alarmplattform melden möchten? Gehen wir zur _debugReportException()Methode, um Folgendes zu sehen:

FlutterErrorDetails _debugReportException(
  String context,
  dynamic exception,
  StackTrace stack, {
    
    
  InformationCollector informationCollector
}) {
    
    
  //构建错误详情对象  
  final FlutterErrorDetails details = FlutterErrorDetails(
    exception: exception,
    stack: stack,
    library: 'widgets library',
    context: context,
    informationCollector: informationCollector,
  );
  //报告错误 
  FlutterError.reportError(details);
  return details;
}

Wir haben festgestellt, dass der Fehler FlutterError.reportErrorüber die Methode gemeldet wurde, und haben Folgendes weiterverfolgt:

static void reportError(FlutterErrorDetails details) {
    
    
  ...
  if (onError != null)
    onError(details); //调用了onError回调
}

Wir haben festgestellt onError, dass es sich um FlutterErroreine statische Eigenschaft mit einer Standardverarbeitungsmethode handelt dumpErrorToConsole. Dies ist hier klar. Wenn wir die Ausnahme selbst melden möchten, müssen wir nur einen benutzerdefinierten Rückruf für die Fehlerbehandlung bereitstellen, z. B.:

void main() {
    
    
  FlutterError.onError = (FlutterErrorDetails details) {
    
    
    reportError(details);
  };
 ...
}

Auf diese Weise können wir die Ausnahmen behandeln, die Flutter für uns abfängt.

Andere Ausnahmeerfassung und Protokollerfassung

In Flutter gibt es immer noch einige Ausnahmen, die Flutter für uns nicht abfängt, z. B. die Ausnahme beim Aufruf einer Nullobjektmethode und die Ausnahme in Future . In Dart gibt es zwei Arten von Ausnahmen: synchrone Ausnahmen und asynchrone Ausnahmen . Synchrone Ausnahmen können try/catchabgefangen werden, während asynchrone Ausnahmen problematischer sind. Beispielsweise kann der folgende Code keine zukünftigen Ausnahmen abfangen:

try{
    
    
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
    
    
    print(e)
}

In Dart gibt es eine Methode runZoned(...), die ein Ausführungsobjekt angeben kann Zone. ZoneStellt den Umfang einer Codeausführungsumgebung dar. Zum leichteren Verständnis können Leser Zonees mit einer Codeausführungs-Sandbox vergleichen. Verschiedene Sandboxen sind isoliert. Sandboxen können einige Codeverhaltensweisen erfassen, abfangen oder ändernZone . Beispielsweise können Protokolle im Verhalten von erfasst werden Ausgabe, Timer-Erstellung, Mikrotask-Planung und alle nicht behandelten AusnahmenZone können ebenfalls abgefangen werden . Schauen wir uns die Methodendefinition unten an :runZoned(...)

R runZoned<R>(R body(), {
    
    
    Map zoneValues, 
    ZoneSpecification zoneSpecification,
}) 
  • zoneValues: Die privaten Daten der Zone können zone[key]über die Instanz abgerufen werden, die als private Daten jeder „Sandbox“ verstanden werden kann.

  • zoneSpecification: Bei einigen Zonenkonfigurationen können Sie einige Codeverhalten anpassen, z. B. das Abfangen von Protokollausgaben und -fehlern usw.

Zum Beispiel:

runZoned(() => runApp(const MyApp()),
    zoneSpecification: ZoneSpecification(
      // 拦截print  
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    
    
        parent.print(zone, "Interceptor: $line");
      },
      // 拦截未处理的异步错误
      handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
          Object error, StackTrace stackTrace) {
    
    
        parent.print(zone, '${
      
      error.toString()} $stackTrace');
      },
    ),
  );

Auf diese Weise printwerden alle Verhaltensweisen der Ausgabeprotokolle der aufrufenden Methode in unserer APP abgefangen. Auf diese Weise können wir auch Protokolle in der Anwendung aufzeichnen, und wenn die Anwendung eine nicht abgefangene Ausnahme auslöst, werden die Ausnahmeinformationen und Protokolle einheitlich gemeldet .

Darüber hinaus haben wir auch nicht erfasste asynchrone Fehler abgefangen, sodass FlutterError.onErrorwir in Kombination mit dem oben Gesagten unsere Flutter-Anwendungsfehler erfassen und melden können!

Der endgültige Fehlerberichtscode lautet ungefähr wie folgt:

void collectLog(String line){
    
    
    ... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
    
    
    ... //上报错误和日志逻辑
}

FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
    
    
    ...// 构建错误信息
}

void main() {
    
    
  var onError = FlutterError.onError; // 先将 onerror 保存起来
  FlutterError.onError = (FlutterErrorDetails details) {
    
    
    onError?.call(details); // 调用默认的onError
    reportErrorAndLog(details); // 上报
  };
  runZoned(() => runApp(MyApp()),
	  zoneSpecification: ZoneSpecification(
	    // 拦截print
	    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    
    
	      collectLog(line);  // 收集日志
	      parent.print(zone, "Interceptor: $line");
	    },
	    // 拦截未处理的异步错误
	    handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone,
	                          Object error, StackTrace stackTrace) {
    
    
	      reportErrorAndLog(details);  // 上报
	      parent.print(zone, '${
      
      error.toString()} $stackTrace');
	    }, ),
  );
}

Analyse des Nachrichtenschleifenprinzips in Flutter

Wie bereits erwähnt, verfügt das Single-Thread-Modell von Dart über zwei Aufgabenwarteschlangen, die beide unter dem Nachrichtenschleifenmechanismus ausgeführt werden. Die Nachrichtenschleife (MessageLoop) ist der Eckpfeiler aller UI-Frameworks, ähnlich dem Looper-Mechanismus von Android , und der unterste Die Flutter-Schicht basiert ebenfalls auf Nachrichten. Schleifengesteuert beginnt die detaillierte Analyse unten.

Analysieren Sie zunächst die Schlüsselklassen und ihre Beziehungen in der Nachrichtenschleife in Flutter, wie in der Abbildung dargestellt:

Fügen Sie hier eine Bildbeschreibung ein

In der Abbildung oben MessageLoopist die Klasse der Einstiegspunkt für Flutter zum Erstellen der Nachrichtenschleifenfunktion und TaskRunnerder Einstiegspunkt für Flutter zum Verwenden der Nachrichtenschleifenfunktion. Die meisten Thread-Aufgabenregistrierungen werden über TaskRunnerdie Methode PostTaskimplementiert .

  • MessageLoopImplImplementiert die allgemeine Logik der Nachrichtenschleife und hält und verwaltet die Nachrichtenwarteschlange (pass MessageLoopTaskQueues). Für die Android- Plattform MessageLoopAndroiderbt MessageLoopImplund stellt es die zugrunde liegende Implementierung der Nachrichtenschleife ALooperund timerfd_createden erstellten Dateideskriptor bereit .

  • MessageLoopTaskQueuesEs verwaltet die Nachrichtenwarteschlangen (auch als Aufgabenwarteschlangen bezeichnet) aller Threads , und die Nachrichtenschleife (Objekt) jedes Threads enthält eine Instanz, die zum Abfragen der Nachrichtenwarteschlange verwendet wird, die der aktuellen Nachrichtenschleife aus dem Feld entspricht. .MessageLoopImplTaskQueueIdMessageLoopTaskQueuesqueue_entries_TaskQueueEntry

  • TaskQueueEntryDas delayed_tasksFeld enthält eine DelayedTaskQueueInstanz DelayedTaskQueueeiner Prioritätswarteschlange ( std::priority_queue), die Nachrichten, die auf ihre Ausführung warten, nach Zeit und Priorität sortiert . Darüber hinaus enthält es einen Verweis auf die Nachrichtenschleife () TaskQueueEntryüber Wakeabledie Schnittstelle , die zur Ausführung der laufenden Aufgaben verwendet wird.MessageLoopImplDelayedTaskQueue

Das Obige ist die Gesamtstruktur der Nachrichtenschleife, und der Kern liegt im Aufrufpfad TaskRunnerzu TaskQueueEntry.

Die Nachrichtenschleife beginnt

Nachdem Flutter die Erstellung eines neuen Threads über die System-API abgeschlossen hat, startet es die Nachrichtenschleife basierend auf dem Thread und EnsureInitializedForCurrentThreadanalysiert dann den Start der Nachrichtenschleife basierend auf der Logik der Methode weiter, wie in Codeliste 10 gezeigt -1.

// 代码清单10-1 engine/fml/message_loop.cc
FML_THREAD_LOCAL ThreadLocalUniquePtr<MessageLoop> tls_message_loop; // 线程独有

void MessageLoop::EnsureInitializedForCurrentThread() {
    
    
  if (tls_message_loop.get() != nullptr) {
    
     return; } // 已经完成初始化
  tls_message_loop.reset(new MessageLoop()); // 初始化
} // 由于每个线程持有自己的MessageLoop,因此无须加锁

MessageLoop::MessageLoop()
    : loop_(MessageLoopImpl::Create()), // 创建消息循环实例,见代码清单10-2
      task_runner_(fml::MakeRefCounted<fml::TaskRunner>(loop_)) {
    
     } // 创建TaskRunner
      
MessageLoop& MessageLoop::GetCurrent() {
    
    
  auto* loop = tls_message_loop.get();
  return *loop;
}

Die obige Logik löst MessageLoopden Konstruktor, die Vervollständigung loop_und task_runner_die Initialisierung der Felder aus. Analysieren Sie zunächst MessageLoopImpl::Createdie Logik der Methode, wie in Listing 10-2 gezeigt.

// 代码清单10-2 engine/fml/message_loop_impl.cc
fml::RefPtr<MessageLoopImpl> MessageLoopImpl::Create() {
    
    
#if OS_MACOSX // 在编译期确定
  return fml::MakeRefCounted<MessageLoopDarwin>();
#elif OS_ANDROID
  return fml::MakeRefCounted<MessageLoopAndroid>();
// SKIP OS_FUCHSIA / OS_LINUX / OS_WIN
#else
  return nullptr;
#endif
}

Die obige Logik löst MessageLoopAndroidden Konstruktor aus und seine Logik wird in Listing 10-8 im Detail analysiert. Analysieren Sie zunächst den Konstruktor MessageLoopAndroidder übergeordneten Klasse MessageLoopImpl(beachten Sie, dass der Konstruktor der übergeordneten Klasse in C++ implizit ausgelöst wird), wie in Listing 10-3 gezeigt.

// 代码清单10-3 engine/fml/message_loop_impl.cc
MessageLoopImpl::MessageLoopImpl()
    : task_queue_(MessageLoopTaskQueues::GetInstance()), // 见代码清单10-4
      queue_id_(task_queue_->CreateTaskQueue()), // 见代码清单10-5
      terminated_(false) {
    
     // 当前消息循环是否停止
  task_queue_->SetWakeable(queue_id_, this); // 见代码清单10-5
}

Die obige Logik ist immer noch die Initialisierung von Klassenmitgliedsfeldern. Analysieren Sie zunächst task_queue_die Initialisierung des Feldes, wie in Listing 10-4 gezeigt.

// 代码清单10-4 engine/fml/message_loop_task_queues.cc
fml::RefPtr<MessageLoopTaskQueues> MessageLoopTaskQueues::instance_;
fml::RefPtr<MessageLoopTaskQueues> MessageLoopTaskQueues::GetInstance() {
    
    
  std::scoped_lock creation(creation_mutex_);
  if (!instance_) {
    
    
    instance_ = fml::MakeRefCounted<MessageLoopTaskQueues>();
  }
  return instance_;
}

Die obige Logik ist eine typische Singleton- Implementierung. Analysieren Sie als Nächstes queue_id_die Initialisierung der Felder, wie in Listing 10-5 gezeigt.

// 代码清单10-5 engine/fml/message_loop_task_queues.cc
TaskQueueId MessageLoopTaskQueues::CreateTaskQueue() {
    
    
  std::lock_guard guard(queue_mutex_);
  TaskQueueId loop_id = TaskQueueId(task_queue_id_counter_);
  ++task_queue_id_counter_; // TaskQueue的计数id
  queue_entries_[loop_id] = std::make_unique<TaskQueueEntry>();
  return loop_id;
}
void MessageLoopTaskQueues::SetWakeable(TaskQueueId queue_id,
                fml::Wakeable* wakeable) {
    
    
  std::lock_guard guard(queue_mutex_);
  queue_entries_.at(queue_id)->wakeable = wakeable;
}

In der obigen Logik wird davon ausgegangen, dass queue_entries_die von verschiedenen Threads erstellten Daten gespeichert werden TaskQueueId, sodass jede Verwendung gesperrt werden muss. SetWakeableBewirkt, dass task_queue_die Umkehrung MessageLoopImpldie Referenz der Instanz enthält, die in der nachfolgenden Logik verwendet wird.

In der obigen Logik TaskQueueIdgibt es TaskQueueEntryeine Eins-zu-eins-Entsprechung TaskQueueEntryund wakeableder Verweis auf die aktuelle Nachrichtenschleifeninstanz (eingestellt in Codelisting 10-3) wird beibehalten. Als nächstes analysieren Sie TaskQueueEntryden Konstruktor weiter, wie in Listing 10-6 gezeigt.

// 代码清单10-6 engine/fml/message_loop_task_queues.cc
const size_t TaskQueueId::kUnmerged = ULONG_MAX;
TaskQueueEntry::TaskQueueEntry()
    : owner_of(_kUnmerged), subsumed_by(_kUnmerged) {
    
    
  wakeable = NULL; // 消息循环的引用
  task_observers = TaskObservers();
  delayed_tasks = DelayedTaskQueue(); // 等待中的任务队列,详见10.1.2节
}

Die obige Logik dient hauptsächlich TaskQueueEntryder Initialisierung verwandter Felder, owner_ofund subsumed_bydie Felder werden mit den später eingeführten dynamischen Threads zusammengeführt. Unter normalen Umständen sind sie Konstanten, was _kUnmergeddarauf hinweist, dass die aktuelle Aufgabenwarteschlange nicht von anderen Aufgabenwarteschlangen gehalten wird und auch keine anderen Aufgabenwarteschlangen.

Fahren Sie als Nächstes mit der Analyse TaskQueueEntry(Value)der entsprechenden Klasse fort Key, TaskQueueIdwie in Codeauflistung 10-7 gezeigt.

// 代码清单10-7 engine/fml/message_loop_task_queues.h
class TaskQueueId {
    
    
 public:
  static const size_t kUnmerged; // ULONG_MAX
  explicit TaskQueueId(size_t value) : value_(value) {
    
    }
  operator int() const {
    
     return value_; }
 private:
  size_t value_ = kUnmerged; // 默认值
};

TaskQueueIdWie der Name schon sagt, handelt es sich um eine Aufgabenwarteschlange id. Gemäß der obigen Logik ist ihr Kern eine intArt Ganzzahl ( value_).

Das Obige ist MessageLoopImpldie vom Konstruktor ausgelöste Logik, bei der es sich um die Initialisierung der allgemeinen Logik im Zusammenhang mit der Nachrichtenschleife in Flutter handelt. Als nächstes starten verschiedene Plattformen die plattformbezogene Initialisierungslogik in der Nachrichtenschleife basierend auf ihren jeweiligen System-APIs und führen sie dann am Beispiel von Android ein, wie in Codeliste 10-8 gezeigt.

// 代码清单10-8 engine/fml/platform/android/message_loop_android.cc
MessageLoopAndroid::MessageLoopAndroid()
    : looper_(AcquireLooperForThread()), // 见代码清单10-9
      // timerfd_create函数创建一个定时器对象,同时返回一个与之关联的文件描述符
      timer_fd_(::timerfd_create(kClockType, TFD_NONBLOCK | TFD_CLOEXEC)), 
	  // 第1步,创建定时器对象
      running_(false) {
    
     // 判断当前消息循环是否正在运行,初始化时为false
  static const int kWakeEvents = ALOOPER_EVENT_INPUT; // 第2步,构造响应回调
  ALooper_callbackFunc read_event_fd = [](int, int events, void* data) -> int {
    
    
    if (events & kWakeEvents) {
    
     // 轮询到数据,触发本回调
      reinterpret_cast<MessageLoopAndroid*>(data)->OnEventFired(); // 见代码清单10-16
    }
    return 1;  // continue receiving callbacks
  }; // 第3步,为Looper添加一个用于轮询的文件描述符
  int add_result = ::ALooper_addFd(looper_.get(), // 目标Looper
            timer_fd_.get(), // 添加提供给Looper轮询的文件描述符
            ALOOPER_POLL_CALLBACK, // 表明轮询到数据时将触发回调
            kWakeEvents, // 用于唤醒Looper的事件类型
            read_event_fd, // 将被触发的回调
            this); // 回调的持有者的引用
  FML_CHECK(add_result == 1);
}

Die obige Logik ist hauptsächlich in 3Schritte unterteilt, und die relevanten Details sind im Code angegeben. In,

  • 1Die erste Methode timerfd_createist eine Linux- System -API , die einen Dateideskriptor für Looperdie anschließende Abfrage generiert . Der Wert des ersten Parameters timerfd_createder Methode bedeutet im Wesentlichen , dass die Zeitmessung ab dem Zeitpunkt des Systemstarts beginnt und nicht durch die Änderung der Systemzeit durch den Benutzer beeinflusst wird ; der erste Parameter umfasst und , beides um die normale Verwendung des Dateideskriptors im aktuellen Inhalt sicherzustellen. Dies zeigt insbesondere an, dass es sich derzeit im nicht blockierenden Modus befindet. Dies bedeutet, dass der aktuelle Dateideskriptor automatisch vom System geschlossen wird und nicht weiter übergeben wird, wenn das Programm die Funktion ausführt.1clockidkClockTypeCLOCK_MONOTONIC2flagsTFD_NONBLOCKTFD_CLOEXECTFD_NONBLOCKTFD_CLOEXECexec

  • Der erste 2Schritt besteht darin, einen RückrufLooper zu erstellen, der ausgelöst wird , wenn Daten im Zieldateideskriptor vorhanden sind . Weitere Informationen finden Sie in der Codeliste 10-16.

  • Der erste Schritt besteht darin, die Bindung mit dem Dateideskriptor 3über ALooper_addFddiesen Systemaufruf abzuschließen .Loopertimer_fd_

In der obigen Logik Looperbefindet sich die Initialisierungslogik in AcquireLooperForThreadder Methode, wie in Listing 10-9 gezeigt.

// 代码清单10-9 engine/fml/platform/android/message_loop_android.cc
static ALooper* AcquireLooperForThread() {
    
    
  ALooper* looper = ALooper_forThread(); // 返回与调用线程相关联的Looper
  if (looper == nullptr) {
    
     // 当前线程没有关联Looper
    looper = ALooper_prepare(0); // 初始化并返回一个与当前线程相关联的Looper
  }
  ALooper_acquire(looper);
  return looper;
}

Für Platformden Thread (Hauptthread) ALooper_forThreadkann er abgerufen werden Looper(dh die Nachrichtenschleife des Android-Hauptthreads), während UIThread, RasterThread und Thread durch Erstellen eines neuen Threads I/Oerstellt werden müssen .ALooper_prepareLooper

Nach Abschluss der obigen Logik kann es offiziell gestartet werden Looper, wie in Listing 10-10 gezeigt.

// 代码清单10-10 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::Run() {
    
    
  FML_DCHECK(looper_.get() == ALooper_forThread()); // 确保Looper一致
  running_ = true;
  while (running_) {
    
    
    int result = ::ALooper_pollOnce(-1, // 超时时间,-1表示无限轮询
                    nullptr, nullptr, nullptr);
    if (result == ALOOPER_POLL_TIMEOUT || // 异常情况
            result == ALOOPER_POLL_ERROR) {
    
    
      running_ = false;
    }
  } // while
}

In der obigen Logik handelt es ALooper_pollOncesich um einen Mechanismus , der nach dem Aufruf dieser Methode CPU-Ressourcen freigibt und auf Linuxdie Daten aus dem von Looper abgefragten Dateideskriptor wartet. Er blockiert weder wie eine herkömmliche synchrone Methode noch asynchron wie eine herkömmliche Methode .Methode direkt in eine Endlosschleife einfügen .pipe/epollwhile

Analysieren Sie als Nächstes, wie Sie Aufgaben registrieren oder an die Nachrichtenschleife senden.

Aufgabenregistrierung

Die Logik der bereits mehrfach aufgetauchten Methode PostTaskist in Listing 10-11 dargestellt.

// 代码清单10-11 engine/fml/task_runner.cc
void TaskRunner::PostTask(const fml::closure& task) {
    
    
  loop_->PostTask(task, fml::TimePoint::Now()); // 立即执行
}
void TaskRunner::PostTaskForTime(const fml::closure& task, fml::TimePoint target_time)
{
    
    
  loop_->PostTask(task, target_time); // 指定目标时间
}
void TaskRunner::PostDelayedTask(const fml::closure& task, fml::TimeDelta delay) {
    
    
  loop_->PostTask(task, fml::TimePoint::Now() + delay); // 指定时间间隔
}

loop_In der obigen Logik wird unabhängig von der Methode die Methode des Felds schließlich aufgerufen PostTask, wie in der Codeliste 10-12 gezeigt.

// 代码清单10-12 engine/fml/message_loop_impl.cc
void MessageLoopImpl::PostTask(const fml::closure& task, // 目标任务
            fml::TimePoint target_time) {
    
     // 目标执行时间
  if (terminated_) {
    
     return; } // 消息循环已经停止
  task_queue_->RegisterTask(queue_id_, task, target_time); // 见代码清单10-13
}

Die obige Logik dient hauptsächlich dazu, RegisterTaskAufgaben in der Aufgabenwarteschlange zu registrieren, die der Nachrichtenschleife des aktuellen Threads entspricht, und die Weckzeit über Methoden festzulegen, wie in Codeliste 10-13 gezeigt.

// 代码清单10-13 engine/fml/message_loop_task_queues.cc
void MessageLoopTaskQueues::RegisterTask(TaskQueueId queue_id, // 目标任务队列
    const fml::closure& task, fml::TimePoint target_time) {
    
     // 任务和触发时间
  std::lock_guard guard(queue_mutex_);
  size_t order = order_++;
  const auto& queue_entry = queue_entries_.at(queue_id);
  queue_entry->delayed_tasks.push({
    
    order, task, target_time}); // 加入任务队列
  TaskQueueId loop_to_wake = queue_id;
  if (queue_entry->subsumed_by != _kUnmerged) {
    
     // 详见10.2节
    loop_to_wake = queue_entry->subsumed_by;
  }
  WakeUpUnlocked(loop_to_wake, GetNextWakeTimeUnlocked(loop_to_wake));
}
void MessageLoopTaskQueues::WakeUpUnlocked(TaskQueueId queue_id,
    fml::TimePoint time) const {
    
    
  if (queue_entries_.at(queue_id)->wakeable) {
    
     // 存在对应的消息循环实现
    queue_entries_.at(queue_id)->wakeable->WakeUp(time); // 设置唤醒时间
  }
}

Die obige Logik schließt zunächst delayed_tasksdie Aufgabenregistrierung über das Feld ab und WakeUpweist dann die Nachrichtenschleife an, die Aufgabenausführung zum angegebenen Zeitpunkt über die Methode auszulösen, wie in der Codeliste 10-14 gezeigt.

// 代码清单10-14 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::WakeUp(fml::TimePoint time_point) {
    
    
  bool result = TimerRearm(timer_fd_.get(), time_point); // 见代码清单10-15
}

Die obige Logik implementiert die Timer-Logik auf der Hardwareebene über den in Codeauflistung 10-8 erstellten Dateideskriptor, wie in Codeauflistung 10-15 gezeigt.

// 代码清单10-15 engine/fml/platform/linux/timerfd.cc
bool TimerRearm(int fd, fml::TimePoint time_point) {
    
    
  uint64_t nano_secs = time_point.ToEpochDelta().ToNanoseconds(); // 转换为纳秒
  if (nano_secs < 1) {
    
     nano_secs = 1; }
  struct itimerspec spec = {
    
    }; 
// it_value是首次超时时间,it_interval是后续周期性超时时间
  spec.it_value.tv_sec = (time_t)(nano_secs / NSEC_PER_SEC); // 超过的部分转换为秒
  spec.it_value.tv_nsec = nano_secs % NSEC_PER_SEC; // 小于1s的部分仍用纳秒表示
  spec.it_interval = spec.it_value;
  int result = ::timerfd_settime(              // 系统调用
                           fd,                 // 目标文件描述符,即代码清单10-8中的timer_fd_ 
                           TFD_TIMER_ABSTIME,  // 绝对定时器
                           &spec,              // 超时时间设置
                           nullptr);
  return result == 0;
}

Die obige Logik verwendet hauptsächlich die Systemaufrufmethode, timerfd_settimeum die Einstellung des Timers abzuschließen, daher werde ich sie hier nicht wiederholen.

Aufgabenausführung

Wenn die angegebene Zeit erreicht ist, timer_fd_werden eine Abfrage und der Rückruf in Listing 10-8 ausgelöst, OnEventFiredund die Logik ist in Listing 10-16 dargestellt.

// 代码清单10-16 engine/fml/platform/android/message_loop_android.cc
void MessageLoopAndroid::OnEventFired() {
    
    
  if (TimerDrain(timer_fd_.get())) {
    
     // 见代码清单10-17
    RunExpiredTasksNow(); // 父类MessageLoopImpl的方法
  }
}
// engine/fml/message_loop_impl.cc
void MessageLoopImpl::RunExpiredTasksNow() {
    
    
  FlushTasks(FlushType::kAll); // 见代码清单10-18
}

Die obige Logik ruft zunächst TimerDraindie zu prüfende Methode auf, wie in Listing 10-17 gezeigt.

// 代码清单10-17 engine/fml/platform/linux/timerfd.cc
bool TimerDrain(int fd) {
    
    
  uint64_t fire_count = 0;
  ssize_t size = FML_HANDLE_EINTR(::read(fd, &fire_count, sizeof(uint64_t)));
  if (size != sizeof(uint64_t)) {
    
    
    return false;
  }
  return fire_count > 0;
}

Nachdem die Prüfung bestanden wurde FlushTasks, wird die Methode ausgelöst, um die in Listing 10-13 registrierten Aufgaben zu verarbeiten. Die spezifische Logik ist in Listing 10-18 dargestellt.

// 代码清单10-18 engine/fml/message_loop_impl.cc
void MessageLoopImpl::FlushTasks(FlushType type) {
    
    
  TRACE_EVENT0("fml", "MessageLoop::FlushTasks");
  const auto now = fml::TimePoint::Now();
  fml::closure invocation;
  do {
    
    
    invocation = task_queue_->GetNextTaskToRun(queue_id_, now); // 见代码清单10-28
    if (!invocation) {
    
     break; } // 如果是非法任务,直接退出
    invocation(); // 执行任务,即代码清单10-11中传入的task参数
    std::vector<fml::closure> observers = task_queue_->GetObserversToNotify(queue_id_);
    for (const auto& observer : observers) {
    
    
      observer(); // 通知已注册当前任务队列监听的观察者
    }
    if (type == FlushType::kSingle) {
    
     break; } // 只执行一个任务
  } while (invocation);
}

Die obige Logik besteht hauptsächlich task_queue_darin, eine Aufgabe aus dem Aufgabenwarteschlangenfeld der aktuellen Nachrichtenschleife zu entnehmen, sie auszuführen und die registrierten Beobachter zu benachrichtigen. GetNextTaskToRunDie Logik der Methode wird in Abschnitt 10.2 ausführlich vorgestellt.

Bisher wurde die Analyse der Nachrichtenschleife der Flutter Engine und des zugrunde liegenden Mechanismus abgeschlossen.

Zusammenfassen:

Auf der Flutter-Seite MessageLoopImplwird die allgemeine Logik der Nachrichtenschleife sowie das Halten und Verwalten der Nachrichtenwarteschlange implementiert, und auf der Seite der Android-Plattform MessageLoopAndroidwird die Implementierung der Nachrichtenschleife auf der Android-Plattform verkörpert.

  • Für Android ist die unterste Ebene der Nachrichtenschleife untrennbar mit dem Linux- epollMechanismus verbunden. Dieser Mechanismus ist der Kern, um sicherzustellen, dass die Aufgabenwarteschlange in der System-UI-Architektur die CPU nicht belegt und jederzeit aktiviert werden kann.
  • Natürlich müssen die Aufgaben in der Nachrichtenwarteschlange nach Priorität oder verzögerter Ausführung in die Warteschlange gestellt werden, und für jede registrierte RegisterTaskAufgabe ist eine Weckzeit festgelegt. Der Schlüssel hier ist die Anordnung der Ausführungszeit. Da Android auf dem Linux-Kernel basiert , die Lösung ist natürlich Es ist untrennbar mit dem Dateideskriptor von Linux verbunden (alles in Linux ist eine Datei). Der Hauptpunkt besteht darin, timer_fd_diesen Dateideskriptor zu verwenden, um die Hardware-Timing-Zeit durch Systemaufrufe festzulegen. Wenn die Timing-Zeit abgelaufen ist, wird ausgelöst den Rückruf und führen Sie die Aufgabe aus.

Dynamische Thread-Merging-Technologie

Obwohl es sich bei der dynamischen Thread-Merging-Technologie in Flutter Engine selbst um eine Reihe unabhängiger Logik handelt, ist sie in vielen anderen Logiken verstreut. Wenn sie nicht separat analysiert wird, ist sie aufgrund dieser seltsamen und abrupten Logik beim Lesen anderer Codes schwer zu verstehen. gründlich. Beispielsweise GetNextTaskToRunkann die Methode in Listing 10-18, wenn es keine dynamische Thread-Zusammenführung gibt, einfach die Aufgabe mit der höchsten Priorität verarbeiten, task_queue_nämlich die von der aktuellen Nachrichtenschleife gehaltenen Felder abzurufen, aber aufgrund der Existenz einer dynamischen Thread-Zusammenführung ihre Logik ändern wird um ein Vielfaches komplizierter sein.

Der Hauptzweck der dynamischen Thread-Zusammenführung besteht darin , im selben Frame Flutter UIwie das OriginalView rendern zu können (da die Rendering-Ausgabe Platformim Thread wird ). In Kombination mit dem Nachrichtenschleifenmechanismus kann man es erraten: FlutterImageViewdie sogenannte dynamische Thread-Zusammenführung bedeutet nicht, Threads und Threads auf Systemebene zusammenzuführen, sondern die Nachrichtenschleife des Threads übernehmen zu lassen und die Aufgabenwarteschlange zu verarbeiten, die von der Nachrichtenschleife des ursprünglichen Threads gehalten wird.CanvasFlutter UIPlatformRasterPlatformRaster

Darüber hinaus bringt das dynamische Zusammenführen von Threads auch einige praktische Probleme mit sich, z. B. wann das Zusammenführen von Threads gestartet und wann das Zusammenführen von Threads deaktiviert werden soll, da nach dem Verschwinden das Zusammenführen Platform Viewdynamischer Threads natürlich nicht mehr erforderlich ist Die ursprüngliche normale Verarbeitungsbeziehung sollte wiederhergestellt werden.

Zusammenführen, pflegen und auflösen

Analysieren Sie zunächst die Zusammenführungslogik von Threads (genauer gesagt Nachrichtenschleifen, siehe unten), die die Grundlage für die nachfolgende Analyse bilden. Aufgrund Platform Viewder Existenz von MergeWithLeasewird die Methode ausgelöst und ihre Logik ist in Listing 10-19 dargestellt.

// 代码清单10-19 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::MergeWithLease(size_t lease_term) {
    
     // 合并处于维持状态的帧数
  std::scoped_lock lock(lease_term_mutex_);
  if (TaskQueuesAreSame()) {
    
     return; } // 见代码清单10-20
  if (!IsEnabledUnSafe()) {
    
     return; } // 见代码清单10-20
  FML_DCHECK(lease_term > 0) << "lease_term should be positive.";
  if (IsMergedUnSafe()) {
    
     // 见代码清单10-20
    merged_condition_.notify_one();
    return;
  } // 检查工作完成,开始合并,见代码清单10-21
  bool success = task_queues_->Merge(platform_queue_id_, gpu_queue_id_);
  if (success && merge_unmerge_callback_ != nullptr) {
    
    
    merge_unmerge_callback_(); // 通知
  }
  FML_CHECK(success) << "Unable to merge the raster and platform threads.";
  lease_term_ = lease_term; // 线程合并处于维持状态的帧数,默认为10帧
  // 唤醒某个等待(Wait)的线程,如果当前没有等待线程,则该函数什么也不做
  merged_condition_.notify_one(); // For WaitUntilMerged方法
}

Die obige Logik prüft zunächst, ob die dynamische Thread-Zusammenführung gestartet werden muss. Die entsprechende Logik ist in Listing 10-20 dargestellt. Nach Abschluss der Inspektion wird die Zusammenführung offiziell gestartet und lease_term_das Feld aktualisiert. Dieses Feld wird verwendet, um zu beurteilen, ob der Thread-Zusammenführungsstatus beibehalten werden muss. Die spezifische Rolle wird später analysiert.

// 代码清单10-20 engine/fml/raster_thread_merger.cc
bool RasterThreadMerger::IsEnabledUnSafe() const {
    
    
  return enabled_; // 检查是否允许动态线程合并
}
bool RasterThreadMerger::IsMergedUnSafe() const {
    
    
  return lease_term_ > 0 || TaskQueuesAreSame(); // 检查是否已处于合并状态
}
bool RasterThreadMerger::TaskQueuesAreSame() const {
    
    
  return platform_queue_id_ == gpu_queue_id_; // 检查两个任务队列本身是否相同
}

Die spezifische Logik der dynamischen Thread-Zusammenführung wird im Folgenden analysiert, wie in Codeliste 10-21 dargestellt.

// 代码清单10-21 engine/fml/message_loop_task_queues.cc
// owner: 合并后任务队列的所有者通常为Platform线程
// subsumed: 被合并的任务队列通常为Raster线程
bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
    
    
  if (owner == subsumed) {
    
     return true; } // 合并自身,异常参数
  std::lock_guard guard(queue_mutex_);
  auto& owner_entry = queue_entries_.at(owner); // 合并方的任务队列入口
  auto& subsumed_entry = queue_entries_.at(subsumed); // 被合并方的任务队列入口
  if (owner_entry->owner_of == subsumed) {
    
    
    return true; // 合并方的任务队列已经是被合并方的持有者(owner_of)
  } // 下面开始真正合并
  std::vector<TaskQueueId> owner_subsumed_keys = {
    
    
      // 检查合并方当前是否持有任务队列或被其他任务队列持有
      owner_entry->owner_of, owner_entry->subsumed_by,
      // 检查被合并方当前是否持有任务队列或被其他任务队列持有
      subsumed_entry->owner_of,subsumed_entry->subsumed_by};
  for (auto key : owner_subsumed_keys) {
    
    
    if (key != _kUnmerged) {
    
     return false; } // 通过检查以上4个关键字段是否为_kUnmerged
  } // 判断owner和subsumed对应的任务队列当前是否处于动态线程合并状态,若已处于则返回
  owner_entry->owner_of = subsumed; // 标记owner_entry持有被合并方(subsumed)
  subsumed_entry->subsumed_by = owner; // 标记被合并方被owner持有
  if (HasPendingTasksUnlocked(owner)) {
    
     // 如果有未处理的任务,则见代码清单10-22
    WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner)); // 见代码清单10-13
  }
  return true;
}

Da die native Benutzeroberfläche in Android vom Hauptthread gerendert werden muss, ownerist die obige Logik Platformdie Aufgabenwarteschlange des Threads, subsumeddie Rasterdie Aufgabenwarteschlange des Threads darstellt. Die obige Logik dient hauptsächlich dazu, das Feld ownerder Aufgabenwarteschlange owner_ofauf Rasterdie Aufgabenwarteschlange des Threads und das Feld subsumedder Aufgabenwarteschlange subsumed_byauf Platformdie Aufgabenwarteschlange des Threads festzulegen. Auf diese Weise wird ownerbei der Verarbeitung der Aufgabenwarteschlange die owner_ofdem Aufruf entsprechende Aufgabenwarteschlange gemeinsam verarbeitet. Wenn subsumed_bydas Feld einer Aufgabenwarteschlange nicht vorhanden ist _kUnmerged, bedeutet dies, dass sie von anderen Aufgabenwarteschlangen gemeinsam verarbeitet wird. Beenden Sie sie daher einfach direkt Dieser Teil wird später detailliert analysiert.

Die obige Logik prüft nach Abschluss der Zusammenführung, ob über die Methode HasPendingTasksUnlockedunverarbeitete Aufgaben vorhanden sind , wie in der Codeliste 10-22 gezeigt.

// 代码清单10-22 engine/fml/message_loop_task_queues.cc
bool MessageLoopTaskQueues::HasPendingTasksUnlocked(TaskQueueId queue_id) const {
    
    
  const auto& entry = queue_entries_.at(queue_id);
  bool is_subsumed = entry->subsumed_by != _kUnmerged;
  if (is_subsumed) {
    
     
    return false; // 当前任务队列已被合并进其他任务队列,无须在此处理
  }
  if (!entry->delayed_tasks.empty()) {
    
    
    return true; // 当前任务队列存在待处理任务
  } // 当前任务队列不存在待处理任务,开始检查是否有被当前消息循环合并的任务队列
  const TaskQueueId subsumed = entry->owner_of;
  if (subsumed == _kUnmerged) {
    
    
    return false; // 如果不存在被合并的任务队列,则认为确实不存在排队任务
  } else {
    
     // 根据被合并的任务队列是否有排队任务返回结果
    return !queue_entries_.at(subsumed)->delayed_tasks.empty();
  }
}

ownerNachdem man die Bedeutung von und verstanden hat subsumed, wird die obige Logik sehr klar. Analysieren Sie als Nächstes die Aufrechterhaltung des Status der dynamischen Thread-Zusammenführung. Wenn es sich in Codeauflistung 9-42 bereits im Zustand der Thread-Zusammenführung befindet und gerade den enthaltenen Frame zeichnet Platform View, wird die Methode aufgerufen ExtendLeaseTo, um die Dauer der dynamischen Thread-Zusammenführung zu verlängern, wie in Codeauflistung 10-23 gezeigt.

// 代码清单10-23 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::ExtendLeaseTo(size_t lease_term) {
    
     // 动态线程合并维持的帧数
  if (TaskQueuesAreSame()) {
    
     return; }
  std::scoped_lock lock(lease_term_mutex_);
  FML_DCHECK(IsMergedUnSafe()) << "lease_term should be positive.";
  if (lease_term_ != kLeaseNotSet && // 不要延长一个未设置的值
      static_cast<int>(lease_term) > lease_term_) {
    
     // 最大不超过原来的值
    lease_term_ = lease_term;
  }
}

In der obigen Logik ist der Wert ExtendLeaseTodes eingehenden Parameters der Methode lease_termim Allgemeinen 10, wie aus der if-Logik ersichtlich ist, wenn Platform Viewer gerendert wurde, lease_term_wird er immer aktualisiert 10, anstatt jedes Mal zu akkumulieren 10, das heißt, jedes Mal Wenn die Methode aufgerufen wird, führt der dynamische Thread den Status zusammen, um den Rahmen weiterhin beizubehalten lease_term. Die Aufrechterhaltung des dynamischen Thread-Zusammenführungsstatus besteht im Wesentlichen aus lease_term_der Aktualisierung von Feldern. lease_term_Analysieren Sie als Nächstes die Auflösung des dynamischen Thread-Zusammenführungsstatus und die Rolle von Feldern in diesem Prozess .

In Listing 5-100 wird beim Rendern eines Frames DecrementLeasedie Methode ausgelöst, wie in Listing 10-24 gezeigt.

// 代码清单10-24 engine/fml/raster_thread_merger.cc
RasterThreadStatus RasterThreadMerger::DecrementLease() {
    
    
  if (TaskQueuesAreSame()) {
    
     // 见代码清单10-20
    return RasterThreadStatus::kRemainsMerged;
  }
  std::unique_lock<std::mutex> lock(lease_term_mutex_);
  if (!IsMergedUnSafe()) {
    
     // 已经解除合并
    return RasterThreadStatus::kRemainsUnmerged;
  }
  if (!IsEnabledUnSafe()) {
    
     // 不允许执行相关操作
    return RasterThreadStatus::kRemainsMerged;
  } // 调用本方法时lease_term_必须大于0,即线程处于合并状态
  FML_DCHECK(lease_term_ > 0) 
      << "lease_term should always be positive when merged.";
  lease_term_--; // -1,为0时表示动态线程合并状态结束
  if (lease_term_ == 0) {
    
    
    lock.unlock();
    UnMergeNow(); // 开始消解两个任务队列的关系,见代码清单10-25
    return RasterThreadStatus::kUnmergedNow;
  }
  return RasterThreadStatus::kRemainsMerged;
}

lease_term_Die Hauptaufgabe der obigen Logik besteht darin , die Anzahl des Felds zu verringern, wenn die Bedingungen dies zulassen 1. Wenn lease_term_der Wert des Felds festgelegt ist 0, kann die Auflösung der dynamischen Thread-Zusammenführung gestartet und die Aufgabenwarteschlange entbunden werden, wie in Listing 10-25 gezeigt.

// 代码清单10-25 engine/fml/raster_thread_merger.cc
void RasterThreadMerger::UnMergeNow() {
    
    
  std::scoped_lock lock(lease_term_mutex_);
  if (TaskQueuesAreSame()) {
    
     return; }
  if (!IsEnabledUnSafe()) {
    
     return; }
  lease_term_ = 0; // 重置
  bool success = task_queues_->Unmerge(platform_queue_id_); // 见代码清单10-26
  if (success && merge_unmerge_callback_ != nullptr) {
    
    
    merge_unmerge_callback_(); // 告知监听者
  }
}

task_queues_Die obige Logik besteht hauptsächlich darin, die Methode des Objekts aufzurufen Unmergeund den Rückruf zum Aufheben der Bindung auszulösen. UnmergeDie Logik der Methode ist in Listing 10-26 dargestellt.

// 代码清单10-26 engine/fml/message_loop_task_queues.cc
bool MessageLoopTaskQueues::Unmerge(TaskQueueId owner) {
    
    
  std::lock_guard guard(queue_mutex_);
  const auto& owner_entry = queue_entries_.at(owner);
  const TaskQueueId subsumed = owner_entry->owner_of;
  if (subsumed == _kUnmerged) {
    
     return false; } // 无须解除绑定
  queue_entries_.at(subsumed)->subsumed_by = _kUnmerged;
  owner_entry->owner_of = _kUnmerged; // 重置相关字段
  if (HasPendingTasksUnlocked(owner)) {
    
     // 见代码清单10-22
    WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner)); // 见代码清单10-13
  } // 分别检查两个任务队列是否有排队任务,不同于代码清单10-21,此时需要分别处理
  if (HasPendingTasksUnlocked(subsumed)) {
    
     // 因为subsumed已经从owener中释放
    WakeUpUnlocked(subsumed, GetNextWakeTimeUnlocked(subsumed));
  }
  return true; // 消解成功
}

Die obige Logik wurde im Code vermerkt und wird hier nicht wiederholt. Darüber hinaus DecrementLeaseist die Methode nicht UnMergeNowder einzige Triggerpunkt der Methode. Wenn die Methode in Embedder aufgerufen wird , wird die Methode nativeSurfaceDestroyedder Shell ausgelöst , und die Methode löst die Methode aus, wie in der Codeliste 10-27 gezeigt.OnPlatformViewDestroyedRasterizerTeardown

// 代码清单10-27 engine/shell/common/rasterizer.cc
void Rasterizer::Teardown() {
    
     // 渲染相关资源的清理、重置
  compositor_context_->OnGrContextDestroyed();
  surface_.reset();
  last_layer_tree_.reset();
  if (raster_thread_merger_.get() != nullptr && raster_thread_merger_.get()->
      IsMerged()) {
    
    
    FML_DCHECK(raster_thread_merger_->IsEnabled());
    raster_thread_merger_->UnMergeNow(); // 见代码清单10-25
    raster_thread_merger_->SetMergeUnmergeCallback(nullptr);
  }
}

In der obigen Logik wird bei Bedarf auch die Auflösung des dynamisch zusammengeführten Threads (dh das Aufheben der Bindung der Aufgabenwarteschlange) ausgelöst. Bisher wurden nur die Zuweisung und das Zurücksetzen mehrerer Felder wie owner_of, subsumed_by, usw. eingeführt. Die Auswirkungen dieser Felder wurden nicht berührt. Beginnen wir mit der folgenden Analyse.lease_term_

Aufgabenausführung im zusammengeführten Zustand

Im Codelisting 10-18 GetNextTaskToRunwird die Methode verwendet, um die nächste auszuführende Aufgabe abzurufen, wie im Codelisting 10-28 gezeigt.

// 代码清单10-28 engine/fml/message_loop_task_queues.cc
fml::closure MessageLoopTaskQueues::GetNextTaskToRun( TaskQueueId queue_id, 
    fml::TimePoint from_time) {
    
    
  std::lock_guard guard(queue_mutex_);
  if (!HasPendingTasksUnlocked(queue_id)) {
    
     // 见代码清单10-22
    return nullptr; // 如果没有排队任务,则直接返回
  }
  TaskQueueId top_queue = _kUnmerged; 
  const auto& top = PeekNextTaskUnlocked(queue_id, top_queue); // 见代码清单10-29
  if (!HasPendingTasksUnlocked(queue_id)) {
    
    
    WakeUpUnlocked(queue_id, fml::TimePoint::Max());
  } else {
    
     // 存在排队任务,在下一个任务的预期执行时间触发
    WakeUpUnlocked(queue_id, GetNextWakeTimeUnlocked(queue_id));
  } // 如果尚未到任务的预期执行时间,则直接返回
  if (top.GetTargetTime() > from_time) {
    
     return nullptr; }
  fml::closure invocation = top.GetTask(); // 读取任务,并移出队列
  queue_entries_.at(top_queue)->delayed_tasks.pop(); // 确定invocation满足条件后再移除
  return invocation;
}

Der Kern der obigen Logik besteht darin, PeekNextTaskUnlockeddie Warteschlange mit der höchsten Priorität abzurufen. Die spezifische Logik ist in Listing 10-29 dargestellt.

// 代码清单10-29 engine/fml/message_loop_task_queues.cc
const DelayedTask& MessageLoopTaskQueues::PeekNextTaskUnlocked(
    TaskQueueId owner, // 目标任务队列id
    TaskQueueId& top_queue_id) const {
    
     // 一般将_kUnmerged作为默认值
  FML_DCHECK(HasPendingTasksUnlocked(owner));
  const auto& entry = queue_entries_.at(owner); // 目标任务队列
  const TaskQueueId subsumed = entry->owner_of; // 被合并的任务队列id
  if (subsumed == _kUnmerged) {
    
     // 自身没有合并其他任务队列
    top_queue_id = owner;
    return entry->delayed_tasks.top(); // 取任务队列第1个任务
  } // 以下是存在被合并任务队列的情况
  const auto& owner_tasks = entry->delayed_tasks;
  const auto& subsumed_tasks = queue_entries_.at(subsumed)->delayed_tasks;
  const bool subsumed_has_task = !subsumed_tasks.empty();
  const bool owner_has_task = !owner_tasks.empty();
  if (owner_has_task && subsumed_has_task) {
    
     // 两个队列均有任务
    const auto owner_task = owner_tasks.top();
    const auto subsumed_task = subsumed_tasks.top();
    if (owner_task > subsumed_task) {
    
     // 取优先级较高者,见代码清单10-30
      top_queue_id = subsumed;
    } else {
    
    
      top_queue_id = owner;
    }
  } else if (owner_has_task) {
    
     // 仅owner任务队列有任务
    top_queue_id = owner;
  } else {
    
     // 仅subsumed任务队列有任务
    top_queue_id = subsumed;
  }
  return queue_entries_.at(top_queue_id)->delayed_tasks.top(); // 取第1个任务
}

Die Erklärung der obigen Logik wurde im Code angegeben, und owner_taskdie Größenvergleichsregeln sind in der Codeliste 10-30 aufgeführt. Es ist zu beachten, dass sie DelayedTaskin Inkrementen angeordnet sind und je kleiner der Wert, desto höher die Sortierung und desto höher die Priorität. hoch.

// 代码清单10-30 engine/fml/delayed_task.cc
bool DelayedTask::operator>(const DelayedTask& other) const {
    
    
  if (target_time_ == other.target_time_) {
    
     // 预期执行时间相同
    return order_ > other.order_; // order_值越小,优先级越高
  }
  return target_time_ > other.target_time_; // 预期执行时间越小,优先级越高
}

Bisher haben wir die Analyse der dynamischen Thread-Merging-Technologie abgeschlossen.

Zusammenfassung: Der Kern der dynamischen Thread-Merging-Technologie besteht darin, die vom Thread im Thread gehaltene Aufgabenwarteschlange Platformauszuführen .Raster


Referenz:

Ich denke du magst

Origin blog.csdn.net/lyabc123456/article/details/130740736
Empfohlen
Rangfolge