背景1Flutter性能优化实践移动端闲话编程Contents背景 .................................................................................................... 1定位问题 ............................................................................................... 2使用工具换个思路继续试让事件在单独的线程中运行 ........................................................................... 4线程模型机制方法中通信我的加密软件有一个登录页面,需要用户输入主密码然后验证密码之后才能进入。因为密码转换过程中用到了算法,而这个算法没有原生的实现,所以必须要通过插件的形式来完成,为此我还专门做了一个插件。调用插件得到秘钥这个过程大概要花个秒钟,最近在安卓真机上测试发现,这个过程中我的进度条竟然出现了卡顿,也就是说本来应该转圈圈的,结果一开始就卡住不动了,那我还需要这个加载动画干嘛呢?为此研究了一番,如何来解决这个问题。背景先还是大致介绍一下我的场景。如图所示: 定位问题2点击完成之后,期待的情况应该是显示圈圈转动的,但很明显这个地方卡住了。代码实现是是采用的作状态管理,然后使用插件调用的的原生代码。页面的部分如下:children: <Widget>[ _profileImage(), Observer( builder: (_) { return _userStore.isBusy ? _progressBar() : _input(context); }, )],然后是登录事件的处理代码@actionFuture<bool> login(ProtectedValue masterPassword) async { setBusy(); bool success = false; try { final PasswordCredential credential = await _loginService.checkUserCredential(masterPassword); _errorMessage = null; _userCredential = credential; success = true; } catch (_) { _userCredential = null; _errorMessage = "密码验证失败"; } setIdle(); return success;}定位问题使用flutter profile工具这就让我犯迷糊了,不是用了么,这么厉害来着,为毛会卡住呢?因为本身我是和的初学者,不清楚一些细节,所以一开始怀疑的是,在点击登录之后设置busy状态的这一个步骤没有生效导致的。因为本身用的是框架来处理的,很自然就开始怀疑是不是这个玩意有?或者说是我的用法有些问题?然而搜索了很久也没有找到相关的问题,一度有些不知所措。所以首先想搞清楚的是,到底是什么操作导致了卡顿呢?这时候想到可能有性能分析的工具,能不能帮助定位到具体的代码行呢?于是在模式下运行程序,这是相关的信息: 定位问题3然后还有中的对应信息:很可惜从这个分析结果中我没有能找到对我有帮助的信息,唯一只能说确定的确会卡顿换个思路继续试因为从这条路感觉已经走不通了,所以决定从其他的地方入手继续查。我注意到在登录的时候,偶尔会出现这样的日志: 让事件在单独的线程中运行4I/Choreographer(15562): Skipped 107 frames! The application may be doing too much work on its main thread.这说明啥?说明的确是卡了这时候我开始怀疑,如果在登录这个事件里面做的事情很多的情况下,即便我们用了,是不是也会卡顿呢?为了屏蔽掉其他条件的干扰,我们需要将登陆事件变得简单一些:oid onPasswordSubmitted(BuildContext context, String password) async { print('login with: ${password}'); for(int i = 0; i < 100000; i++) { }}果然发现这样一样会卡住,甚至卡的更厉害了,根本连进度圈都出不来了。从这个结果来看,说明并不能保证不会住,顺着这个思路朝下找,于是就找到问题的根本原因了。让事件在单独的线程中运行Flutter线程模型原来跟一样是一个单线程的模型,也就是是说,里面的方法并不是在多个线程中取执行的,而是通过事件机制,在单线程中完成的。那么就很容易解释卡顿的场景了,更新和事件代码是交替执行的,如果其中事件执行的部分花费了较长时间,就没办法去更新,所以界面就会卡在那里,也就是日志里面所说的Skipped 107 frames! The application may be doing too much work on its main thread.了。从的文档中可以了解到,内部有两个队列::包含所有的外部事件例如、点击、绘制等:微任务通常来自于内部或者手动插入Future.microtask(…)队列的优先级是要高于事件队列的。这里我们的登录事件和更新都同在事件队列中,很显然是因为我们的登录事件耗时太长从而掉帧了,那么解决的方案也就是,可不可以在新的线程中执行我们的事件呢?Isolate机制研究了一下发现在中不叫线程,如果想达到这种目的需要使用一个称之为Isolate的东西,大致相当于新开一个线程来处理。要使用有两种办法:使用compute方法使用Isolate.spawn(更低级的操作)下面是一个例子:Future<List<Photo>> fetchPhotos(http.Client client) async { final response = await client.get('https://jsonplaceholder.typicode.com/photos'); // Use the compute function to run parsePhotos in a separate isolate. 让事件在单独的线程中运行5 return compute(parsePhotos, response.body);}Compute方法看样子使用看似就像在中新建一个线程一样,然后就可以在线程中运行代码了。那我们直接把登录事件的处理挪进去不就得了么?然而实际情况是,这玩意并不是十分好用,有着诸多(恶心)的限制。先来看一下最简单直接的compute方法:typedef ComputeCallback<Q, R> = FutureOr<R> Function(Q message);这个方法有两个参数,一个是(相当于中的,就是你要执行的方法,另一个是,相当于是参数。这些参数有着如下的限制:必须是顶级的方法或者static方法,不能是类的实例方法或者是闭包只有一个参数,那么我的方法需要传多个参数怎么办?这就有些尴尬了,不仅要求是静态方法,还限制了参数,而我们的事件处理中有很多的依赖项,这可咋放进去呢?那么很可能只能用另一种方式了。Isolate.spawn这个就显得更为麻烦了,大致的用法是这样的:var ourFirstReceivePort = new ReceivePort(); // 需要一个ReceivePort来接收消息await Isolate.spawn(echo, ourFirstReceivePort.sendPort); // 创建一个Isolatevar echoPort = await ourFirstReceivePort.first; // 等待执行完成并接收返回值// 在Isolate中运行的代码需要将返回值通过sendPort发送过来sendPort.send(...);这个spawn方法如下:external static Future<Isolate> spawn<T>( void entryPoint(T message), T message, {bool paused: false, bool errorsAreFatal, SendPort onExit, SendPort onError, @Since("2.3") String debugName});同样有着如下的限制:两个参数,一个,另一个是这个方法的唯一参数(也就是方法必须是顶级方法或者静态方法参数必须是基本类型、或者只包含这两者的或者。这样就更加有些尴尬了,如果我们希望调用一个实例方法怎么办呢?我的登录过程中的一个关键步骤是调用,这个方法我希望能在单独的线程之中调用: 让事件在单独的线程中运行6class Argon2Kdf implements Kdf { @override Future<Uint8List> derive(Uint8List password, Uint8List salt) async { Argon2 argon2 = new Argon2(); return await argon2.argon2i(password, salt) as Uint8List; }}这个方法本身是一个实例方法,既然我们只能用顶级方法和实例方法,而且这个类也没有什么依赖,也罢,那就创建一个好了,没有问题,现在问题来了,我们需要的两个参数是和,这个参数类型不是基本类型,不支持呢,咋办呢?Isolate中通信原来不像的线程一样可以使用共享内存,而是想模型一样,只能通过消息进行通信。那么,我们需要将参数转换为支持的基本类型,通过发送过去:class Argon2Kdf implements Kdf { static final Argon2 argon2 = Argon2(iterations: 2); static void argon2Call(SendPort replyPort) async { final receivePort = ReceivePort(); replyPort.send(receivePort.sendPort); final List<dynamic> params = await receivePort.first; final SendPort reportPort1 = params[0]; final String passwordHex = params[1]; final String saltHex = params[2]; final Uint8List password = hex.decode(passwordHex) as Uint8List; final Uint8List salt = hex.decode(saltHex) as Uint8List; final Uint8List result = await argon2.argon2i(password, salt) as Uint8List; reportPort1.send(hex.encode(result)); } @override Future<Uint8List> derive(Uint8List password, Uint8List salt) async { final ReceivePort response = ReceivePort(); final isolate = await FlutterIsolate.spawn(argon2Call, response.sendPort); final SendPort sendPort = await response.first; final ReceivePort response1 = ReceivePort(); sendPort.send([response1.sendPort, hex.encode(password), hex.encode(salt)]); final String result = await response1.first; isolate.kill(); 让事件在单独的线程中运行7 return hex.decode(result); }}这里有两点需要注意:调用await response.first之后这个就自动解除订阅了,不能用来接收其他消息了不支持运行插件代码,所以用这个库来实现,而使用方法是一致的如果直接使用Isolate.spawn会有如下的报错信息E/flutter (18071): [ERROR:flutter/runtime/dart_isolate.cc(808)] Unhandled exception:E/flutter (18071): error: native function 'Window_sendPlatformMessage' (4 arguments) cannot be found这样最终的效果就是 让事件在单独的线程中运行8单线程模型