Flutter 笔记 | Flutter 中的路由、包、资源、异常和调试

news2024/11/23 10:53:16

路由管理

Flutter中的路由通俗的讲就是页面跳转。在Flutter中通过Navigator组件管理路由导航。并提供了管理堆栈的方法。如:Navigator.pushNavigator.pop

Flutter中给我们提供了两种配置路由跳转的方式:1、基本路由2、命名路由

普通路由使用

比如我们现在想从HomePage组件跳转到SearchPage组件。

1、需要在HomPage中引入SearchPage.dart

import '../SearchPage.dart';

2、在HomePage中通过下面方法跳转

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

MaterialPageRoute

MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

  • 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
  • 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
    下面我们介绍一下MaterialPageRoute 构造函数的各个参数的意义:
  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
  • settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
  • maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainStatefalse
  • fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果- fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。

Navigator

Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:

1. Future push(BuildContext context, Route route)

将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。

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

将栈顶路由出栈,result 为页面关闭时返回给上一个页面的数据。

Navigator 还有很多其他方法,如Navigator.replaceNavigator.popUntil等,详情请参考API文档或SDK 源码注释,在此不再赘述。

实例方法

Navigator类中每个第一个参数为context静态方法都对应一个相同功能的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)

普通路由跳转传值

路由跳转时,可以通过组件的构造函数直接传值,比如下面想从HomePage给SearchPage传参数

1、定义一个SearchPage接收传值

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、在跳转页面实现传值

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

命名路由传值

官方文档:navigate-with-arguments

1、配置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、定义页面接收arguments传参

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、在跳转页面实现传参

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

Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,可以自行查看API文档。

RouteSetting获取路由参数

也可以通过settings.arguments获取路由参数,组件构造函数无需添加额外参数

class EchoRoute extends StatelessWidget {

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

在打开路由时传递参数:

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

路由表

路由表的定义如下:

Map<String, WidgetBuilder> routes;

它是一个Mapkey为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

注册路由表

直接看代码:

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'), //注册首页路由
  } 
);

可以看到,如果想配置根路由页面,我们只需在路由表routes中注册一下MyHomePage路由,然后将其名字作为MaterialAppinitialRoute属性值即可,该属性决定应用的初始路由页是哪一个命名路由。这样就可以替代默认示例样板中的 home 参数来指定首页。

路由生成钩子

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制,如:

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

这个函数可以用来做页面拦截器、用户权限判断等。

注意,onGenerateRoute 只会对命名路由生效。

在单独文件中统一配置路由表

我们可以把路由表和路由钩子函数统一配置到一个独立的dart文件中,方便管理和使用。

1、新建routers/routers.dart 配置路由

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;
  }
}

然后使用的时候就可以这样:

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,
    );
  }
}

这是使用路由钩子的情况,如果不使用路由钩子,可以这样写:

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

路由返回

Navigator.of(context).pop();

路由返回传值给上一个页面

首先,在启动页面,主要使用await/async来等待要打开的页面的返回结果,这是因为Navigator.pushNamed返回的是一个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}'),
  } 
);        

然后,在打开的路由页面中使用 Navigator.pop(context, result) 来返回值。

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("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

替换路由

比如我们从用户中心页面跳转到了registerFirst页面,然后从registerFirst页面通过
pushReplacementNamed跳转到了registerSecond页面。这个时候当我们点击registerSecond的返回按钮的时候它会直接返回到用户中心。

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

返回根路由

比如我们从用户中心跳转到registerFirst页面,然后从registerFirst页面跳转到registerSecond页面,然后从registerSecond跳转到了registerThird页面。这个时候我们想的是registerThird注册成功后返回到用户中心。 这个时候就用到了返回到根路由的方法。

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

在这里插入图片描述

Android 和Ios使用同样风格的路由跳转

Material组件库中提供了一个MaterialPageRoute组件,它可以使用和平台风格一致的路由切换动画,如在iOS上会左右滑动切换,而在Android上会上下滑动切换 , CupertinoPageRoute是Cupertino组件库提供的iOS风格的路由切换组件如果在Android上也想使用左右切换风格,可以使用CupertinoPageRoute。

1、routers.dart中引入cupertino.dart

import 'package:flutter/cupertino.dart';

2、MaterialPageRoute改为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;
};

路由观察器

路由观察器,可以监听所有路由跳转动作,首先创建一个类继承 NavigatorObserver 实现路由监听 :

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');
    }
  }
}

然后在 MaterialApp 中配置 navigatorObservers 属性:

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

监听未注册的路由

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

包管理

除了常规的在 pub.dev上搜索库文件然后在pubspec.yaml文件下添加外,还有以下几种方式:

  • 依赖本地包:如果我们正在本地开发一个包,包名为pkg1,我们可以通过下面方式依赖
dependencies:
	pkg1:
        path: ../../code/pkg1
  • 依赖Git:你也可以依赖存储在Git仓库中的包。如果软件包位于仓库的根目录中,请使用以下语法
dependencies:
  pkg1:
    git:
      url: git://github.com/xxx/pkg1.git

上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:

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

上面介绍的这些依赖方式是Flutter开发中常用的,但还有一些其他依赖方式,完整的内容读者可以自行查看:https://www.dartlang.org/tools/pub/dependencies 。

注意,不要添加错了位置,要添加到dependencies后面,不要添加到了dev_dependencies后面( 这个是配置开发环境依赖的工具包,而不是flutter应用本身依赖的包)。

配置完后执行界面上的 “Pub get” 提示,自动更新下载依赖包,或者在命令行手动执行 flutter packages get 也可以。

小技巧:

如果我们使用的插件,尤其是native插件,出现报错时(感谢Android混乱的SDK版本以及令人头晕AGP版本),最好的办法是到 pub.dev上搜索它的最新版本使用。

可是,当你找到插件库,打开页面一看:Published 24 months ago … 好家伙,已经超过2年没更新了。。。

这时就会很尴尬,该怎么办呢,有两种办法:

  • 1)可以在 pub.dev 上搜索类似的插件,比如假设你之前使用的是flutter_webview_plugin,那么现在这个库作者还没有更新,我们可以搜索关键字webview,这样就能找到类似的库,初步只要看两个指标:它的发布时间和POPULARITYLIKES指数,发布越近、POPULARITY指数越高越好,因为往往最新的库会修复以前的bug,并且喜欢指数越高的说明问题较少兼容性较好。
  • 2)如果第一种方法没有解决你的问题,或者找到的库有新的兼容问题存在,那么你可以到原来出现问题的库的Github主页上面的issues中搜索,看回复评论较多的,有些国外大神们往往会留下自己暂时解决问题的fork版本地址,你可以尝试使用它(通过上面的Git依赖的配置方式)。
  • 3)如果找遍整个世界,都没有找到你想要的,但是假如你的团队中有专业的Android和iOS开发人员的话,那么可以把原始库的源码下载下来自己团队去修复和维护一个版本。
  • 4)以上都没有解决,那我只能 deeply sorry for that,Leave it to God!

资源管理

可以在在pubspec.yaml文件配置存放图片、字体等资源文件

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

这里的目录是与pubspec.yaml文件同级的,一般就是根目录。

工程里需要用到的图片比较多,有时不想一个一个添加,太麻烦,可使用下面方式批量添加:

assets: [images/]

asset 变体

构建过程支持“asset变体”的概念:不同版本的 asset 可能会显示在不同的上下文中。 在pubspec.yamlassets 部分中指定 asset 路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的 asset 一起被包含在 asset bundle 中。

例如,如果应用程序目录中有以下文件:

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

然后pubspec.yaml文件中只需包含:

flutter:
  assets:
    - graphics/background.png

那么这两个graphics/background.pnggraphics/dark/background.png 都将包含在您的 asset bundle中。前者被认为是 main asset(主资源),后者被认为是一种变体(variant)。

在选择匹配当前设备分辨率的图片时,Flutter会使用到 asset 变体。

加载 assets

您的应用可以通过AssetBundle 对象访问其 asset 。有两种主要方法允许从 Asset bundle 中加载字符串或图片(二进制)文件。

1. 加载文本assets

  • 通过rootBundle 对象加载:每个Flutter应用程序都有一个rootBundle 对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。
  • 通过 DefaultAssetBundle 加载:建议使用 DefaultAssetBundle 来获取当前 BuildContextAssetBundle。 这种方法不是使用应用程序构建的默认 asset bundle,而是使父级 widget 在运行时动态替换的不同的 AssetBundle,这对于本地化或测试场景很有用。

通常,可以使用DefaultAssetBundle.of()在应用运行时来间接加载 asset(例如JSON文件),而在 widget 上下文之外,或其他AssetBundle句柄不可用时,可以使用rootBundle直接加载这些 asset,例如:

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

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

2. 加载图片

1)声明分辨率相关的图片 assets

AssetImage 可以将asset的请求逻辑映射到最接近当前设备像素比例(dpi)的asset。为了使这种映射起作用,必须根据特定的目录结构来保存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'),
      ),
    ),
  );
}

注意,AssetImage 并非是一个widget, 它实际上是一个ImageProvider,有些时候你可能期望直接得到一个显示图片的widget,那么你可以使用Image.asset()方法,如:

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

使用默认的 asset bundle 加载资源时,内部会自动处理分辨率等,这些处理对开发者来说是无感知的。 (如果使用一些更低级别的类,如 ImageStreamImageCache时你会注意到有与缩放相关的参数)

3)依赖包中的资源图片

要加载依赖包中的图像,必须给AssetImage提供package参数。

例如,假设您的应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:

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

然后加载图像,使用:

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

或者:

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

注意:包在使用本身的资源时也应该加上package参数来获取。

打包 assets

如果在pubspec.yaml文件中声明了期望的资源,它将会打包到相应的package中。特别是,包本身使用的资源必须在pubspec.yaml中指定。

包也可以选择在其lib文件夹中包含未在其pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml中指定包含哪些图像。 例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:

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

要包含第一张图像,必须在pubspec.yamlassets部分中声明它:

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

lib/是隐含的,所以它不应该包含在资产路径中。

特定平台的 assets 使用

上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用,如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets。

1)设置APP图标

更新Flutter应用程序启动图标的方式与在本机Android或iOS应用程序中更新启动图标的方式相同。

  • Android

    在 Flutter 项目的根目录中,导航到../android/app/src/main/res目录,里面包含了各种资源文件夹(如mipmap-hdpi已包含占位符图像 “ic_launcher.png”,见下图)。 只需按照Android开发人员指南中的说明, 将其替换为所需的资源,并遵守每种屏幕密度(dpi)的建议图标大小标准。
    在这里插入图片描述
    注意: 如果您重命名.png文件,则还必须在您AndroidManifest.xml<application>标签的android:icon属性中更新名称。

  • iOS

    在Flutter项目的根目录中,导航到../ios/Runner。该目录中Assets.xcassets/AppIcon.appiconset已经包含占位符图片(见下图), 只需将它们替换为适当大小的图片,保留原始文件名称。
    在这里插入图片描述

2)更新启动页

在 Flutter 框架加载时,Flutter 会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。

注意: 这意味着如果您不在应用程序的main()方法中调用runApp 函数 (或者更具体地说,如果您不调用window.render去响应window.onDrawFrame)的话, 启动屏幕将永远持续显示。

  • Android

    要将启动屏幕(splash screen)添加到您的Flutter应用程序, 请导航至../android/app/src/main。在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面(你也可以直接换一张图片)。

  • iOS

    要将图片添加到启动屏幕(splash screen)的中心,请导航至../ios/Runner。在Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为LaunchImage.png、LaunchImage@2x.png、LaunchImage@3x.png。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。

    您也可以通过打开Xcode完全自定义storyboard。在Project Navigator中导航到Runner/Runner然后通过打开Assets.xcassets拖入图片,或者通过在LaunchScreen.storyboard中使用Interface Builder进行自定义,如图所示。
    在这里插入图片描述

平台共享资源

官方文档:sharing-assets-with-the-underlying-platform

如果我们采用的是Flutter+原生的开发模式,那么可能会存Flutter和原生需要共享资源的情况,比如Flutter项目中已经有了一张图片A,如果原生代码中也要使用A,我们可以将A拷贝一份到原生项目的特定目录,这样的话虽然功能可以实现,但是最终的应用程序包会变大,因为包含了重复的资源,为了解决这个问题,Flutter 提供了一种Flutter和原生之间共享资源的方式。

1.在 Android 中加载 Flutter 资源文件

在 Android 平台上,assets 通过 AssetManager API 读取。通过 PluginRegistry.RegistrarlookupKeyForAsset 方法,或者 FlutterViewgetLookupKeyForAsset 方法来获取文件路径,然后 AssetManageropenFd 根据文件路径得到文件描述符。开发插件时可以使用 PluginRegistry.Registrar,而开发应用程序使用平台视图时,FlutterView 是最好的选择。

举个例子,假设你在 pubspec.yaml 中这样指定:

flutter:
  assets:
    - icons/heart.png

在你的 Flutter 应用程序对应以下结构。

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

想要在 Java 插件中访问 icons/heart.png:

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

2.在 iOS 中加载 Flutter 资源文件

在 iOS 平台上,assets 资源文件通过 mainBundle 读取。通过 pathForResource:ofType:lookupKeyForAsset 或者 lookupKeyForAsset:fromPackage: 方法获取文件路径,同样,FlutterViewControllerlookupKeyForAsset: 或者 lookupKeyForAsset:fromPackage: 方法也可以获取文件路径。开发插件时可以使用 FlutterPluginRegistrar,而开发应用程序使用平台视图时, FlutterViewController 是最好的选择。

举个例子,假设你的 Flutter 配置和上面一样。

要在 Objective-C 插件中访问 icons/heart.png

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

这有一个更完整的实例可以理解 Flutter 的应用:video_player plugin。

pub.dev 上的 ios_platform_images plugin 将这些逻辑封装成方便的类别。它允许编写:

Objective-C:

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

Swift:

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

使用第三方 Icons 图标资源

这里推荐两个比较好用的Icons图标资源网站,可能做前端开发的人已经比较熟悉了,它们是:

  • iconfont:https://www.iconfont.cn/ 可以搜索自己想要的图标,然后下载下来(需要登录),里面会包含各种格式,导入Flutter中使用即可。
  • fluttericon:https://fluttericon.com/ 这个是专门为Flutter设计的,提供了很多 Material Design 风格的图标,直接在页面中选择喜欢的图标后点击顶部的DOWNLOAD按钮下载即可,下载文件中会同时包含Dart使用示例代码,

注意,虽然这两个网站提供了其他格式的图标,但是尽量选择ttf字体图标,因为可以动态设置字体图标的颜色,不然当需要更改应用主题颜色时就会比较尴尬了。当然如果你选择使用普通png图片做图标的话,自己就别折腾了,还是让设计切图吧。

调试Flutter应用

常规调试手段总结:

  • 巧用断点调试
  • Debugger面板
  • 善用变量Variables视窗与观察Watchers视窗
  • 善用控制台(Console)进行log分析
  • 使用 Dart DevTools
  • 使用 Flutter Inspector 诊断布局问题(不清楚现有布局)
  • 使用 Flutter Outline

日志与断点

1. debugger() 声明

当使用Dart Observatory(或另一个Dart调试器,例如IntelliJ IDE中的调试器)时,可以使用该debugger()语句插入编程式断点。要使用这个,你必须添加import 'dart:developer';到相关文件顶部。

debugger()语句采用一个可选when参数,我们可以指定该参数仅在特定条件为真时中断,如下所示:

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

2. print、debugPrint、flutter logs

Dart print()功能将输出到系统控制台,我们可以使用flutter logs来查看它(基本上是一个包装adb logcat)。

如果你一次输出太多,那么Android有时会丢弃一些日志行。为了避免这种情况,我们可以使用Flutter的foundation库中的debugPrint() (需要导入flutter/foundation包),它封装了 print,将一次输出的内容长度限制在一个级别(内容过多时会分批输出),避免被Android内核丢弃。

还可以根据kDebugModekReleaseMode来决定是否只在debug/release模式下输出日志:

import 'package:flutter/foundation.dart';

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

Flutter框架中的许多类都有toString实现,按照惯例,输出信息通过包括对象的运行时类型 、类名以及关键字段等信息。 树中的一些类也具有toStringDeep实现,从该点返回整个子树的多行描述。一些具有详细信息toString的类会实现一个toStringShort,它只返回对象的类型或其他非常简短的(一个或两个单词)描述。

3. 调试模式断言

在Flutter应用调试过程中,Dart assert语句被启用,并且 Flutter 框架使用它来执行许多运行时检查来验证是否违反一些不可变的规则。当一个某个规则被违反时,就会在控制台打印错误日志,并带上一些上下文信息来帮助追踪问题的根源。

要关闭调试模式并使用发布模式,请使用flutter run --release运行我们的应用程序。 这也关闭了Observatory调试器。一个中间模式可以关闭除Observatory之外所有调试辅助工具的,称为“profile mode”,用--profile替代--release即可。

4. 断点

开发过程中,断点是最实用的调试工具之一,我们以 Android Studio 为例:

在这里插入图片描述
我们在 93 行打了一个断点,一旦代码执行到这一行就会暂停,这时我们可以看到当前上下文所有变量的值,然后可以选择一步一步的执行代码。关于如何通过 IDE 来打断点,可以自行搜索网上教程很多。

Flutter 调试中的后悔药:

  • 通过Frames回退
  • 通过Drop Frame回退

在这里插入图片描述

调试应用程序层

Flutter框架的每一层都提供了将其当前状态或事件转储(dump)到控制台(使用debugPrint)的功能。

1. Widget 树调试 debugDumpApp()

要转储Widgets树的状态,请调用debugDumpApp()。 只要应用程序已经构建了至少一次(即在调用build()之后的任何时间),我们可以在应用程序未处于构建阶段(即不在build()方法内调用 )的任何时间调用此方法(在调用runApp()之后)。

如, 这个应用程序:

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'),
        ),
      ),
    );
  }
}

会输出这样的内容:

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))
... #省略剩余内容

这是一个“扁平化”的树,显示了通过各种构建函数投影的所有widget(如果你在widget树的根中调用toStringDeepwidget,这是你获得的树)。 你会看到很多在你的应用源代码中没有出现的widget,因为它们是被框架中widgetbuild()函数插入的。例如,InkFeatureMaterial widget的一个实现细节 。

当按钮从被按下变为被释放时debugDumpApp()被调用,TextButton对象同时调用setState(),并将自己标记为"dirty"。我们还可以查看已注册了哪些手势监听器; 在这种情况下,一个单一的GestureDetector被列出,并且监听“tap”手势(“tap”是TapGestureDetectortoStringShort函数输出的)。

如果我们编写自己的widget,则可以通过覆盖debugFillProperties()来添加信息。 将DiagnosticsProperty 对象作为方法参数,并调用父类方法。 该函数是该toString方法用来填充小部件描述信息的。

2. 渲染树调试 debugDumpRenderTree()

如果我们尝试调试布局问题,那么Widget树可能不够详细。在这种情况下,我们可以通过调用debugDumpRenderTree()转储渲染树。 正如debugDumpApp(),除布局或绘制阶段外,我们可以随时调用此函数。作为一般规则,从frame 回调或事件处理器中调用它是最佳解决方案。

要调用debugDumpRenderTree(),我们需要添加import'package:flutter/rendering.dart';到我们的源文件。

上面这个小例子的输出结果如下所示:

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: CustomPaintBannerCheckedModeBanner ←
I/flutter ( 6559):WidgetsApp-[GlobalObjectKey _MaterialAppState(1009803148)] ←
I/flutter ( 6559):ThemeAnimatedThemeScrollConfigurationMaterialApp ←
I/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)
... # 省略

这是根RenderObject对象的toStringDeep函数的输出。

当调试布局问题时,关键要看的是sizeconstraints字段。约束沿着树向下传递,尺寸向上传递。

如果我们编写自己的渲染对象,则可以通过覆盖debugFillProperties() 将信息添加到转储。 将DiagnosticsProperty 对象作为方法的参数,并调用父类方法。

3. Layer树调试 debugDumpLayerTree()

渲染树是可以分层的,而最终绘制需要将不同的层合成起来,而Layer则是绘制时需要合成的层,如果我们尝试调试合成问题,则可以使用debugDumpLayerTree() 。对于上面的例子,它会输出:

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

这是根LayertoStringDeep输出的。

根部的变换是应用设备像素比的变换; 在这种情况下,每个逻辑像素代表3.5个设备像素。

RepaintBoundary widget在渲染树的层中创建了一个RenderRepaintBoundary。这用于减少需要重绘的需求量。

4. 语义树调试 debugDumpSemanticsTree()

我们还可以调用debugDumpSemanticsTree() 获取语义树(呈现给系统可访问性API的树)的转储。 要使用此功能,必须首先启用辅助功能,例如启用系统辅助工具或SemanticsDebugger

对于上面的例子,它会输出:

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. 调度

要找出相对于帧的开始/结束事件发生的位置,可以切换debugPrintBeginFrameBannerdebugPrintEndFrameBanner布尔值以将帧的开始和结束打印到控制台。

例如:

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 : ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

debugPrintScheduleFrameStacks 还可以用来打印导致当前帧被调度的调用堆栈。

6. 可视化调试

我们也可以通过设置debugPaintSizeEnabledtrue以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。

当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widgetPadding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widgetCenterAlign)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。

debugPaintBaselinesEnabled 做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。

debugPaintPointersEnabled 标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。

如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary widget,则可以使用debugPaintLayerBordersEnabled标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled 标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。

所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug...” 开头的任何内容都只能在调试模式下工作。

7. 调试动画

调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation 变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。

8. 调试性能问题

要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacksdebugPrintMarkNeedsPaintStacks 标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹。

9. 统计应用启动时间

要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run时使用trace-startupprofile选项。

$ flutter run --trace-startup --profile

跟踪输出保存为start_up_info.json,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:

  • 进入Flutter引擎时.
  • 展示应用第一帧时.
  • 初始化Flutter框架时.
  • 完成Flutter框架初始化时.

如 :

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

10. 跟踪Dart代码性能

要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace )。 使用dart:developerTimeline 工具来包含你想测试的代码块,例如:

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

然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。

刷新页面将在Chrome的跟踪工具中显示应用按时间顺序排列的timeline记录。

请确保运行flutter run时带有--profile标志,以确保运行时性能特征与我们的最终产品差异最小。

Dart DevTools 使用

Tools->Flutter->Open Dart DevTools 首次运行会先自动安装,点击debug运行后在控制台中有个 Open DevTools 的蓝色小图标,点击会在浏览器打开Dart DevTools

  • 选中select widget mode后,点击手机上的控件可以进入选中的控件的调试页面
  • 当选中某个控件后点击layout explorer之后可以调试
  • 如果是flex布局如Row Column等,在layout explorer可点击主轴和交叉轴的对齐方式,手机界面会实时的根据选中结果变化效果
  • 如果报错可能是Widget组件没有指定 textDirection: TextDirection.ltr 导致

Flutter 在 Android Studio中如何运行除了main.dart以外的dart文件(包含main函数),执行命令:

flutter run lib/animated_list.dart

Flutter 运行快捷键命令:

  • r:热重新加载。
  • R:热重启。
  • h:重复此帮助信息。
  • d:分离(终止“颤动运行”,但保持应用程序运行)。
  • c:清除屏幕。
  • q:退出(终止设备上的应用程序)。

一般最常用的就是热加载输入:r

Flutter 创建应用:

  • 在终端中运行:flutter create xxx 快速创建一个flutter应用模板

Flutter Outline面板顶部有一排按钮可以使点击的控件被快速包裹进某个常用布局容器中 或者选中控件右键。

DevTools提供了很多很全面的功能,更多内容可以参考其官网:DevTools (点击该页面左侧的目录了解更多)

Flutter的四种运行模式

Flutter有四种运行模式:DebugReleaseProfileTest,这四种模式在build的时候是完全独立的。

  • Debug:Debug模式可以在真机模拟器上同时运行:会打开所有的断言,包括debugging信息、debugger aids(比如observatory)和服务扩展。优化了快速develop/run循环,但是没有优化执行速度、二进制大小和部署。运行命令:flutter run,通过sky/tools/gn --android或者sky/tools/gn --iosbuild。有时候也被叫做“checked模式”或者“slow模式”。

  • Release:Release模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和debugging信息,关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。禁用所有的debugging aids和服务扩展。这个模式是为了部署给最终的用户使用。运行命令:flutter run --release,通过sky/tools/gn --android --runtime-mode=release或者sky/tools/gn --ios --runtime-mode=releasebuild

  • Profile:Profile模式只能在真机上运行,不能在模拟器上运行:基本和Release模式一致,除了启用了服务扩展和tracing,以及一些为了最低限度支持tracing运行的东西(比如可以连接observatory到进程)。命令flutter run --profile就是以这种模式运行的,通过sky/tools/gn --android --runtime-mode=profile或者sky/tools/gn --ios --runtime-mode=profile```来build。因为模拟器不能代表真实场景,所以不能在模拟器上运行。

  • Test:headless test模式只能在桌面上运行:基本和Debug模式一致,除了是headless的而且你能在桌面运行。命令flutter test就是以这种模式运行的,通过sky/tools/gnbuild

在我们实际开发中,应该用到上面所说的四种模式又各自分为两种:一种是未优化的模式,供开发人员调试使用;一种是优化过的模式,供最终的开发人员使用。默认情况下是未优化模式,如果要开启优化模式,build的时候在命令行后面添加--unoptimized参数。

注意,release模式Android有可能要手动添加Androidmanifest.xml 中的INTERNET权限(不然你可能会发现不能使用网络)

Flutter 热键 / 快捷代码生成

Android Studio可以通过Settings->Editor->Live Templates中配置热键输入关键字快速生成Flutter的有状态和无状态组件:

在这里插入图片描述
在这里插入图片描述

这样当我们在dart文件中输入stless时就会快速生成StatelessWidget模板,输入stful时就会快速生成StatefulWidget模板。

其他任何你想要避免重复输入的代码片段,都可以如法炮制。

当然,如果你比较懒,想用别人配好的,直接在Android Studio中的Settings->Plugins搜索Flutter Snippets 插件安装使用:

在这里插入图片描述

安装完成后,可以打开Settings->Editor->Live Templates找到Flutter分组查看它有哪些快捷热键,另外你也可以直接查看其官方文档的说明:Flutter Snippets

对于使用 Visual Studio Code 的用户,同样可以在它的插件市场中搜索到类似的插件。

Flutter异常捕获

Dart 单线程模型

JavaObjective-C(以下简称“OC”)中,如果程序发生异常且没有被捕获,那么程序将会终止,但是这在DartJavaScript中则不会!究其原因,这和它们的运行机制有关系。JavaOC 都是多线程模型的编程语言,任意一个线程触发异常且该异常未被捕获时,就会导致整个进程退出。但 DartJavaScript 不会,它们都是单线程模型,运行机制很相似(但有区别),下面我们通过Dart官方提供的一张图来看看 Dart 大致运行原理:

在这里插入图片描述

Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列microtask queue,另一个叫做“事件队列event queue。从图中可以发现,微任务队列的执行优先级高于事件队列

现在我们来介绍一下Dart线程运行过程,如上图中所示,入口函数 main() 执行完后,消息循环机制便启动了。首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件任务执行完毕后程序便会退出,但是,在事件任务执行的过程中也可以插入新的微任务事件任务,在这种情况下,整个线程的执行过程便是一直在循环,不会退出,而Flutter中,主线程的执行过程正是如此,永不终止

在Dart中,所有的外部事件任务都在事件队列中,如 IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。值得注意的是,我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。

在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其他任务执行的

Flutter 框架的异常捕获

Dart 中可以通过try/catch/finally来捕获代码块异常,这个和其他编程语言类似。

Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不合规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下:


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

可以看到,在发生异常时,Flutter默认的处理方式是弹一个ErrorWidget,但如果我们想自己捕获异常并上报到报警平台的话应该怎么做?我们进入_debugReportException()方法看看:

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;
}

我们发现,错误是通过FlutterError.reportError方法上报的,继续跟踪:

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

我们发现onErrorFlutterError的一个静态属性,它有一个默认的处理方法 dumpErrorToConsole,到这里就清晰了,如果我们想自己上报异常,只需要提供一个自定义的错误处理回调即可,如:

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

这样我们就可以处理那些Flutter为我们捕获的异常了。

其他异常捕获与日志收集

在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常Future中的异常。在Dart中,异常分两类:同步异常异步异常,同步异常可以通过try/catch捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:

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

Dart中有一个runZoned(...) 方法,可以给执行对象指定一个ZoneZone表示一个代码执行的环境范围,为了方便理解,读者可以将Zone类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。下面我们看看runZoned(...)方法定义:

R runZoned<R>(R body(), {
    Map zoneValues, 
    ZoneSpecification zoneSpecification,
}) 
  • zoneValues: Zone 的私有数据,可以通过实例zone[key]获取,可以理解为每个“沙箱”的私有数据。

  • zoneSpecification:Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误等。

举个例子:

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');
      },
    ),
  );

这样一来,我们 APP 中所有调用print方法输出日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将异常信息和日志统一上报。

另外我们还拦截了未被捕获的异步错误,这样一来,结合上面的 FlutterError.onError 我们就可以捕获我们Flutter应用错误了并进行上报了!

最终的错误上报代码大致如下:

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');
	    }, ),
  );
}

Flutter 中的消息循环原理分析

前面在 Dart 单线程模型中提到它有两个任务队列,都是在消息循环机制下运行的,消息循环(MessageLoop)是所有UI框架的基石,类似 Android 的 Looper 机制,Flutter 的底层也是基于消息循环驱动的,下面开始详细分析。

首先分析Flutter中消息循环的关键类及其关系,如图所示:

在这里插入图片描述

上图中,MessageLoop类是Flutter创建消息循环能力的入口,而TaskRunner则是Flutter使用消息循环能力的入口,大部分线程任务的注册都是通过TaskRunnerPostTask方法实现的。

  • MessageLoopImpl实现了消息循环的通用逻辑以及持有并管理消息队列(通过MessageLoopTaskQueues)。对Android平台而言,MessageLoopAndroid继承了MessageLoopImpl,并提供消息循环的底层实现:ALoopertimerfd_create所创建的文件描述符

  • MessageLoopTaskQueues管理所有线程的消息队列(也称任务队列),每个线程的消息循环(MessageLoopImpl对象)都将持有一个TaskQueueId实例,用于向MessageLoopTaskQueuesqueue_entries_字段查询当前消息循环所对应的消息队列——TaskQueueEntry

  • TaskQueueEntrydelayed_tasks字段持有一个DelayedTaskQueue的实例,DelayedTaskQueue是一个优先队列std::priority_queue),将根据时间优先级对等待执行的消息进行排序。此外TaskQueueEntry还会通过Wakeable接口持有消息循环(MessageLoopImpl)的引用,用以执行DelayedTaskQueue中的任务。

以上便是消息循环的总体架构,核心在于TaskRunnerTaskQueueEntry的调用路径。

消息循环启动

Flutter通过系统API完成新线程的创建后,会基于该线程启动了消息循环,接下来基于EnsureInitializedForCurrentThread方法的逻辑继续分析消息循环的启动,如代码清单10-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;
}

以上逻辑将触发MessageLoop的构造函数,完成loop_task_runner_字段的初始化。首先分析MessageLoopImpl::Create方法的逻辑,如代码清单10-2所示。

// 代码清单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
}

以上逻辑将触发MessageLoopAndroid的构造函数,其逻辑将在代码清单10-8中详细分析。首先分析MessageLoopAndroid的父类MessageLoopImpl的构造函数(注意C++中父类的构造函数是隐式触发的),如代码清单10-3所示。

// 代码清单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
}

以上逻辑仍然是类成员字段的初始化。首先分析task_queue_字段的初始化,如代码清单10-4所示。

// 代码清单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_;
}

以上逻辑是一个典型的单例实现。接下来分析queue_id_字段的初始化,如代码清单10-5 所示。

// 代码清单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;
}

以上逻辑中,考虑到queue_entries_将存储不同线程创建的TaskQueueId,因此每次使用都需要加锁。SetWakeable使得task_queue_反向持有MessageLoopImpl实例的引用,其将在后续逻辑中用到。

以上逻辑中,TaskQueueIdTaskQueueEntry一一对应,而TaskQueueEntry又会通过wakeable持有当前消息循环实例的引用(在代码清单10-3中设置)。接下来继续分析TaskQueueEntry的构造函数,如代码清单10-6所示。

// 代码清单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节
}

以上逻辑主要是TaskQueueEntry的相关字段的初始化,owner_ofsubsumed_by字段将用于后面内容介绍的动态线程合并,它们在正常情况下为常量_kUnmerged,表示当前任务队列不被其他任务队列持有,也不持有其他任务队列。

下面,继续分析TaskQueueEntry(Value)所对应的Key,即TaskQueueId类,如代码清单10-7所示。

// 代码清单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; // 默认值
};

TaskQueueId顾名思义是一个任务队列的id,由以上逻辑可知,其本质就是一个int类型的整数(value_)。

以上便是MessageLoopImpl的构造函数所引发的逻辑,是Flutter中消息循环相关的通用逻辑的初始化。接下来,不同平台将基于各自的系统API开始消息循环中与平台相关的初始化逻辑,接下来以Android为例进行介绍,如代码清单10-8所示。

// 代码清单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);
}

以上逻辑主要分为3步,相关细节在代码中均已注明。其中,

  • 1步的timerfd_create方法是一个Linux系统API,将产生一个文件描述符用于后续 Looper轮询timerfd_create方法的第1个参数clockid的值为kClockType,本质是CLOCK_MONOTONIC,表示从系统启动这一刻开始计时不受系统时间被用户改变的影响;第2个参数flags包括TFD_NONBLOCKTFD_CLOEXEC,均是为了保证当前面内容件描述符的正常使用。具体来说,TFD_NONBLOCK表明当前是非阻塞模式,TFD_CLOEXEC表示当程序执行exec函数时,当前文件描述符将被系统自动关闭而不继续传递。

  • 2步,构造一个回调,当目标文件描述符存在数据时Looper将触发这个回调,详见代码清单10-16。

  • 3步,通过ALooper_addFd这个系统调用完成Looper和文件描述符timer_fd_的绑定。

以上逻辑中,Looper的初始化逻辑在AcquireLooperForThread方法中,如代码清单10-9所示。

// 代码清单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;
}

Platform线程(主线程)来说,ALooper_forThread即可获得Looper(即Android主线程的消息循环),而UI线程、Raster线程和I/O线程则需要通过ALooper_prepare新建一个Looper

完成以上逻辑后,可以正式启动Looper了,如代码清单10-10所示。

// 代码清单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
}

以上逻辑中,ALooper_pollOnce涉及Linuxpipe/epoll机制,即调用该方法后便会释放CPU资源并等待Looper轮询的文件描述符传来的数据,既不会像常规的同步方法那样阻塞,也不会像常规的异步方法那样直接进入while无限循环

接下来分析如何向消息循环注册或提交任务。

任务注册

在前面曾多次出现的PostTask方法,其逻辑如代码清单10-11所示。

// 代码清单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_字段的PostTask方法,如代码清单10-12所示。

// 代码清单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
}

以上逻辑主要是通过RegisterTask方法向当前线程的消息循环所对应的任务队列注册任务并设置唤醒时间,如代码清单10-13所示。

// 代码清单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); // 设置唤醒时间
  }
}

以上逻辑首先通过delayed_tasks字段完成任务的注册,然后通过WakeUp方法告知消息循环在指定时间触发任务执行,如代码清单10-14所示。

// 代码清单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
}

以上逻辑将通过代码清单10-8中创建的文件描述符来实现硬件层的定时器逻辑,如代码清单10-15所示。

// 代码清单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;
}

以上逻辑主要借助系统调用timerfd_settime方法完成定时器的设置,在此不再赘述。

任务执行

当到达指定时间时,timer_fd_将触发完成一次轮询以及代码清单10-8中的回调,OnEventFired的逻辑如代码清单10-16所示。

// 代码清单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
}

以上逻辑首先调用TimerDrain方法进行检查,如代码清单10-17所示。

// 代码清单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;
}

检查通过后将触发FlushTasks方法,处理代码清单10-13中注册的任务,具体逻辑如代码清单10-18所示。

// 代码清单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);
}

以上逻辑主要是从当前消息循环所持有的任务队列task_queue_字段中取出一个任务并执行,并通知已注册的观察者,GetNextTaskToRun方法的逻辑将在10.2节详细介绍。

至此,Flutter Engine的消息循环及其底层机制分析完毕。

总结:

在Flutter端通过MessageLoopImpl实现了消息循环的通用逻辑以及持有并管理消息队列,到了Android平台端则通过MessageLoopAndroid具体化在Android平台消息循环的实现。

  • 对于Android,消息循环底层离不开Linux的epoll机制,该机制是保证系统UI架构中的任务队列既不占用CPU,又可以随时被唤醒的核心。
  • 当然消息队列中的任务,需要按优先级排队,或延时执行,而每个通过RegisterTask注册的任务都设置了唤醒时间,这里的关键就是关于执行时间的安排,由于Android基于Linux内核,解决方式自然离不开Linux的文件描述符(Linux万物皆文件),要点就是使用timer_fd_这个文件描述符通过系统调用来设置硬件定时时间,定时时间到,触发回调,执行任务。

动态线程合并技术

Flutter Engine中的动态线程合并技术,虽然本身是一套独立的逻辑,但是却分散在其他诸多逻辑中,如果不单独加以分析,那么在阅读其他代码时会因这个奇怪而突兀的逻辑而难以理解透彻。比如代码清单10-18中的GetNextTaskToRun方法,如果没有动态线程合并,那么可以简单地处理为取出当前消息循环所持有的task_queue_字段的最高优先级任务,但是由于存在动态线程合并,其逻辑将复杂好几倍。

动态线程合并主要是为了Flutter UI能与原生View同帧渲染(因为Platform线程中的FlutterImageViewCanvas变成Flutter UI的渲染输出),结合消息循环机制,可以猜测:所谓动态线程合并,并不是将Platform线程和Raster线程在系统层面做了合并,而是让Platform线程的消息循环可以接管并处理原Raster线程的消息循环所持有的任务队列。

此外,动态线程合并还涉及一些现实问题,比如何时启动线程合并、何时关闭线程合并,因为Platform View消失之后,自然不需要动态线程合并,此时为了提高性能,应该恢复原来正常的处理关系。

合并、维持与消解

首先分析线程(更准确地说是消息循环,下同)的合并逻辑,它们是后续分析的基础。由于Platform View的存在,会触发MergeWithLease方法,其逻辑如代码清单10-19所示。

// 代码清单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方法
}

以上逻辑首先检查是否有必要开始动态线程合并,相关逻辑如代码清单10-20所示。检查完成后将正式开始合并,并更新lease_term_字段,该字段用于判断当前是否有必要维持线程合并状态,具体作用将在后面内容分析。

// 代码清单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_; // 检查两个任务队列本身是否相同
}

下面分析动态线程合并的具体逻辑,如代码清单10-21所示。

// 代码清单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;
}

由于Android中原生UI必须主线程渲染,因此以上逻辑中ownerPlatform线程的任务队列,subsumedRaster线程的任务队列。以上逻辑主要是将owner任务队列的owner_of字段设置为Raster线程的任务队列,将subsumed任务队列的subsumed_by字段设置为Platform线程的任务队列。这样,处理owner任务队列时就会一并处理调用owner_of所对应的任务队列,而如果一个任务队列的subsumed_by字段不为_kUnmerged,则说明它将由其他任务队列连带处理,因此直接退出即可,这部分内容后面将详细分析。

以上逻辑将在合并完成后通过HasPendingTasksUnlocked方法检查是否有未处理的任务,如代码清单10-22所示。

// 代码清单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();
  }
}

在理解ownersubsumed的含义后,以上逻辑变得十分清晰。接下来分析动态线程合并状态的维持。在代码清单9-42中,如果已经处于线程合并状态,而当前又正好在绘制包含Platform View的帧,则会调用ExtendLeaseTo方法以延长动态线程合并维持的时间,如代码清单10-23所示。

// 代码清单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;
  }
}

以上逻辑中,ExtendLeaseTo方法的传入参数lease_term的值一般是10,由if逻辑可知,如果Platform View一直在渲染,lease_term_会被始终更新成10,而不是每次累加10,即每次调用该方法,都将让动态线程合并状态继续维持lease_term帧。动态线程合并状态的维持,本质是lease_term_字段的更新。接下来分析动态线程合并状态的消解,以及在此过程中lease_term_字段所产生的作用。

在代码清单5-100中,完成一帧的渲染后,会触发DecrementLease方法,如代码清单10-24所示。

// 代码清单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_字段的计数减1。当lease_term_字段的值为0时,即可开始动态线程合并的消解,解绑任务队列,如代码清单10-25所示。

// 代码清单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_对象的Unmerge方法,并触发解除绑定的回调。Unmerge方法的逻辑如代码清单10-26所示。

// 代码清单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; // 消解成功
}

以上逻辑已在代码中注明,在此不再赘述。此外,DecrementLease方法并非UnMergeNow 方法的唯一触发点,当Embedder中调用nativeSurfaceDestroyed方法时,将会触发Shell的OnPlatformViewDestroyed方法,该方法又将触发RasterizerTeardown方法,如代码清单10-27所示。

// 代码清单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);
  }
}

以上逻辑中,如有必要,也会触发已经动态合并线程的消解(即任务队列绑定的解除)。到目前为止,只介绍了owner_ofsubsumed_bylease_term_等几个字段的赋值与重置,这些字段产生的影响尚未触及,下面开始分析。

合并状态下的任务执行

代码清单10-18中,GetNextTaskToRun方法用于获取下一个被执行的任务,如代码清单10-28所示。

// 代码清单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;
}

以上逻辑的核心在于通过PeekNextTaskUnlocked获取优先级最高的队列,具体逻辑如代码清单10-29所示。

// 代码清单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个任务
}

以上逻辑的解释均已在代码中注明,其中owner_task的大小比较规则如代码清单10-30所示,需要注意的是,DelayedTask是递增排列的,其取值越小,排序越靠前,优先级越高。

// 代码清单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_; // 预期执行时间越小,优先级越高
}

至此,我们已经完成动态线程合并技术的分析。

总结:动态线程合并技术的本质就是在Platform线程执行Raster线程所持有的任务队列。


参考:

  • 《Flutter实战·第二版》
  • 《Flutter内核源码剖析》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550033.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

详解c++STL—函数对象

目录 1、函数对象 1.1、函数对象概念 1.2、函数对象的使用 2、谓词 2.1、谓词概念 2.2、一元谓词 2.3、二元谓词 3、内建函数对象 3.1、理解内建函数对象 3.2、算术仿函数 3.3、关系仿函数 3.4、逻辑仿函数 1、函数对象 1.1、函数对象概念 概念&#xff1a; 重载…

数据结构第三天 【二叉搜索树】

这道题真是写的我想吐了&#xff0c;主要是函数太多&#xff0c;排错太难了&#xff0c;搞了两个小时&#xff0c;基本就是在排错&#xff0c;排了一个小时&#xff0c;后面自己心态也有点崩溃了&#xff0c;其实不是一道很难的题&#xff0c;但是是一个非常麻烦的题目&#xf…

使用Serv-U搭建FTP服务器并公网访问

文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 转载自内网穿透工具的文章&#xff1a;使用Serv-U搭建FTP服务器并公网访问【内网穿透】_ 1. 前言…

linux专题:GDB详细调试方法与实现

系列文章目录 例如&#xff1a;第一章 Linux-GDB 调试实验的使用 文章目录 目录 系列文章目录 文章目录 一、实验目的 二、实验现象 三、实验准备 四、Linux GDB调试实验流程 五、Linux GDB 调试器 总结 一、实验目的 掌握使用 gcc 分步编译 c 代码为可执行程序步骤以及 gc…

【数学建模】步长的选择(优化建模)

人们每天都在行走&#xff0c;排除以运动健身为目的的走路方式&#xff0c;而仅仅考虑距离固定&#xff0c;以节省体力为最终目的的行走&#xff0c;那么选择多大的步长才最省力&#xff1f; 人在走路时所做的功等于抬高人体重心所需的势能与两腿运动所需的动能之和。在给定速度…

又到520了,来画一朵抽搐的玫瑰花吧

文章目录 静态的玫瑰 敲了这么多年代码&#xff0c;每年都得画一些心啊花啊什么的&#xff0c;所以现在常规的已经有些倦怠了&#xff0c;至少也得来个三维图形才看着比较合理&#xff0c;而且光是三维的也没啥意思&#xff0c;最好再加上能动起来。 静态的玫瑰 网上有很多生…

AIGC技术研究与应用 ---- 下一代人工智能:新范式!新生产力!(1-简介)

文章大纲 AI GC参考文献与学习路径模型进化券商研报陆奇演讲AI GC AI模型可大致分为决策式/分析式AI(Discriminant/Analytical AI)和生成式AI (Generative AI)两类。 决策式AI:学习数据中的条件概率分布,根据已有数据进行分析、判断、预测,主要应用模型有用于推荐系 统和…

Elasticsearch 集群部署插件管理及副本分片概念介绍

Elasticsearch 集群配置版本均为8以上 安装前准备 CPU 2C 内存4G或更多 操作系统: Ubuntu20.04,Ubuntu18.04,Rocky8.X,Centos 7.X 操作系统盘50G 主机名设置规则为nodeX.qingtong.org 生产环境建议准备单独的数据磁盘主机名 #各自服务器配置自己的主机名 hostnamectl set-ho…

chatgpt赋能Python-pythonf检验

Python的重要性与应用 Python是一种高级编程语言&#xff0c;因其简单易学和灵活性而备受欢迎。它已经成为数据分析、web开发、机器学习等许多领域的重要工具。在本篇文章中&#xff0c;我们将探讨Python在SEO中的作用。 Python对SEO的影响 SEO是搜索引擎优化的缩写&#xf…

【数据结构】线性表 ⑥ ( 双循环链表 | 双循环链表插入操作 | 双循环链表删除操作 | LinkedList 双循环链表源码分析 )

文章目录 一、双循环链表插入操作处理二、双循环链表删除操作处理三、LinkedList 双循环链表源码分析1、链表节点2、LinkedList 链表中收尾元素指针3、链表插入操作4、链表向指定位置插入操作5、获取指定索引的元素6、删除指定索引的元素 一、双循环链表插入操作处理 双循环链表…

【JVM】6. 堆

文章目录 6.1. 堆&#xff08;Heap&#xff09;的核心概述6.1.1. 堆内存细分6.1.2. 堆空间内部结构&#xff08;JDK7&#xff09;6.1.3. 堆空间内部结构&#xff08;JDK8&#xff09; 6.2. 设置堆内存大小与OOM6.2.1. 堆空间大小的设置6.2.2. OutOfMemory举例 6.3. 年轻代与老年…

[CTF/网络安全] 攻防世界 backup 解题详析

[CTF/网络安全] 攻防世界 backup 解题详析 PHP备份文件名备份文件漏洞成因备份文件名常用后缀姿势总结 题目描述&#xff1a;X老师忘记删除备份文件&#xff0c;他派小宁同学去把备份文件找出来,一起来帮小宁同学吧&#xff01; PHP备份文件名 PHP 脚本文件的备份文件名&#…

【瑞萨RA_FSP】外部中断

文章目录 一、外部引脚中断二、中断过程三、按键外部中断 一、外部引脚中断 1. ICU框图 根据ICU的功能框图可以知道&#xff0c;首先需要配置IRQCR寄存器(IRQ Control Register&#xff0c;IRQ英文全称&#xff1a;Interrupt ReQuest&#xff0c;中文名&#xff1a;中断请求&a…

C++入门篇---(命名空间、缺省参数、以及输入、输出)

前言 c 我来了,恭喜牛牛解锁新世界.开启c的学习之旅. &#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:讲解C…

30年后,茶产业规模是现在的10倍

做个预言&#xff1a;30年后&#xff0c;茶产业是现在的10倍 【5.21是世界茶日】 杭州中国茶博会&#xff0c;我来啦 人工智能越让生产效率越来越高 常用物质将会唾手可得 人闲着&#xff0c;无法体会活着的意义&#xff0c;才是挑战 田园诗茶生活方式会有一席之地 趣讲大白话&…

【EMC专题】案例:读一读TI的按接口选择ESD器件指南

在TI的官网上看到一份ESD by Interface Selection Guide,也就是按接口选择ESD器件指南。因此想读一读看看一起学习一下。 首先看一下文档,是比较简明的。可以看到不同的接口推荐了一些不同的保护器件。因为应用环境不一样,所有有不同的器件封装(如单体、集成TVS等),这样在…

Spyder可在线使用!?

不同安装&#xff0c;如果想使用spyder进行编程&#xff0c;可以用其在线版&#xff0c;和本地版功能一样&#xff0c;就是有点慢。 另外需要用chrome浏览器&#xff0c;用火狐没法正常访问。 Spyder可以在线使用&#xff0c;所以在没有安装python环境的电脑上&#xff0c;想…

Linux常用命令——hostname命令

在线Linux命令查询工具 hostname 显示和设置系统的主机名 补充说明 hostname命令用于显示和设置系统的主机名称。环境变量HOSTNAME也保存了当前的主机名。在使用hostname命令设置主机名后&#xff0c;系统并不会永久保存新的主机名&#xff0c;重新启动机器之后还是原来的主…

字符串匹配--BF算法和KMP算法

0.前言 字符串函数strstr相信大家都不陌生–就是在一个字符串&#xff08;主串&#xff09;中找查找另一个字符串&#xff08;子串&#xff09;&#xff0c;并返回子串在主串中的位置。那么这个函数是怎么实现的呢&#xff1f;这就涉及字符串匹配的问题&#xff0c;本章就让我们…

Node.js 事件循环和事件派发器

目录 1、process.nextTick() 介绍 2、setTimeout() 3、零延迟 4、setInterval() 5、递归setTimeout 6、setImmediate() 7、Node.js 事件派发器 1、process.nextTick() 介绍 Node.js中 process.nextTick函数以一种特殊的方式与事件循环交互。 当你试图理解Node.js事件循…