注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Flutter极简状态管理库Creator

我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。先看一下它的co...
继续阅读 »

我之前一直用riverpod来做状态管理,最近发现了一个新发布的库,尝试了一下,非常简洁好用,给大家推荐一下。叫做Creator(地址),刚发布几天就有几十个👍。

这个库的API跟riverpod很接近,但是更加简洁清晰,基本上没有什么上手难度。

先看一下它的counter例子:

// 定义状态
final counter = Creator.value(0);
Widget build(BuildContext context) {
return Column(
  children: [
    // 响应状态
    Watcher((context, ref, _) => Text('${ref.watch(counter)}')),
    TextButton(
      // 更新状态
      onPressed: () => context.ref.update<int>(counter, (count) => count + 1),
      child: const Text('+1'),
    ),
  ],
);
}

它的核心概念极其简单,只提供两种creator:

  • Creator 产生一系列的 T

  • Emitter 产生一系列的 Future<T>

这里T可以是任何类型,甚至可以是Widget。然后它把所有的creator都组织成一个有向图(叫做Ref)。

还是举一个官网的例子吧。可以在DartPad上跑,显示摄氏温度或者华氏温度:



// repo.dart

// 假装调用一个后端API。
Future<int> getFahrenheit(String city) async {
await Future.delayed(const Duration(milliseconds: 100));
return 60 + city.hashCode % 20;
}
// logic.dart

// 简单的creator
final cityCreator = Creator.value('London');
final unitCreator = Creator.value('Fahrenheit');

// 可以像Iterable/Stream那样使用 map, where, reduce 之类的.
final fahrenheitCreator = cityCreator.asyncMap(getFahrenheit);

// 组合不同的creator,产生新的业务逻辑。
final temperatureCreator = Emitter<String>((ref, emit) async {
final f = await ref.watch(fahrenheitCreator);
final unit = ref.watch(unitCreator);
emit(unit == 'Fahrenheit' ? '$f F' : '${f2c(f)} C');
});

// 摄氏华氏温度转换
int f2c(int f) => ((f - 32) * 5 / 9).round();
// main.dart

Widget build(BuildContext context) {
return Watcher((context, ref, _) =>
    Text(ref.watch(temperatureCreator.asyncData).data ?? 'loading'));
}
... context.ref.set(cityCreator, 'Pairs'); // 会调用后端API
... context.ref.set(unitCreator, 'Celsius'); // 不会调用后端API

可以看出,当用户改变所选城市之后, 状态会沿着图中的箭头传导,一直传到最后的Creator<Widget>,从而更新UI。

我觉得这个有向图的设计还是非常独特的,很好理解,也很简单。组织比较复杂的业务逻辑的时候非常方便。

这个库的核心代码才500行,感兴趣的同学可以去看官方文档和代码。

欢迎讨论!

作者:Jay_Guo
来源:juejin.cn/post/7107433326054473736

收起阅读 »

在uni-app中使用微软的文字转语音服务

前言尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格.....
继续阅读 »

前言

尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格...


但就是不能下载成mp3文件,所以有一些小伙伴逼不得已只好通过转录电脑的声音来获得音频文件,但这样太麻烦了。其实,能在网页里看到听到的所有资源,都是解密后的结果。也就是说,只要这个声音从网页里播放出来了,我们必然可以找到方法提取到音频文件。

本文就是记录了这整个探索实现的过程,请尽情享用~

本文大部分内容写于今年年初一直按在手里未发布,我深知这个方法一旦公之于众,可能很快会迎来微软的封堵,甚至直接取消网页体验的入口和相关接口。

解析Azure官网的演示功能

使用Chrome浏览器打开调试面板,当我们在Azure官网中点击播放功能时,可以从network标签中监控到一个wss://的请求,这是一个websocket的请求。


两个参数

在请求的URL中,我们可以看到有两个参数分别是AuthorizationX-ConnectionId


有意思的是,第一个参数就在网页的源码里,使用axios对这个Azure文本转语音的网址发起get请求就可以直接提取到


const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const token = RegExp.$1;
}

通过查看发起请求的JS调用栈,加入断点后再次点击播放



可以发现第二个参数X-ConnectionId来自一个createNoDashGuid的函数

this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),

这就是一个uuid v4格式的字符串,nodash就是没有-的意思。

三次发送

请求时URL里的两个参数已经搞定了,我们继续分析这个webscoket请求,从Message标签中可以看到


每次点击播放时,都向服务器上报了三次数据,明显可以看出来三次上报数据各自的作用

第一次的数据:SDK版本,系统信息,UserAgent

Path: speech.config
X-RequestId: 818A1E398D8D4303956D180A3761864B
X-Timestamp: 2022-05-27T16:45:02.799Z
Content-Type: application/json

{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}}

第二次的数据:转语音输出配置,从outputFormat可以看出来,最终的音频格式为audio-24khz-160kbitrate-mono-mp3,这不就是我们想要的mp3文件吗?!

Path: synthesis.context
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:43.340Z
Content-Type: application/json

{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}}

第三次的数据:要转语音的文本信息和角色voice name,语速rate,语调pitch,情感等配置

Path: ssml
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:49.594Z
Content-Type: application/ssml+xml

<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"><voice name="zh-CN-XiaoxiaoNeural"><prosody rate="0%" pitch="0%">我叫大帅,一个热爱编程的老程序猿</prosody></voice></speak>

接收的二进制消息

既然从前三次上报的信息已经看出来返回的格式就是mp3文件了,那么我们是不是把所有返回的二进制数据合并就可以拼接成完整的mp3文件了呢?答案是肯定的!

每次点击播放后接收的所有来自websocket的消息的最后一条,都有明确的结束标识符



turn.end代表转换结束!

用Node.js实现它

既然都解析出来了,剩下的就是在Node.js中重新实现这个过程。

两个参数

  1. Authorization,直接通过axios的get请求抓取网页内容后通过正则表达式提取

const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const Authorization = RegExp.$1;
}
  1. X-ConnectionId,直接使用uuid库即可

//npm install uuid
const { v4: uuidv4 } = require('uuid');

const XConnectionId = uuidv4().toUpperCase();

创建WebSocket连接

//npm install nodejs-websocket
const ws = require("nodejs-websocket");

const url = `wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}`;
const connect = ws.connect(url);

三次发送

第一次发送

function getXTime(){
  return new Date().toISOString();
}

const message_1 = `Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}}`;

connect.send(message_1);

第二次发送

const message_2 = `Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`;

connect.send(message_2);

第三次发送

const SSML = `
  <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
      <voice name="zh-CN-XiaoxiaoNeural">
          <mstts:express-as style="general">
              <prosody rate="0%" pitch="0%">
              我叫大帅,一个热爱编程的老程序猿
              </prosody>
          </mstts:express-as>
      </voice>
  </speak>
  `

const message_3 = `Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}`

connect.send(message_3);

接收二进制消息拼接mp3

当三次发送结束后我们通过connect.on('binary')监听websocket接收的二进制消息。

创建一个空的Buffer对象final_data,然后将每一次接收到的二进制内容拼接到final_data里,一旦监听到普通文本消息中包含Path:turn.end标识时则将final_data写入创建一个mp3文件中。

let final_data=Buffer.alloc(0);
connect.on("text", (data) => {
  if(data.indexOf("Path:turn.end")>=0){
      fs.writeFileSync("test.mp3",final_data);
      connect.close();
  }
})
connect.on("binary", function (response) {
  let data = Buffer.alloc(0);
  response.on("readable", function () {
      const newData = response.read()
      if (newData)data = Buffer.concat([data, newData], data.length+newData.length);
  })
  response.on("end", function () {
      const index = data.toString().indexOf("Path:audio")+12;
      final_data = Buffer.concat([final_data,data.slice(index)]);
  })
});

这样我们就成功的保存出了mp3音频文件,连Azure官网都不用打开!

命令行工具

我已经将整个代码打包成一个命令行工具,使用非常简单

npm install -g mstts-js
mstts -i 文本转语音 -o ./test.mp3

已全部开源: github.com/ezshine/mst…

在uni-app中使用

新建一个云函数

新建一个云函数,命名为mstts


由于mstss-js已经封装好了,只需要在云函数中npm install mstts-js然后require即可,代码如下

'use strict';
const mstts = require('mstts-js')

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
});

下载播放mp3文件

要在uniapp中播放这个mp3格式的文件,有两种方法

方法1. 先上传到云存储,通过云存储地址访问

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
  var uploadRes = await uniCloud.uploadFile({
      cloudPath: "xxxxx.mp3",
      fileContent: res
  })
   
  return uploadRes.fileID;
});

前端用法:

uniCloud.callFunction({
  name:"mstts",
  success:(res)=>{
      const aud = uni.createInnerAudioContext();
      aud.autoplay = true;
      aud.src = res;
      aud.play();
  }
})
  • 优点:云函数安全

  • 缺点:文件上传到云存储不做清理机制的话会浪费空间

方法2. 利用云函数的URL化+集成响应来访问

这种方法就是直接将云函数的响应体变成一个mp3文件,直接通过audio.src赋值即可访问`

exports.main = async (event, context) => {
const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');

return {
mpserverlessComposedResponse: true,
isBase64Encoded: true,
statusCode: 200,
headers: {
'Content-Type': 'audio/mp3',
'Content-Disposition':'attachment;filename=\"temp.mp3\"'
},
body: res.toString('base64')
}
};

前端用法:

const aud = uni.createInnerAudioContext();
aud.autoplay = true;
aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts';
aud.play();
  • 优点:用起来很简单,无需保存文件到云存储

  • 缺点:URL化后的云函数如果没有安全机制,被抓包后可被其他人肆意使用

作者:大帅老猿
来源:juejin.cn/post/7103720862221598757

收起阅读 »

节日献礼:Flutter图片库重磅开源!

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完...
继续阅读 »

背景:

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

简介:

PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的flutter图片库。我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。

能力特点:

  • 支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。

  • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。

  • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。

  • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。

  • 完善自定义图片类型通道。解决业务自定义图片获取诉求。

  • 完善的异常捕获与收集。

  • 支持动图。(来自淘特的PR)

Flutter 原生方案:

在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。


原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image

  • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。

  • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。

  • ImageStream:图片资源加载的对象。

在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

新一代方案:

我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

FFI:

正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image

首先 native 侧先获取必要的参数(以 iOS 为例):

_rowBytes = CGImageGetBytesPerRow(cgImage);
CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);
NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;

dart 侧拿到后

@override  FutureOr createImageInfo(Map map) {
Completer completer = Completer();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat = ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);     //释放 native 内存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

  1. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。

  2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

Texture:

Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:

问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理

都有解决方案:

问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:

import 'dart:typed_data';
import 'dart:
ui'
as ui show Image;
import 'dart:ui';
class TextureImage implements ui.Image {
    int _width;
    int _height;
    int textureId;
   TextureImage(this.textureId, int width, int height)     : _width = width,       _height = height;
    @override void dispose() {
    // TODO: implement dispose }
     @override int get height => _height;
     @override Future
toByteData(     {ImageByteFormat format = ImageByteFormat.rawRgba}) {  
         // TODO: implement toByteData  
             throw UnimplementedError();
     }
     @override int get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE 与 PR。

问题三:关于 native 侧感知 flutter image 释放时机的问题

修改的 ImageCache 释放如下(部分代码):

typedef void HasRemovedCallback(dynamic key, dynamic value);

class RemoveAwareMap<K, V> implements Map<K, V> {
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if (key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if (isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if (!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if (key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}

整体架构:

我们将两种解决方案非常优雅地结合在了一起:


我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。

蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。

蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。

这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。

除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。

数据:

FFI vs Texture:


机型:iPhone 11 Pro;图片:300 张网络图;行为:在listView中手动滚动到底部再滚动到顶部;native Cache20 maxMemoryCount; flutter Cache30MBflutter version 2.5.3; release 模式下

这里有两个现象:

FFI:   186MB波动Texture: 194MB波动

在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。

图中棋格图,为打开 checkerboardRasterCacheImages 后所展示,可以看出,ffi方案会缓存整个cell,而texture方案,只有cell中的文字被缓存,RasterCache 会使得 ffi 在流畅度方面会有一定优势。

滚动流畅性分析:


设备: Android OnePlus 8t,CPU和GPU进行了锁频。case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。
复制代码

结论:

  • UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。

  • Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

更精简的代码:


dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

单测:


为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。

关于开源:

我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。

Issues:

关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。


对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue

我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。

PR:

为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。

在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。


得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。

未来:

开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift···

PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。

PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。

其他四个Flutter开源项目: 闲鱼技术**公众号-闲鱼开源

PowerImage相关链接:

GitHub:(✅star🌟)

github.com/alibaba/pow…

Flutter pub:(✅like👍)

pub.dev/packages/po…

作者:闲鱼技术——新宿

收起阅读 »

为了看Flutter到底有没有人用我竟然

首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。github.com/zhaobozhen/…github.com/sugood/apka…App列...
继续阅读 »

Flutter这个东西出来这么久了,到底市场占有率怎么样呢?为了让大家了解这一真实数据,也为了让大家了解当前Flutter在各大App中的使用情况,我今天下载了几百个App,占了手机将近80G空间,就为了得出一个结论——Flutter,到底有没有人用。

首先,我在vivo应用市场中,下载了4月11日软件排行榜中的所有App,总计230个,再加上平时用的比较多的一些App,总共270个App,作为我们的统计基数。

检测方法,我使用LibChecker来查看App是否有使用Flutter相关的so。

github.com/zhaobozhen/…

除了使用LibChecker之外,还有其它方案也可以,例如使用shell指令——zipinfo。

github.com/sugood/apka…

Apk本质上也是一种压缩包,所以,通过zipinfo指令并进行grep,就可以很方便的获取了,同时,如果配合一下爬虫来爬取应X宝的Apk下载地址,就可以成为一个全自动化的脚本分析工具,这里没这么强的需求,所以就不详细做了。

App列表

我们来看下,我都下载了多少App。

Screenshot_2022-04-12-09-45-44-13_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-47-46_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-49-64_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-45-51-75_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-53-78_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-45-55-92_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-45-58-12_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-00-27_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-02-34_92b64b2a7aa6eb3771ed6e18d0029815
Screenshot_2022-04-12-09-46-04-34_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-06-60_92b64b2a7aa6eb3771ed6e18d0029815Screenshot_2022-04-12-09-46-09-14_92b64b2a7aa6eb3771ed6e18d0029815

这些App基本上已经覆盖了应用商店各个排行榜里的Top软件,所以应该还是比较具有代表性和说服力的。

下面我们就用LibChecker来看下,这些App里面到底有多少使用了Flutter。

统计结果

Screenshot_2022-04-12-09-51-25-73_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-34-94_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-51-39-66_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-44-41_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-51-49-75_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-51-58-19_708f76cdf2c7449ff16a8486e0e036f6
Screenshot_2022-04-12-09-52-04-67_708f76cdf2c7449ff16a8486e0e036f6Screenshot_2022-04-12-09-52-13-25_708f76cdf2c7449ff16a8486e0e036f6

已经使用Flutter的App共52个,占全体样本的19.2%,作为参考,统计了下RN相关的App,共有45个,占全体样本的16.6%,可以说,Flutter已经超过RN成为跨平台方案的首选。

在52个使用Flutter的App中:

  • 腾讯系:QQ邮箱、微信、QQ同步助手、蓝盾、腾讯课堂、QQ浏览器、微视、企业微信、腾讯会议

  • 百度系:百度网盘、百度输入法

  • 阿里系:优酷视频、哈啰出行、淘特、酷狗直播、阿里1688、学习强国、钉钉、淘宝、闲鱼

  • 其它大厂:链家、转转、智联招聘、拍拍贷、哔哩哔哩漫画、网易有道词典、爱奇艺、考拉海购、携程旅行、微博、Soul、艺龙旅行、唯品会、飞猪旅行

从上面的数据来看,各大厂都对Flutter有使用,头条系未列出的原因是,目前好像只有头条系大规模使用了Flutter的动态化加载方案,所以原始包内找不到Flutter相关的so,所以未检出(猜测是这样,具体可以请头条系的朋友指出,根据上次头条的分享,内部有90+App在使用Flutter)。

不过这里要注意的 ,这里并不是选取的大家常用的一些APP来做测试的,而是直接选取的排行榜,如果直接用常用APP来测试,那比例可能更高,大概统计了下,估计在60%左右。

不过大厂里面,京东没有使用Flutter我还是比较意外的,看了下京东的几个App,目前还是以RN为主作为跨平台的方案。这跟其它很多大厂一样,它们不仅使用了Flutter,RN也还可以检出,这也从侧面说明了,各个厂商,对跨平台的方案探索,从未停止。

所以,总结一下,目前使用Flutter的团队的几个特定:

  • 创业公司:快速试错、快速开发,像Blued、夸克这也的

  • 大厂:大厂的话题永远是效率,如何利用跨平台技术来提高开发效率,是它们引入Flutter的根本原因

  • 创新型业务:例如B漫、淘特、Soul这类没有太多历史包袱的新业务App,可以利用Flutter进行极为高效的开发

所以,整体在知乎上吵「Flutter被抛弃了」、「Flutter要崛起了」,有什么意义呢?所有的争论都抵不过数据来的真实。

嘴上说着不要,身体倒是很诚实。

希望这份数据能给你一些帮助。


作者:xuyisheng
来源:juejin.cn/post/7088864824284676110


收起阅读 »

【 Flutter 极限测试】连续 1000000 次 setState 会怎么样

测试描述可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?本篇文章将做一个极限测试,看一下连续触发 1000000 次 setState 会发生...
继续阅读 »
测试描述

可能很多人会认为,每次的 State#setState 都会触发当前状态类的 build 方法重新构建。但真的是这样吗,你真的了解 Flutter 界面的更新流程吗?

本篇文章将做一个极限测试,看一下连续触发 1000000setState 会发生什么?是连续触发 1000000 次屏幕更新,导致界面卡死,还是无事发生?用你的眼睛来见证吧!


1、测试代码说明

如下所示,在默认案例基础上添加了两个蓝色文字,点击时分别触发如下的 _increment1_setState1000000 。其中 _setState1000000 是遍历执行 1000000setState


void _increment1() {
 setState(() {
   _counter++;
});
}

void _setState1000000() {
 for (int i = 0; i < 1000000; i++) {
   setState(() {
     _counter++;
  });
}
}

2、运行结果

如下是在 profile 模式下,网页调试工具中的测试结果。可以看出即使连续触发了 1000000 次的 steState ,也不会有 1000000 次的帧触发来更新界面。也就是说,并非每次的 steState 方法触发时,都会进行重新构建,所以,你真的懂 State#steState 吗?



3. 源码调试分析

如下,在 State#setState 源码中可以看出,它只做了两件事:

  • 触发入参回调 fn 。

  • 执行持有元素的 markNeedsBuild 方法。


这里 1121 行的 fn() 做了什么,不用多说了吧。就是 setState 入参的那个自加方法。



此时该 State 中持有的 _element 对象类型是 StatefulEmement ,也就是 MyHomePage 组件创建的元素。



Elememt#markNeedsBuild 方法中没有一个非常重要的判断,那就是下面 4440 行 中,如果 dirty 已经是 true 时,则直接返回,不会执行接下来的方法。如果 dirtyfalse ,那接下来会置为 true

另外,owner.scheduleBuildFor 用于收集脏元素,以及申请新帧的触发。这就是为什么连续执行 1000000stateState 时,该元素不会加入脏表 1000000 次,不会触发 1000000 帧的原因。


总的来说, State#setState 的核心作用就是把持有的元素标脏申请新帧调度。而只有新帧到来,执行完构建之后,元素的 dirty 才会置为 false 。也就是说,两帧之间,无论调用多少次 setState ,都只会触发一次, 元素标脏申请新帧调度 。这就是为什么连续触发 1000000 次,并无大事发生的原因。


作者:张风捷特烈
来源:https://juejin.cn/post/7091471603774521352

收起阅读 »

【Flutter】Dart语法之List & Map

【Flutter】学习笔记——Dart中的List & Map的使用 list列表,相当于 OC 中的 NSArray 数组,分为可变和不可变两种。 map键值对,相当于 OC 中的 NSDicti...
继续阅读 »
【Flutter】学习笔记——Dart中的List & Map的使用



  • list列表,相当于 OC 中的 NSArray 数组,分为可变不可变两种。

  • map键值对,相当于 OC 中的 NSDictionary 字典,也分为可变不可变两种。



1. list数组



list默认都是可变的,列表中可以添加不同数据类型的数据。



1.1 可变list

void main() { 
// 直接 list创建
List a = ["1", 2, "3.0", 4.0];
print(a);

// var 创建
var list = [1, 2, "zjp", 3.0];
print(list);
}

运行结果如下:


image.png


1.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(list[3]);
// 直接修改指定下标数据
list[3] = "reno";

插入数据:


list.insert(1, "hellow"); // list.insert(index, element)
print(list);

删除数据:


list.remove(1); // list.remove(element)
print(list);

清空所有数据:


list.clear();
print(list);

运行结果如下:


image.png


1.3 排序和截取

void main() {
List b = [3, 4, 5, 8, 6, 7];
// 排序
b.sort();
print(b);
// 截取
print(b.sublist(1, 3));
}

运行结果如下:


image.png


1.4 不可变list


不可变的 list 需要使用const修饰。



void main() {
List b = const [3, 4, 5, 8, 6, 7];
b[3] = 10; // 报错
}

不可变list不能修改其元素值,否则会报错


image.png


2. map键值对



map默认也是可变的。



2.1 可变map

void main() {
Map a = {"a": 1, "b": 2};
print(a);

var a1 = {"a1": 1, "a2": 2};
print(a1);
}

运行结果如下:


image.png


2.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(a["a"]);
// 直接修改指定下标数据
a["a"] = "aa";
print(a["a"]);

获取map长度


print(a.length);

获取map所有的key


print(a.keys);

获取map所有的value


print(a.values);

运行结果如下:


image.png


2.3 不可变map


不可变的 map 也是使用const修饰。



void main() {
Map a = const {"a": 1, "b": 2};
a["a"] = 10; // 报错
}

不可变map不能修改其元素值,否则也会报错


image.png


3. list转map


void main() {
List b = ["zjp", "reno"];
print(b.asMap());
}

运行结果如下:



作者:忻凯同学
链接:https://juejin.cn/post/7085642932761395237
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter实现心碎的感觉

前言 继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。 效果图先上: 1、绘制一个心 首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径pat...
继续阅读 »

前言


继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。


效果图先上:


1、绘制一个心


首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径path进行绘制出来,效果:


image.png

绘制代码:


canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.close();
Path path2 = Path();
canvas.save();
canvas.drawPath(
path,
paint
..color = Colors.red
..style = PaintingStyle.stroke);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.close();
canvas.drawPath(
path2,
paint..color = Colors.black87);

2、绘制心的裂痕


我们看到心确实分成两半了,但是中间还缺少裂痕,接下来我们就绘制心碎的裂痕,也很简单,在两段路径path闭合前进行绘制线,效果:


image.png


绘制代码:


path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);

path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);

OK,我们已经看到心已经有了裂痕,如何心碎,只需将画布进行翻转一定角度即可,这里我们将画布翻转45°,看下效果:

左边:
image.png

右边:
image.png


3、加入动画


已经有心碎的感觉了,接下来加入动画元素让心碎的过程动起来。

思路: 我们可以想一下,心碎的过程是什么样子,心的颜色慢慢变灰,心然后慢慢裂开,下方的动画运动曲线看起来更符合心碎的过程,里面有不舍,不甘,但最后心还是慢慢的碎了。
xinsui.gif


我们把画笔进行填充将这个动画加入进来看下最终效果。

df5dbcbb-f36b-4f05-9613-0e94149d888f.gif
是不是心碎了一地。


知识点: 这里我们需要找到红色和灰色的RGB色值,通过Color.fromRGBO(r, g, b, opacity)方法赋值颜色的色值。然后通过动画值改变RGB的值即可。
这里我使用的色值是:

红色:Color.fromRGBO(255, 0, 0, 1)

灰色:Color.fromRGBO(169, 169, 169, 1)


最终代码:


class XinSui extends StatefulWidget {
const XinSui({Key? key}) : super(key: key);

@override
_XinSuiState createState() => _XinSuiState();
}

class _XinSuiState extends State<XinSui> with SingleTickerProviderStateMixin {
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 4000))
..repeat();
late CurvedAnimation cure =
CurvedAnimation(parent: _controller, curve: Curves.bounceInOut);

late Animation<double> animation =
Tween<double>(begin: 0.0, end: 1.0).animate(cure);

@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
size: Size(double.infinity, double.infinity),
painter: _XinSuiPainter(animation),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

class _XinSuiPainter extends CustomPainter {
Animation<double> animation;

_XinSuiPainter(this.animation) : super(repaint: animation);

@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);
path.close();
Path path2 = Path();
canvas.save();
canvas.rotate(-pi / 4 * animation.value);
canvas.drawPath(
path,
paint
..color = Colors.red
..color = Color.fromRGBO(
255 - (86 * animation.value).toInt(),
(animation.value * 169).toInt(),
(animation.value * 169).toInt(),
1)

..style = PaintingStyle.fill);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);
path2.close();
canvas.rotate(pi / 4 * animation.value);
canvas.drawPath(
path2,paint);
}
@override
bool shouldRepaint(covariant _XinSuiPainter oldDelegate) {
return oldDelegate.animation != animation;
}
}

小结


动画曲线Curves配合绘制可以实现很多好玩的东西,这个需要勤加练习方能掌握,仅将此心碎献给今天受伤的股民朋友们(ಥ﹏ಥ)


作者:老李code
来源:https://juejin.cn/post/7090457954415017991
收起阅读 »

Flutter实现一个牛顿摆

前言牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。 - 知识点:绘制、动画曲线、多动画状态更新 效果图: 1、绘制静态效果 首先我们需要把线和小圆球绘制出来,...
继续阅读 »

前言

  • 牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。

- 知识点:绘制、动画曲线、多动画状态更新


效果图:


638bdf30-7b2a-4c3e-ad14-94da128b68f1.gif


1、绘制静态效果


首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:

 
image.png

关键代码:


// 小圆球半径
double radius = 6;

/// 小球圆心和直线终点一致
//左边小球圆心
Offset offset = Offset(20, 60);
//右边小球圆心
Offset offset2 = Offset(20 * 6 * 8, 60);

Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制小圆球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);

2、加入动画


思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。


两个关键点


小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。


运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。

下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。

 
ndb.gif

完整源码:


class OvalLoading extends StatefulWidget {
const OvalLoading({Key? key}) : super(key: key);

@override
_OvalLoadingState createState() => _OvalLoadingState();
}

class _OvalLoadingState extends State
with TickerProviderStateMixin
{
// 左边小球
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
_controller2.forward();
}
})
..forward();
// 右边小球
late AnimationController _controller2 =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller2.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
// 反向执行完毕左边小球执行
_controller.forward();
}
});
late var cure =
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
late var cure2 =
CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic);

late Animation animation = Tween(begin: 0.0, end: 1.0).animate(cure);

late Animation animation2 =
Tween(begin: 0.0, end: 1.0).animate(cure2);

@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsetsDirectional.only(top: 300, start: 150),
child: CustomPaint(
size: Size(100, 100),
painter: _OvalLoadingPainter(
animation, animation2, Listenable.merge([animation, animation2])),
),
);
}

@override
void dispose() {
_controller.dispose();
_controller2.dispose();
super.dispose();
}
}

class _OvalLoadingPainter extends CustomPainter {
double radius = 6;
final Animation animation;
final Animation animation2;
final Listenable listenable;

late Offset offset; // 左边小球圆心
late Offset offset2; // 右边小球圆心

final double lineLength = 60; // 线长

_OvalLoadingPainter(this.animation, this.animation2, this.listenable)
: super(repaint: listenable) {
offset = Offset(20, lineLength);
offset2 = Offset(20 * radius * 8, lineLength);
}

// 摆动角度
double angle = pi / 180 * 30; // 30°

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

// 左边小球 默认坐标 下方是90度 需要+pi/2
var dx = 20 + 60 * cos(pi / 2 + angle * animation.value);
var dy = 60 * sin(pi / 2 + angle * animation.value);
// 右边小球
var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value);
var dy2 = 60 * sin(pi / 2 + angle * animation2.value);

offset = Offset(dx, dy);
offset2 = Offset(dx2, dy2);

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(
Offset(20 + radius * 2, 60),
radius,
paint);

canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);
}
@override
bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

去掉线的效果:

b6a23e8a-9c4a-4aa8-9518-46a53b756a88.gif


总结


本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点,本篇只展示了实现的原理,希望对大家有一些帮助~


作者:老李code
来源:https://juejin.cn/post/7090123854135164935 收起阅读 »

在Flutter上优雅的请求网络数据

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。 解决的问题通用异常处理请求资源状态可见(加载成功,加载中,加载失败)通用重试逻辑 效果展示 为了演示请求失败的处理,特意在wa...
继续阅读 »

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。


解决的问题

  • 通用异常处理
  • 请求资源状态可见(加载成功,加载中,加载失败)
  • 通用重试逻辑


效果展示


为了演示请求失败的处理,特意在wanApi抛了两次错
LBeZ5Q.gif


正文


搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo


定义数据源


使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库

class _WanApi implements WanApi {
_WanApi(this._dio, {this.baseUrl}) {
baseUrl ??= 'https://www.wanandroid.com';
}

final Dio _dio;

String? baseUrl;

@override
Future<BannerModel> getBanner() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BannerModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/banner/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BannerModel.fromJson(_result.data!);
return value;
}

@override
Future<TopArticleModel> getTopArticle() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<TopArticleModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/article/top/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = TopArticleModel.fromJson(_result.data!);
return value;
}

@override
Future<PopularSiteModel> getPopularSite() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<PopularSiteModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/friend/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = PopularSiteModel.fromJson(_result.data!);
return value;
}

RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
}



repository封装


Resource是封装的资源加载状态类,用于包装资源


enum ResourceState { loading, failed, success }

class Resource {
final T? data;
final ResourceState state;
final dynamic error;
Resource._({required this.state, this.error, this.data});

factory Resource.failed(dynamic error) {
return Resource._(state: ResourceState.failed, error: error);
}

factory Resource.success(T data) {
return Resource._(state: ResourceState.success, data: data);
}

factory Resource.loading() {
return Resource._(state: ResourceState.loading);
}

bool get isLoading => state == ResourceState.loading;
bool get isSuccess => state == ResourceState.success;
bool get isFailed => state == ResourceState.failed;
}

接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream> homeData() async* {
//加载中
yield Resource.loading();
try {
var result = await Future.wait([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
} catch (e) {
//加载失败
yield Resource.failed(e);
}
}
}

咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream> homeData() async* {
///定义加载函数
loadHomeData()async*{
var result = await Future.wait([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
///将加载函数放在一个包装器里执行
yield* MyWrapper.customStreamWrapper(loadHomeData);
}
}

得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能)
包装器代码:


class MyWrapper {
//流的方式
static Stream> customStreamWrapper(
Stream> Function() function,
{bool retry = false}) async* {
yield Resource.loading();
try {
var result = function.call();
await for(var data in result)
{
yield data;
}
} catch (e) {
//重试代码
if (retry) {
var toRetry = await Get.dialog(const RequestRetryDialog());
if (toRetry == true) {
yield* customStreamWrapper(function,retry: retry);
}
else
{
yield Resource.failed(e);
}
} else {
yield Resource.failed(e);
}
}
}
}

其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:


@Repo()
abstract class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();

///获取首页所需的所有数据
@ProxyCall()
@Retry()
Stream> homeData() async* {
var result = await Future.wait(
[wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
}

是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:


// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'wan_repository.dart';

// **************************************************************************
// RepositoryGenerator
// **************************************************************************

class WanRepositoryImpl = WanRepository with _WanRepository;

mixin _WanRepository on WanRepository {
@override
Stream> homeData() {
return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
}
}

结语


感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考


附demo地址:gitee.com/cysir/examp…


flutter版本:2.8


作者:贼不走空
链接:https://juejin.cn/post/7088223867017101343
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter bottomSheet 高度自适应及溢出处理

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应 先放上显示弹窗的代码,如下: Future<T?> showSheet<T>( BuildContext context, Widg...
继续阅读 »

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应



先放上显示弹窗的代码,如下:


Future<T?> showSheet<T>(
BuildContext context,
Widget body, {
bool scrollControlled = false,
Color bodyColor = Colors.white,
EdgeInsets? bodyPadding,
BorderRadius? borderRadius,
}) {
const radius = Radius.circular(16);
borderRadius ??= const BorderRadius.only(topLeft: radius, topRight: radius);
bodyPadding ??= const EdgeInsets.all(20);
return showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: bodyColor,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
barrierColor: Colors.black.withOpacity(0.25),
// A处
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height - MediaQuery.of(context).viewPadding.top),
isScrollControlled: scrollControlled,
builder: (ctx) => Padding(
padding: EdgeInsets.only(
left: bodyPadding!.left,
top: bodyPadding.top,
right: bodyPadding.right,
// B处
bottom: bodyPadding.bottom + MediaQuery.of(ctx).viewPadding.bottom,
),
child: body,
));
}


其中,A处、B处的作用就是,让弹窗的内容始终显示在安全区域内



高度自适应问题


首先,我们在弹窗中显示点内容:


showSheet(context, Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

效果如下图所示:




此时,我们只需要将显示内容的代码改为如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min, // 这一行是关键所在
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

现在的效果图如下:




现在我们可以看到,弹窗的高度已经根据内容自适应了。


内容溢出问题


前面的解决方式,仅在内容高度小于默认高度时有效。当内容过多,高度大于默认高度时,就会出现溢出警告,如下图所示:




此时,我们该怎么办呢?


答案是:运用 showModalBottomSheet 的 isScrollControlled 参数,将其设置为true即可,代码如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
Text('这是很长长..(此处省略若干字)..的一段话,')
],
), scrollControlled: true); // 这一行用于告诉系统,弹窗的内容完全由我们自己管理

此时,效果图如下:



showSheet 补充说明


对前面showSheet代码中,A处、B处的进一步说明:


A处:如果不对内容的高度进行限制,则内容会显示在状态栏之后,而引起用户交互问题。如下图所示:



B处:如果不加 MediaQuery.of(ctx).viewPadding.bottom 这一句,则内容有可能会显示在底部横条的下方,此时也不利于交互


最终版本图样


内容较少(高度跟随内容自适应):



内容很多(顶部、底部均显示在安全区域内):



作者:JameLee
链接:https://juejin.cn/post/7081475476765556749
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

2022了,来体验下 flutter web

前言 flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。 最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持...
继续阅读 »

前言


flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。
最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持AOT(ahead of time: 提前编译),开发阶段使用JIT模式提高时效性,同时在发布阶段使用AOT模式提高编译性能。
作为前端的话,还是更关注flutter web的支持情况。为了体验flutter web,特意用flutter写了个小游戏来看编译后的代码在web上运行如何。


开始之前


早在3年前的 19年初 1.0 出来没多久的时候就尝试用flutter来写一些常见的菜单注册登录等页面demo来,那时候flutter的生态还在发展中,除了官方提供的一些解决方案,三方的一些包很多都不成体系,应用范围较小,由于当时是抱着前端的固有思路来尝鲜flutter,flutter 刚发展起来,轮子远没有那么多,发现写起来远没有Vue、React 这类生态成熟的框架写起来舒服,除了 widget 组件多,写起UI来可以直接看文档写完很方便外,网络请求,路由管理、状态管理(这些像vue有axios/vue-router/vuex)用官方的方法写起来相当麻烦(也可能是我不会用,对新手不友好),维护起来就更麻烦了。


过去3年了,再看flutter,2.0版本发布也快一年了,当再次想用flutter写个demo的时候,发现了社区已经出现了一些经过几年发展的provider、getx之类的状态管理框架,能帮助新手快速入门,用了 getx 感觉是个脚手架,又不仅仅是脚手架,简直是大而全的轮子,状态管理、路由管理一应俱全,生成的目录结构清晰,你只需要去填充 UI 和处理数据。用法也很简单,对新手很友好。


flutter + getx 写一个小游戏


既然选好了那就用 getx 生成项目目录,开始开发,选用了一个很常见的小游戏:数字华容道,功能也简单。 项目地址


项目可以打包成原生应用,也可以打包成 web 应用


数字华容道web版


flutter web 渲染模式


不同的渲染器在不同场景下各有优势,因此 Flutter 同时支持以下两种渲染模式:


HTML 渲染器: 结合了 HTML 元素、CSS、Canvas 和 SVG。该渲染模式的下载文件体积较小。
CanvasKit 渲染器: 渲染效果与 Flutter 移动和桌面端完全一致,性能更好,widget 密度更高,但增加了约 2MB 的下载文件体积。
为了针对每个设备的特性优化您的 Flutter web 应用,渲染模式默认设置为自动。这意味着您的应用将在移动浏览器上使用 HTML 渲染器运行,在桌面浏览器上使用 CanvasKit 渲染器运行。官方文档




使用 HTML 渲染


flutter run -d chrome --web-renderer html
复制代码

使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小,元素数量多,请求都是http2


元素如下



请求如下



使用 CanvasKit 渲染


CanvasKit 是以 WASM 为编译目标的Web平台图形绘制接口,其目标是将 Skia 的图形 API 导出到 Web 平台。


flutter run -d chrome --web-renderer canvaskit
复制代码

默认 CanvasKit 渲染,元素数量比html少很多,就是需要请求 canvaskit.wasm,该文件大小7MB左右、默认在 unpkg.com 国内加载速度慢,可以将文件放到国内 cdn 以提升请求效率



元素如下



请求如下,部分还使用了http3



小结


flutter web 通过编译成浏览器可运行的代码,经实践来看,性能还是有些问题,不过如果是单单想要写SPA,那恐怕还是js首选。目前来说flutter的生态经过几年的发展已经有了很多开源轮子,但要说稳定性还无法击败js,要不要用 flutter web 就要根据实际需求来决定了。


作者:c_137Summer
链接:https://juejin.cn/post/7072280090066812941
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

面试官:知道 Flutter 生命周期?下周来入职!

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里...
继续阅读 »

作为一名移动端开发工程师,刚接触 Flutter 的时候,一定会有这样的疑问:Flutter 的生命周期是怎么样的?是如何处理生命周期的?我的 onCreate()[Android] 在哪里?viewDidLoad()[iOS] 呢? 我的业务逻辑应该放在哪里处理?初始化数据呢?希望看了这篇文章后,可以对你有一点小小的帮助。


安卓


如果你是一名安卓开发工程师,那么对于 Activity 生命周期肯定不陌生



  • onCreate

  • onStart

  • onResume

  • onPause

  • onStop

  • onDestroy


android_life_cycle


iOS


如果你是一名 iOS 开发工程师,那么 UIViewController 的生命周期肯定也已经很了解了。



  • viewDidLoad

  • viewWillAppear

  • viewDidAppear

  • viewWillDisappear

  • viewDidDisappear

  • viewDidUnload


ios_life_cycle


Flutter


知道了 Android 和 iOS 的生命周期,那么 Flutter 呢?有和移动端对应的生命周期函数么?如果之前你对 Flutter 有一点点了解的话,你会发现 Flutter 中有两个主要的 Widget:StatelessWidget(无状态)StatefulWidget(有状态)。本篇文章我们主要来介绍下 StatefulWidget,因为它有着和 Android 和 iOS 相似的生命周期。


StatelessWidget


无状态组件是不可变的,这意味着它们的属性不能变化,所有的值都是最终的。可以理解为将外部传入的数据转化为界面展示的内容,只会渲染一次。
对于无状态组件生命周期只有 build 这个过程。无状态组件的构建方法通常只在三种情况下会被调用:小组件第一次被插入树中,小组件的父组件改变其配置,以及它所依赖的 InheritedWidget 发生变化时。


StatefulWidget


有状态组件持有的状态可能在 Widget 生命周期中发生变化,是定义交互逻辑和业务逻辑。可以理解为具有动态可交互的内容界面,会根据数据的变化进行多次渲染。实现一个 StatefulWidget 至少需要两个类:



  • 一个是 StatefulWidget 类。

  • 另一个是 Sate 类。StatefulWidget 类本身是不可变的,但是 State 类在 Widget 生命周期中始终存在。StatefulWidget 将其可变的状态存储在由 createState 方法创建的 State 对象中,或者存储在该 State 订阅的对象中。


StatefulWidget 生命周期



  • createState:该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被创建时会立即执行 createState。createState 函数执行完毕后表示当前组件已经在 Widget 树中,此时有一个非常重要的属性 mounted 被置为 true。

  • initState:该函数为 State 初始化调用,只会被调用一次,因此,通常会在该回调中做一些一次性的操作,如执行 State 各变量的初始赋值、订阅子树的事件通知、与服务端交互,获取服务端数据后调用 setState 来设置 State。

  • didChangeDependencies:该函数是在该组件依赖的 State 发生变化时会被调用。这里说的 State 为全局 State,例如系统语言 Locale 或者应用主题等,Flutter 框架会通知 widget 调用此回调。类似于前端 Redux 存储的 State。该方法调用后,组件的状态变为 dirty,立即调用 build 方法。

  • build:主要是返回需要渲染的 Widget,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次而导致状态异常。

  • reassemble:主要在开发阶段使用,在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。此回调在 release 模式下永远不会被调用。

  • didUpdateWidget:该函数主要是在组件重新构建,比如说热重载,父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。

  • deactivate:在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。

  • dispose:永久移除组件,并释放组件资源。调用完 dispose 后,mounted 属性被设置为 false,也代表组件生命周期的结束。


不是生命周期但是却非常重要的几个概念


下面这些并不是生命周期的一部分,但是在生命周期中起到了很重要的作用。



  • mounted:是 State 中的一个重要属性,相当于一个标识,用来表示当前组件是否在树中。在 createState 后 initState 前,mounted 会被置为 true,表示当前组件已经在树中。调用 dispose 时,mounted 被置为 false,表示当前组件不在树中。

  • dirty:表示当前组件为脏状态,下一帧时将会执行 build 函数,调用 setState 方法或者执行 didUpdateWidget 方法后,组件的状态为 dirty。

  • clean:与 dirty 相对应,clean 表示组件当前的状态为干净状态,clean 状态下组件不会执行 build 函数。


stateful_widget_lifecycle 生命周期流程图


上图为 flutter 生命周期流程图


大致分为四个阶段



  1. 初始化阶段,包括两个生命周期函数 createState 和 initState;

  2. 组件创建阶段,包括 didChangeDependencies 和 build;

  3. 触发组件多次 build ,这个阶段有可能是因为 didChangeDependencies、 setState 或者 didUpdateWidget 而引发的组件重新 build ,在组件运行过程中会多次触发,这也是优化过程中需要着重注意的点;

  4. 最后是组件销毁阶段,deactivate 和 dispose。


组件首次加载执行过程


首先我们来实现下面这段代码(类似于 flutter 自己的计数器项目),康康组件首次创建是否按照上述流程图中的顺序来执行的。



  1. 创建一个 flutter 项目;

  2. 创建 count_widget.dart 中添加以下代码;


import 'package:flutter/material.dart';

class CountWidget extends StatefulWidget {
CountWidget({Key key}) : super(key: key);

@override
_CountWidgetState createState() {
print('count createState');
return _CountWidgetState();
}
}

class _CountWidgetState extends State<CountWidget> {
int _count = 0;
void _incrementCounter() {
setState(() {
print('count setState');
_count++;
});
}

@override
void initState() {
print('count initState');
super.initState();
}

@override
void didChangeDependencies() {
print('count didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(CountWidget oldWidget) {
print('count didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('count deactivate');
super.deactivate();
}

@override
void dispose() {
print('count dispose');
super.dispose();
}

@override
void reassemble() {
print('count reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('count build');
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'$_count',
style: Theme.of(context).textTheme.headline4,
),
Padding(
padding: EdgeInsets.only(top: 100),
child: IconButton(
icon: Icon(
Icons.add,
size: 30,
),
onPressed: _incrementCounter,
),
),
],
),
);
}
}

上述代码把 StatefulWidget 的一些生命周期都进行了重写,并且在执行中都打印了标识,方便看到函数的执行顺序。



  1. 在 main.dart 中加载该组件。代码如下:


import 'package:flutter/material.dart';

import './pages/count_widget.dart';

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: CountWidget(),
);
}
}

这时 CountWidget 作为 MyHomePage 的子组件。我们打开模拟器,开始运行。在控制台可以看到如下日志,可以看出 StatefulWidget 在第一次被创建的时候是调用下面四个函数。


flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

点击屏幕上的 ➕ 按钮,_count 增加 1,模拟器上的数字由 0 变为 1,日志如下。也就是说在状态发生变化的时候,会调用 setStatebuild 两个函数。


flutter: count setState
flutter: count build

command + s 热重载后,日志如下:


flutter: count reassemble
flutter: count didUpdateWidget
flutter: count build

注释掉 main.dart 中的 CountWidget,command + s 热重载后,这时 CountWidget 消失在模拟器上,日志如下:


class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
// body: CountWidget(),
);
}
}

flutter: count reassemble
flutter: count deactivate
flutter: count dispose

经过上述一系列操作之后,通过日志打印并结合生命周期流程图,我们可以很清晰的看出各生命周期函数的作用以及理解生命周期的几个阶段。
相信很多细心的同学已经发现了一个细节,那就是 build 方法在不同的操作中都被调用了,下面我们来介绍什么情况下会触发组件再次 build。


触发组件再次 build


触发组件再次 build 的方式有三种,分别是 setStatedidChangeDependenciesdidUpdateWidget


1.setState 很好理解,只要组件状态发生变化时,就会触发组件 build。在上述的操作过程中,点击 ➕ 按钮,_count 会加 1,结果如下图:


set_state


2.didChangeDependencies,组件依赖的全局 state 发生了变化时,也会调用 build。例如系统语言等、主题色等。


3.didUpdateWidget,我们以下方代码为例。在 main.dart 中,同样的重写生命周期函数,并打印。在 CountWidget 外包一层 Column ,并创建同级的 RaisedButton 做为父 Widget 中的计数器。


class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

final String title;

@override
_MyHomePageState createState() {
print('main createState');
return _MyHomePageState();
}
}

class _MyHomePageState extends State<MyHomePage> {
int mainCount = 0;

void _changeMainCount() {
setState(() {
print('main setState');
mainCount++;
});
}

@override
void initState() {
print('main initState');
super.initState();
}

@override
void didChangeDependencies() {
print('main didChangeDependencies');
super.didChangeDependencies();
}

@override
void didUpdateWidget(MyHomePage oldWidget) {
print('main didUpdateWidget');
super.didUpdateWidget(oldWidget);
}

@override
void deactivate() {
print('main deactivate');
super.deactivate();
}

@override
void dispose() {
print('main dispose');
super.dispose();
}

@override
void reassemble() {
print('main reassemble');
super.reassemble();
}

@override
Widget build(BuildContext context) {
print('main build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: <Widget>[
RaisedButton(
onPressed: () => _changeMainCount(),
child: Text('mainCount = $mainCount'),
),
CountWidget(),
],
),
);
}
}

重新加载 app,可以看到打印日志如下:


father_widget_create_state


flutter: main createState
flutter: main initState
flutter: main didChangeDependencies
flutter: main build
flutter: count createState
flutter: count initState
flutter: count didChangeDependencies
flutter: count build

可以发现:



  • 父组件也经历了 createStateinitStatedidChangeDependenciesbuild 这四个过程。

  • 并且父组件要在 build 之后才会创建子组件。


点击 MyHomePage(父组件)的 mainCount 按钮 ,打印如下:


flutter: main setState
flutter: main build
flutter: count didUpdateWidget
flutter: count build

点击 CountWidget 的 ➕ 按钮,打印如下:


flutter: count setState
flutter: count build

可以说明父组件的 State 变化会引起子组件的 didUpdateWidget 和 build,子组件自己的状态变化不会引起父组件的状态改变


组件销毁


我们重复上面的操作,为 CountWidget 添加一个子组件 CountSubWidget,并用 count sub 前缀打印日志。重新加载 app。


注释掉 CountWidget 中的 CountSubWidget,打印日志如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count didUpdateWidget
flutter: count build
flutter: count sub deactivate
flutter: count sub dispose

恢复到注释前,注释掉 MyHomePage 中的 CountWidget,打印如下:


flutter: main reassemble
flutter: count reassemble
flutter: count sub reassemble
flutter: main didUpdateWidget
flutter: main build
flutter: count deactivate
flutter: count sub deactivate
flutter: count sub dispose
flutter: count dispose

因为是热重载,所以会调用 reassembledidUpdateWidgetbuild,我们可以忽略带有这几个函数的打印日志。可以得出结论:
父组件移除,会先移除节点,然后子组件移除节点,子组件被永久移除,最后是父组件被永久移除。


Flutter App Lifecycle


上面我们介绍的生命周期主要是 StatefulWidget 组件的生命周期,下面我们来简单介绍一下和 app 平台相关的生命周期,比如退出到后台。


我们创建 app_lifecycle_state.dart 文件并创建 AppLifecycle,他是一个 StatefulWidget,但是他要继承 WidgetsBindingObserver。


import 'package:flutter/material.dart';

class AppLifecycle extends StatefulWidget {
AppLifecycle({Key key}) : super(key: key);

@override
_AppLifecycleState createState() {
print('sub createState');
return _AppLifecycleState();
}
}

class _AppLifecycleState extends State<AppLifecycle>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
print('sub initState');
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
print('didChangeAppLifecycleState');
if (state == AppLifecycleState.resumed) {
print('resumed:');
} else if (state == AppLifecycleState.inactive) {
print('inactive');
} else if (state == AppLifecycleState.paused) {
print('paused');
} else if (state == AppLifecycleState.detached) {
print('detached');
}
}

@override
Widget build(BuildContext context) {
print('sub build');
return Container(
child: Text('data'),
);
}
}

didChangeAppLifecycleState 方法是重点,AppLifecycleState 中的状态包括:resumedinactivepauseddetached 四种。


didChangeAppLifecycleState 方法的依赖于系统的通知(notifications),正常情况下,App是可以接收到这些通知,但有个别情况下是无法接收到通知的,比如用户关机等。它的四种生命周期状态枚举源码中有详细的介绍和说明,下面附上源码以及简单的翻译说明。


app_life_cycle_state



  • resumed:该应用程序是可见的,并对用户的输入作出反应。也就是应用程序进入前台。

  • inactive:应用程序处于非活动状态,没有接收用户的输入。在 iOS 上,这种状态对应的是应用程序或 Flutter 主机视图在前台非活动状态下运行。当处于电话呼叫、响应 TouchID 请求、进入应用切换器或控制中心时,或者当 UIViewController 托管的 Flutter 应用程序正在过渡。在 Android 上,这相当于应用程序或 Flutter 主机视图在前台非活动状态下运行。当另一个活动被关注时,如分屏应用、电话呼叫、画中画应用、系统对话框或其他窗口,应用会过渡到这种状态。也就是应用进入后台。

  • pause:该应用程序目前对用户不可见,对用户的输入没有反应,并且在后台运行。当应用程序处于这种状态时,引擎将不会调用。也就是说应用进入非活动状态。

  • detached:应用程序仍然被托管在flutter引擎上,但与任何主机视图分离。处于此状态的时机:引擎首次加载到附加到一个平台 View 的过程中,或者由于执行 Navigator pop,view 被销毁。


除了 app 生命周期的方法,Flutter 还有一些其他不属于生命周期,但是也会在一些特殊时机被观察到的方法,如 didChangeAccessibilityFeatures(当前系统改变了一些访问性活动的回调)didHaveMemoryPressure(低内存回调)didChangeLocales(用户本地设置变化时调用,如系统语言改变)didChangeTextScaleFactor(文字系数变化) 等,如果有兴趣的话,可以去试一试。


总结


本篇文章主要介绍了 Widget 中的 StatefulWidget 的生命周期,以及 Flutter App 相关的生命周期。但是要切记,StatefulWidget 虽好,但也不要无脑的所有 Widget 全都用它,能使用 StatelessWidget 还是要尽量去使用 StatelessWidget(仔细想一下,这是为什么呢?)。好啦,看完本篇文章,你就是 Flutter 初级开发工程师了,可以去面试了(狗头保命)。


最后


真正坚持到最后的人,往往靠的不是短暂的激情,而是恰到好处的喜欢和投入。你还那么年轻,完全可以成为任何你想要成为的样子!


作者:百瓶技术
链接:https://juejin.cn/post/7056646298073563166
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 蒙层控件

功能说明 新手引导高亮蒙层 图片进度条 使用说明 Import the packages: import 'package:flutter_mask_view/flutter_mask_view.dart'; show height-light mask ...
继续阅读 »

功能说明



  • 新手引导高亮蒙层

  • 图片进度条


使用说明


Import the packages:


import 'package:flutter_mask_view/flutter_mask_view.dart';

show height-light mask for newer:


 Scaffold(
body: Stack(
children: [
//only display background for demo
Image.asset(ImagesRes.BG_HOME),

//config
HeightLightMaskView(
//控件大小
maskViewSize: Size(720, 1080),
//蒙层颜色
backgroundColor: Colors.blue.withOpacity(0.6),
//高亮区域颜色
color: Colors.transparent,
//设置高亮区域形状,如果width = height = radius 为圆形,否则矩形
rRect: RRect.fromRectAndRadius(
Rect.fromLTWH(100, 100, 50, 50),
Radius.circular(50),
),
)
],
),
)

more:


          HeightLightMaskView(
maskViewSize: Size(720, 1080),
backgroundColor: Colors.blue.withOpacity(0.6),
color: Colors.transparent,
//自定义蒙层区域形状
pathBuilder: (Size size) {
return Path()
..moveTo(100, 100)
..lineTo(50, 150)
..lineTo(150, 150);
},
//在蒙层上自定义绘制内容
drawAfter: (Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.red
..strokeWidth = 15
..style = PaintingStyle.stroke;
canvas.drawCircle(Offset(150, 150), 50, paint);
},
//是否重绘,默认return false, 如果使用动画,此返回true
rePaintDelegate: (CustomPainter oldDelegate){
return false;
},
)

Display



create image progress bar:


      ImageProgressMaskView(
size: Size(360, 840),
//进度图片
backgroundRes: 'images/bg.png',
//当前进度
progress: 0.5,
//蒙层形状,内置以下两种蒙层:
//矩形蒙层:PathProviders.sRecPathProvider
//水波蒙层(可配置水波高度和密度):PathProviders.createWaveProvider

//自定义进度蒙层
pathProvider: PathProviders.createWaveProvider(60, 100),
),
)

PathProviders.sRecPathProvider:



PathProviders.createWaveProvider:



与动画联动:


class _MaskTestAppState extends State<MaskTestApp>
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
_controller =
AnimationController(duration: Duration(seconds: 5), vsync: this);
_controller.forward();
super.initState();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
ImageProgressMaskView(
size: Size(300, 300),
backgroundRes: ImagesRes.IMG,
progress: _controller.value,
pathProvider: PathProviders.createWaveProvider(60, 40),
rePaintDelegate: (_) => true,
),
Text(
'${(_controller.value * 100).toInt()} %',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 30,
),
)
],
);
},
),
),
);
}
}

Result:


case 1:



case 2: (png)



仓库地址


PUB


Github


作者:MY116
链接:https://juejin.cn/post/7075914421297479717
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

React18正式版发布,未来发展趋势是?

2022年3月29号,React18正式版发布。从v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。本文主要讲解v18发布日志中透露的一些未来发展趋势...
继续阅读 »

2022年3月29号,React18正式版发布。

v16开始,React团队就在普及并发的概念。在v18的迭代过程中(alpha、Beta、RC),也一直在科普并发特性,所以正式版发布时,已经没有什么新鲜特性。

本文主要讲解v18发布日志中透露的一些未来发展趋势。

欢迎加入人类高质量前端框架研究群,带飞

开发者可能并不会接触到并发特性

React对增加API是很慎重的。从13年诞生至今,触发更新的方式都是this.setState

而引入并发概念后,光是与并发相关的API就有好几个,比如:

  • useTransition

  • useDeferredValue

甚至出现了为并发兜底的API(即并发情况下,不使用这些API可能会出bug),比如:

  • useSyncExternalStore

  • useInsertionEffect

一下多出这么多API,还不是像useState这种不使用不行的API,况且,并发这一特性对于多数前端开发者都有些陌生。

你可以代入自己的业务想想,让开发者上手使用并发特性有多难。

所以,在未来用v18开发的应用,开发者可能并不会接触到并发特性。这些特性更可能是由各种库封装好的。

比如:startTransition可以让用户在不同视图间切换的同时,不阻塞用户输入。

这一API很可能会由各种Router实现,再作为一个配置项开放给开发者。

万物皆可Suspense

对于React来说,有两类瓶颈需要解决:

  • CPU的瓶颈,如大计算量的操作导致页面卡顿

  • IO的瓶颈,如请求服务端数据时的等待时间

其中CPU的瓶颈通过并发特性的优先级中断机制解决。

IO的瓶颈则交给Suspense解决。

所以,未来一切与IO相关的操作,都会收敛到Suspense这一解决方案内。

从最初的React.lazy到如今仍在开发中的Server Components,最终万物皆可Suspense

这其中有些逻辑是很复杂的,比如:

  • Server Components

  • 新的服务端渲染方案

所以,这些操作不大可能是直接面向开发者的。

这又回到了上一条,这些操作会交由各种库实现。如果复杂度更高,则会交由基于React封装的框架实现,比如Next.jsRemix

这也是为什么React团队核心人物Sebastian会加入Next.js

可以说,React未来的定位是:一个前端底层操作系统,足够复杂,一般开发者慎用。

而开发者使用的是基于该操作系统实现的各种上层应用

总结

如果说v16之前各种React Like库还能靠体积、性能优势分走React部分蛋糕,那未来两者走的完全是两条赛道,因为两者的生态不再兼容。

未来不再会有React全家桶的概念,桶里的各个部件最终会沦为更大的框架中的一个小模块。

当前你们业务里是直接使用React呢,还是使用各种框架(比如Next.js)?

作者:魔术师卡颂
来源:https://juejin.cn/post/7080719159645962271 收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »
前言
跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

我们在开发flutter应用的时候编写代码,要么是同步代码,要么是异步代码。那么什么是同步什么是异步呢?

  • 同步代码就是正常编写的代码块
  • 异步代码就是Future,async等关键字修饰的代码块

一、时机不同

他们区别于运行时机不同,同步代码先执行,异步代码后执行,即使你的同步代码写在最后,那也是你的同步代码执行,之后运行你的异步代码。

二、机制不同

异步代码运行在 event loop中,类似于Android里的Looper机制,是一个死循环,event loop不断的从事件队列里取事件然后运行。

event loop循环机制

如图所示,事件存放于队列中,loop循环执行 运行图 Dart的事件循环如下图所示。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。 在这里插入图片描述 事件队列包含外部事件,例如I/O, Timer,绘制事件等等。 微任务队列则包含有Dart内部的微任务,主要是通过scheduleMicrotask来调度。

  1. 首先处理所有微任务队列里的微任务。
  2. 处理完所有微任务以后。从事件队列里取1个事件进行处理。
  3. 回到微任务队列继续循环。

Dart要先把所有的微任务处理完,再处理一个事件,处理完之后再看看微任务队列。如此循环。

例子:

8个微任务
2个事件

Dart-->执行完8个微任务
Dart-->执行完1个事件
Dart-->查看微任务队列
Dart-->再执行完1个事件
done

异步执行

那么在Dart中如何让你的代码异步执行呢?很简单,把要异步执行的代码放在微任务队列或者事件队列里就行了。

可以调用scheduleMicrotask来让代码以微任务的方式异步执行

    scheduleMicrotask((){
print('a microtask');
});

可以调用Timer.run来让代码以Event的方式异步执行

   Timer.run((){
print('a event');
});

Future异步执行

创建一个立刻在事件队列里运行的Future:

Future(() => print('立刻在Event queue中运行的Future'));

创建一个延时1秒在事件队列里运行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));

创建一个在微任务队列里运行的Future:

Future.microtask(() => print('在Microtask queue里运行的Future'));

创建一个同步运行的Future:

Future.sync(() => print('同步运行的Future'));

这里要注意一下,这个同步运行指的是构造Future的时候传入的函数是同步运行的,这个Future通过then串进来的回调函数是调度到微任务队列异步执行的。

有了Future之后, 通过调用then来把回调函数串起来,这样就解决了"回调地狱"的问题。

Future(()=> print('task'))
.then((_)=> print('callback1'))
.then((_)=> print('callback2'));

在task打印完毕以后,通过then串起来的回调函数会按照链接的顺序依次执行。 如果task执行出错怎么办?你可以通过catchError来链上一个错误处理函数:

 Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));

上面这个Future执行时直接抛出一个异常,这个异常会被catchError捕捉到。类似于Java中的try/catch机制的catch代码块。运行后只会执行catchError里的代码。两个then中的代码都不会被执行。

既然有了类似Java的try/catch,那么Java中的finally也应该有吧。有的,那就是whenComplete:


Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));

无论这个Future是正常执行完毕还是抛出异常,whenComplete都一定会被执行。

结果执行

把如上的代码在dart中运行看看输出

 print('1');
var fu1 = Future(() => print('立刻在Event queue中运行的Future'));
Future future2 = new Future((){
print("future2 初始化任务");
});
print('2');
Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));
print('3');
var fu2 = Future.microtask(() => print('在Microtask queue里运行的Future'));
print('4');
Future.sync(() => print('同步运行的Future')).then((value) => print('then同步运行的Future'));
print('5');
fu1.then((value) => print('then 立刻在Event queue中运行的Future'));
print('6');
fu2.then((value) => print('then 在Microtask queue里运行的Future'));
print('7');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));
print('8');
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));
print('9');
Future future4 = Future.value("立即执行").then((value){
print("future4 执行then");
}).whenComplete((){
print("future4 执行whenComplete");
});
print('10');


future2.then((_) {
print("future2 执行then");
future4.then((_){
print("future4 执行then2");
});

});

输出

I/flutter (29040): 1
I/flutter (29040): 2
I/flutter (29040): 3
I/flutter (29040): 4
I/flutter (29040): 同步运行的Future
I/flutter (29040): 5
I/flutter (29040): 6
I/flutter (29040): 7
I/flutter (29040): 8
I/flutter (29040): 9
I/flutter (29040): 10
I/flutter (29040): 在Microtask queue里运行的Future
I/flutter (29040): thenMicrotask queue里运行的Future
I/flutter (29040): then同步运行的Future
I/flutter (29040): future4 执行then
I/flutter (29040): future4 执行whenComplete
I/flutter (29040): 立刻在Event queue中运行的Future
I/flutter (29040): then 立刻在Event queue中运行的Future
I/flutter (29040): future2 初始化任务
I/flutter (29040): future2 执行then
I/flutter (29040): future4 执行then2
I/flutter (29040): we have a problem
I/flutter (29040): we have a problem
I/flutter (29040): whenComplete
I/flutter (29040): 1秒后在Event queue中运行的Future

输出说明:

  • 先输出同步代码,再输出异步代码
  • 通过then串联起的任务会在主要任务执行完立即执行
  • Future.sync是同步执行,then执行在微任务队列中
  • 通过Future.value()函数创建的任务是立即执行的
  • 如果是在whenComplete之后注册的then,那么这个then的任务将放在microtask执行

Completer

Completer允许你做某个异步事情的时候,调用c.complete(value)方法来传入最后要返回的值。最后通过c.future的返回值来得到结果,(注意:宣告完成的complete和completeError方法只能调用一次,不然会报错)。 例子:

test() async {
Completer c = new Completer();
for (var i = 0; i < 1000; i++) {
if (i == 900 && c.isCompleted == false) {
c.completeError('error in $i');
}
if (i == 800 && c.isCompleted == false) {
c.complete('complete in $i');
}
}

try {
String res = await c.future;
print(res); //得到complete传入的返回值 'complete in 800'
} catch (e) {
print(e);//捕获completeError返回的错误
}
}


收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate

前言 跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企...
继续阅读 »

前言


跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

Dart是单线程的,Dart提供了Isolate,isolate提供了多线程的能力。但作为多线程能力的,却内存不能共享。但同样的内存不能共享,那么就不存在锁竞争问题。


举个例子来展示作用


如果一段代码执行事件很长,flutter如何开发。
基本页面代码(一段代码)


ElevatedButton(
child: Text("登录"),
onPressed: () {
执行运行代码();
}

延时代码块


String work(int value){
print("work start");
sleep(Duration(seconds:value));
print("work end");
return "work complete:$value";
}

第一种:直接执行运行代码(延时5秒)


  执行运行代码() {
work(5);
}

结果:
5秒卡的死死的


第二种:async执行运行代码(延时5秒)


  执行运行代码() async{
work(5);
}

结果:
5秒依旧卡的死死的


------------------------------------------------我是分割线--------------------------------------------------



why?在dart中,async不是异步计算么?(循环机制下篇讲)因为我们仍旧是在同一个UI线程中做运算,异步只是说我可以先运行其他的,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。



第三种:ioslate执行运行代码(延时5秒)



但是由于dart中的Isolate比较重量级,UI线程和Isolate中的数据的传输比较复杂,因此flutter为了简化用户代码,在foundation库中封装了一个轻量级compute操作。



  执行运行代码() async{
var result = await compute(work, 5);
print(result);
}

结果:
居然不卡顿了


使用说明



compute的使用还是有些限制,它没有办法多次返回结果,也没有办法持续性的传值计算,每次调用,相当于新建一个隔离,如果调用过多的话反而会适得其反。我们需要根据不同的业务选择用compute和isolate




Future work(int value) async{
//接收消息管道
ReceivePort rp = new ReceivePort();
//发送消息管道
SendPort port = rp.sendPort;
Isolate isolate = await Isolate.spawn(workEvent, port);
//发送消息管道2
final sendPort2 = await rp.first;
//返回应答数据
final answer = ReceivePort();
sendPort2.send([answer.sendPort, value]);
return answer.first;
}

void workEvent(SendPort port) {
//接收消息管道2
final rPort = ReceivePort();
SendPort port2 = rPort.sendPort;
// 将新isolate中创建的SendPort发送到主isolate中用于通信
port.send(port2);

rPort.listen((message) {
final send = message[0] as SendPort;
send.send(work(5));
});
}

基本方法


    //恢复 isolate 的使用
isolate.resume(isolate.pauseCapability);

//暂停 isolate 的使用
isolate.pause(isolate.pauseCapability);

//结束 isolate 的使用
isolate.kill(priority: Isolate.immediate);

//赋值为空 便于内存及时回收
isolate = null;


两个进程都双向绑定了消息通信的通道,即使新的Isolate中的任务完成了,它的进程也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用isolate.kill(priority: Isolate.immediate);将Isolate立即杀死。



用Future还是isolate?


future使用场景:



  • 代码段可以独立运行而不会影响应用程序的流畅性


isolate使用场景:



  • 繁重的处理可能要花一些时间才能完成

  • 网络加载大图

  • 图片处理
收起阅读 »

跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

与java&kotlin不同的是,dart中有一个特殊的关键字mixin(mix-in),用这个关键字的类被其他类(包含)的时候,其他类就拥有了该类的方法。这样代码不通过继承(extend)就可以重用。


场景来展示mixin如何使用


由于在java&kotlin中经常性的用extent & implements 并不知道mixin是如何使用,那么我举几个特殊的例子来帮助大家理解


场景用例


在这里插入图片描述
如上uml图所示
鸟作为父类,鸟必备的技能为(下蛋和走路),而作为其子类的大雁和麻雀可以飞行,企鹅却不能飞行。
那么飞行却成为个别鸟类的技能,如果在父类中定义实现飞,那在企鹅中就多了个空实现。如果定义一个接口实现飞,那么在能飞的鸟类中就必须都要重新编写飞的代码。如何让这一切变得容易呢。
那么我们用混入(with)来实现如下代码:


abstract class Bird{

void walk() { print('我会走路'); }
void xiadan() { print('我会下蛋'); }
}

abstract class Fly{
void fly() { print('我会飞'); }
}

//大雁

class Dayan extends Bird with Fly {}

//企鹅

class Qier extends Bird {}

如果 Fly 类 不希望作为常规类被使用,使用关键字 mixin 替换 class 。


mixin Fly{
void fly() { print('我会飞'); }
}

如果 Fly 类 只希望限定于鸟类去使用,那么需要加入如下关键字


mixin Fly on Bird{
void fly() { print('我会飞'); }
}

mixin特点



  1. mixin 没有构造函数,不能被实例化

  2. 可以当做接口使用,class 混入之后需要实现

  3. 可以使用on 指定混入的类类型,如果不是报错。

  4. 如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


调用顺序展示


简单顺序调用


如果with后的多个类中有相同的方法,如果当前使用类重写了该方法,就会调用当前类中的方法。如果当前使用类没有重写了该方法,则会调用距离with关键字最远类中的方法。


abstract class First {
void doPrint() {
print('First');
}
}

abstract class Second {
void doPrint() {
print('Second');
}
}

class Father {
void doPrint() {
print('Father');
}
}

class Son extends Father with First,Second {

}

调用:


	Son son = Son();
son.doPrint();

打印:


Second

重写后调用


class Son extends Father with First,Second {
void doPrint() {
print('Son');
}
}

调用:


	Son son = Son();
son.doPrint();

打印:


Son

带有父类方法调用


class Father {
void init() {
print('Father init');
}
}
mixin FirstMixin on Father {
void init() {
print('FirstMixin init start');
super.init();
print('FirstMixin init end');
}
}

mixin SecondMixin on Father {
void init() {
print('SecondMixin init start');
super.init();
print('SecondMixin init end');
}
}


class Son extends Father with FirstMixin, SecondMixin {

@override
void init() {
print('Son init start');
super.init();
print('Son init end');
}
}

调用:


  Son().init();

打印:


flutter: Son init start
flutter: SecondMixin init start
flutter: FirstMixin init start
flutter: Father init
flutter: FirstMixin init end
flutter: SecondMixin init end
flutter: Son init end

说明






















方式类型说明
withmixin混入该类内容
with onmixin混入该类内容,但必须是特点的类型

特别注意


mixin 可以on多个类,但with时候之前的类必须已经有相关的实现


mixin Mix on Mix1,Mix2{ }
收起阅读 »

Flutter真香,我用它写了个桌面版JSON解析工具

Flutter支持稳定的桌面设备开发已经一段时间了,不得不说,Flutter多平台支持的特性真的很香。我本人并没有任何桌面开发的经验,但仍然使用Flutter开发出了一个桌面版小程序,功能很简单,就是对输入的json做格式化处理和转模型。话不多说,先来看看实际...
继续阅读 »

Flutter支持稳定的桌面设备开发已经一段时间了,不得不说,Flutter多平台支持的特性真的很香。我本人并没有任何桌面开发的经验,但仍然使用Flutter开发出了一个桌面版小程序,功能很简单,就是对输入的json做格式化处理和转模型。

话不多说,先来看看实际效果。项目源码地址


开发环境如下:

Flutter: 2.8.1

Dart: 2.15.1

IDE: VSCode

JSON作为我们日常开发工作中经常要打交道的一种数据格式,它共有6种数据类型:null, num, string, object, array, bool。我们势必对它又爱又恨。爱他因为他作为数据处理的一种格式确实非常方便简洁。但是在我们做Flutter开发中,又需要接触到json解析时,就会感觉非常棘手,因为flutter没有反射,导致json转模型这块需要手写那繁杂的映射关系。就像下面这样子。

void fromJson(Map<String, dynamic>? json) {
if (json == null) return;
age = json['age'];
name = json['name'] ?? '';
}

数据量少还能接受,一旦量大,那么光手写这个解析方法都能让你怀疑人生。更何况手写还有出错的可能。好在官方有个工具json_serializable可以自动生成这块转换代码,也解决了flutter界json转模型的空缺。当然,业界也有专门解析json的网站,可以自动生成dart代码,使用者在生成后复制进项目中即可,也是非常方便的。

本项目以json解析为切入点,和大家一起来看下flutter是如何开发桌面应用的。

1、创建项目

要让我们的flutter项目支持桌面设备。我们首先需要修改下flutter的设置。如下,让我们的项目支持windowsmacos系统。

flutter config --enable-windows-desktop
flutter config --enable-macos-desktop

接下来使用flutter create命令创建我们的模版工程。

flutter create -t app --platforms macos,windows  hello_desktop

创建完项目后,我们就可以run起来了。

2、功能介绍

先来看下整体界面,界面四块,分别为功能模块、文件选择模块、输入模块、输出模块。


这里自动修正的功能能帮助我们将异常的格式不正确的json转为正确的格式,不过处于开发阶段,可以不必理会。

3、关键技术点&难点记录:

1、控制窗口Window

我们在新建一个桌面应用时,默认的模版又一个Appbar,此时应用可以用鼠标拖拽移动,放大缩小,还可以缩到很小。但是,我们一旦去掉这个导航栏,那么窗口就不能用鼠标拖动了,并且我们往往不希望用户将我们的窗口缩放的很小,这会导致页面异常,一些重要信息都展示不全。因此这里需要借助第三方组件bitsdojo_window。通过bitsdojo_window,我们可以实现窗口的定制化,拖动,最小尺寸,最大尺寸,窗口边框,窗口顶部放大、缩小、关闭的按钮等。

2、鼠标移动捕捉

通过InkWell组件,可以捕捉到手势、鼠标、触控笔的移动和停留位置

tip = InkWell(
   child: tip,
   hoverColor: Colors.white,
   highlightColor: Colors.white,
   splashColor: Colors.white,
   onHover: (value) {
     bool needChangeState = widget.showTip != value;
     if (needChangeState) {
       if (value) {
         // 鼠标在tip上,显示提示
         showTip(context, PointerHoverEvent());
       } else {
         overlay?.remove();
       }
     }
     widget.showTip = value;
   },
   onTap: () {},
 );

3、鼠标停在指定文字上时显示提示框,移开鼠标时隐藏提示框

这个功能是鼠标移动后的UI交互界面。要在窗口上显示一个提示框,可以使用Overlay。需要注意的是,由于在Overlay上的text的根结点不是Material风格的组件,因此会出现黄色的下划线。因此一定要用Material包一下text。并且你必须给创建的OverlayEntry一个位置,否则它将全屏显示。

Widget entry = const Text(
     '自动修复指输入的JSON格式不正确时,工具将根据正确的JSON格式自动为其补其确实内容。如“”、{}、:等',
     style: TextStyle(
       fontSize: 14,
       color: Colors.black
     ),
     );

entry = Material(
     child: entry,
   );

// ... 其他代码
OverlayEntry overlay = OverlayEntry(
         builder: (_) {
           return entry;
         },
       );
      Overlay.of(context)?.insert(overlay);

      this.overlay = overlay;
 }

4、读取鼠标拖拽的文件

读取说表拖拽的文件一开始想尝试使用InkWell组件,但是这个组件无法识别拖拽中的鼠标,并且也无法从中拿到文件信息。因此放弃。后来从文章《Flutter-2天写个桌面端APP》中发现一个可读取拖拽文件的组件desktop_drop ,能满足要求。

5、本地文件选取

使用开源组件file_picker ,选完图片后的操作和拖拽选择图片后的操作一致。

6、TextField显示富文本

Textfield如果要显示富文本,那么需要自定义TextEditingController。并重写buildTextSpan方法。

class RichTextEditingController extends TextEditingController {

// ...

@override
 TextSpan buildTextSpan(
    {required BuildContext context,
     TextStyle? style,
     required bool withComposing}) {
   if (highlight) {
     TextSpan text;
     String? input = OutputManager().inputJSON;
     text = _serializer.formatRich(input) ?? const TextSpan();
     return text;
  }
   String json = value.text;
   return TextSpan(text: json, style: style);
}
}

7、导出文件报错

在做导出功能时遇到下列报错,保存提示为没有权限访问对应目录下的文件。

flutter: path= /Users/zl/Library/Containers/com.example.jsonFormat/Data/Downloads
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: FileSystemException: Cannot open file, path = '/Users/zl/Library/Containers/com.example.jsonFormat/Data/Downloads/my_format_json.json' (OS Error: Operation not permitted, errno = 1)

通过Apple的开发文档找到有关权限问题的说明。其中有个授权私钥的key为com.apple.security.files.downloads.read-write ,表示对用户的下载文件夹的读/写访问权限。那么,使用Xcode打开Flutter项目中的mac应用,修改工程目录下的DebugProfile.entitlements文件,向entitlements文件中添加com.apple.security.files.downloads.read-write,并将值设置为YES,保存后重启Flutter项目。发现已经可以向下载目录中读写文件了。

当然,这是正常操作。还有个骚操作就是关闭系统的沙盒机制。将entitlements文件的App Sandbox设置为NO。这样我们就可以访问任意路径了。当然关闭应用的沙盒也就相当于关闭了应用的防护机制,因此这个选项慎用。


TODO List:

  • json自动修正

  • 模型代码高亮

  • 自定义导出路径

参考文档:

Flutter桌面支持

Flutter desktop support

Flutter-2天写个桌面端APP

pub.dev-window

Flutter Desktop - bitsdojo-window - bilibili

Apple开发权限文档

作者:ijinfeng
来源:https://juejin.cn/post/7069689952459554830

收起阅读 »

Flutter 多端统一配置

本文介绍Flutter的全局变量统一配置的一种实现方法。3.2 多端统一配置为了方便对项目进行维护,我们需要将配置文件抽象出来进行统一管理。3.2.1 需求建立配置文件,统一常用配置信息,可多端共享。3.2.2 实现1 创建test项目创建项目:flutter...
继续阅读 »
本文介绍Flutter的全局变量统一配置的一种实现方法。

3.2 多端统一配置

为了方便对项目进行维护,我们需要将配置文件抽象出来进行统一管理。

3.2.1 需求

建立配置文件,统一常用配置信息,可多端共享。

3.2.2 实现

1 创建test项目

创建项目:flutter create test

进入项目:cd test

2 assets目录

创建文件夹:mkdir assets

3 创建配置文件

创建全局共享配置文件:touch assets/app.properties

4 编辑配置文件

app.properties中定义所需参数

serverHost=http://127.0.0.1:8080/
version=0.1.1


5 配置assets权限

打开pubspec.yaml,配置app.properties权限

flutter:
...
assets:
    - app.properties


6 创建dart配置文件

创建配置文件:touch lib/config.dart,并写入如下内容:

import 'package:flutter/services.dart';

class Config {
 factory Config() => _instance;
 static Config _instance;
 Config._internal();
 String serverHost = "";
 String version = "";

 Future init() async {
   Map<String, String> properties = Map();
   String value = await rootBundle.loadString("assets/app.properties");
   List<String> list = value.split("\n");
   list?.forEach((element) {
     if (element != null && element.contains("=")) {
       String key = element.substring(0, element.indexOf("="));
       String value = element.substring(element.indexOf("=") + 1);
       properties[key] = value;
    }
  });
   parserProperties(properties);
   return Future.value();
}

 void parserProperties(Map<String, String> properties) {
   serverHost = properties['serverHost'] ?? "";
   version = properties['version'] ?? "";
}
}

以后代码中需要用到全局变量通过Config调用即可。

收起阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?需求包含需求的具体有:界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。点击清空按钮可以...
继续阅读 »

支付宝集五福手画福字功能(含撤销操作)用Flutter如何实现?

今早一觉醒来发现支付宝一年一度的集五福活动又开始了,其中包含了一个功能就是手写福字,还包括撤销一笔,清除重写,保存相册等,那么使用Flutter应该如何实现这些功能呢?

需求

包含需求的具体有:

  • 界面随着用户手指的滑动显示走过轨迹,也就是对应的笔画。

  • 点击清空按钮可以清除所有的笔画。

  • 点击撤销按钮可以清除上一步画过的笔画。

  • 保存所写的文字样式到相册。

实现思路

显示笔画轨迹

使用Listener组件对用户手指落下、滑动和收起的动作进行监听,在onPointerDown,onPointerMove,onPointerUp3个监听方法中返回的PointerMoveEvent对象包含了手指所在的位置坐标偏移量localPosition,用户每次滑动时都会记录下轨迹经过的坐标点,这些坐标点连接起来就是一条线。其次,再配合使用CustomPainter进行画布自绘,将所有划过的点的连接成线使用画笔绘制在界面上即可。

搜集坐标点:

Listener(
child: Container(
  alignment: Alignment.center,
  color: Colors.transparent,
  width: double.infinity,
  height: MediaQuery.of(context).size.height,
),
onPointerDown: (PointerDownEvent event) {
  setState(() {
     
  });
},
onPointerMove: (PointerMoveEvent event) {
  setState(() {
     
  });
},
onPointerUp: (PointerUpEvent event) {
  setState(() {
     
  });
},
),

绘制:

@override
void paint(Canvas canvas, Size size) {
myPaint.strokeCap = StrokeCap.round;
myPaint.strokeWidth = 15.0;
if (lines.isEmpty) {
  canvas.drawPoints(PointMode.polygon, [Offset.zero, Offset.zero], myPaint);
} else {
  for (int k = 0; k < lines.length; k++) {
    for (int i = 0; i < lines[k].length - 1; i++) {
      if (lines[k][i] != Offset.zero && lines[k][i + 1] != Offset.zero) {
        canvas.drawLine(lines[k][i], lines[k][i + 1], myPaint);
      }
    }
  }
}
}

撤销与清空

图片

看到上面的代码有的人可能会比较疑惑,绘制时为什么这么复杂,还出现了双重循环。这就和撤销功能有关了,先假设不需要撤销功能,其实我们就可以直接把所有笔画的点连接到一起进行绘制就可以了,但是一旦引入了撤销功能,就要记录每一笔笔画,福字笔画是13画,那么理论上是需要记录13个笔画的,才能保证每次撤销时都能正常退回上一次画过的笔迹,所以第一反应就是使用集合将每一次笔画记录下来。而上面也说了每一个笔画其实也是多个坐标点的集合,所以所有笔画就是一个坐标点集合的集合,即:

/// 所有笔画划线集合
List<List<Offset>> _lines = [];

另外,也不难想到,我们可以轻易通过手指按下和手指手指的方法回调来区分笔画开始和结束。在两个方法中进行笔画的add和更新。

onPointerDown: (PointerDownEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.add(_points);
});
},
onPointerMove: (PointerMoveEvent event) {
setState(() {
  _event = event;
  _points.add(_event?.localPosition ?? Offset.zero);
  _lines.last = _points;
});
},
onPointerUp: (PointerUpEvent event) {
setState(() {
  _event = event;
  _points.add(Offset.zero);
  _lines.last = _points;
});
_points = [];
},

而前面说的双重遍历这时也比较好理解了:

  • 第一层循环是遍历所有的笔画,遍历次数就是福字的笔画数。

  • 第二层循环是每一个笔画包括的好多个坐标点,遍历出来使用drawLine方法绘制到界面上形成一条线。

这样在进行撤销操作时,调用list的removeLast方法移除最后一项再刷新界面就能实现退回一笔的效果了,清空就是清空笔画集合。

保存到相册

保存相册主要是引入了两个插件库:permission_handlerimage_gallery_saver,一个用来获取存储权限,一个用来保存到相册。 使用RepaintBoundary组件将画布包裹起来,并指定key,在点击保存时按顺序调用如下方法先获取截图后保存即可:

RenderRepaintBoundary boundary =
  key.currentContext!.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3.0);
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
_postBytes = byteData?.buffer.asUint8List();
var result = await ImageGallerySaver.saveImage(_postBytes!);

完整代码与demo下载

github地址

安卓手机扫码下载

作者:单总不会亏待你
来源:https://juejin.cn/post/7054833357267402788

收起阅读 »

淘特 Flutter 流畅度优化实践

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带...
继续阅读 »

不同的业务背景引出不同的技术诉求,“用户体验特爽”是淘特的不懈追求,本文将介绍笔者加入淘特以来在Flutter流畅度方面的诸多优化实践,这些优化不涉及Engine改造、不涉及高大上的“轮子建设“,只需细心细致深入业务抽丝剥茧,坚持实际体感导向,即能为用户体验带来显著提升,值得Flutter开发者将其应用在产品的每一个像素。

背景

淘特具备鲜明的三大特征:

  1. 业务特征:淘特拥有业界最复杂的淘系电商链路

  2. 用户特征:淘特用户中有大量的中老年用户,大量的用户手机系统版本较低,大量的用户使用中低端机

  3. 技术特征:淘特大规模采用Flutter跨平台渲染技术

综上所述:

最复杂业务链路+最低性能用户群体+最新的跨平台技术==>核心问题之一:页面流畅度受到严峻挑战

Flutter核心链路20S快速滚动帧率卡顿率(每秒卡顿率)
直播Tab277.04%
我的41.36667.63%
详情26.715.58%

注:相关数据以vivo Y67,淘特

3.32.999.10 (103) 测得

目标

流畅度是用户体验的关键一环,大家都不希望手机用起来像看电影/刷PPT,尤其是现在高刷屏(90/120hz)的普及,更是极大强化了用户对流畅度的感知,但流畅度也跟产品复杂度强相关,也是一次繁与简的取舍,淘特流畅度一期优化目标:

Flutter核心链路页面达到高流畅度(平均帧率:低端机45FPS、中端机50FPS、高端50FPS)

一期优化后的状态

事项平均帧率卡顿率提升效果
1.直播Tab推荐、分类栏目46.00.35%帧率提高19帧、卡顿率降低6.7%
2.我的页面46.00%帧率提高4.6帧,卡顿率降低7.6%
3.详情45.02%帧率提高18.3桢,卡顿率降低13.58%

旧版3.32如视频左,新版3.37如视频右。因uiautomator工具会触发无障碍性能ISSUE,此版本对比为人工测试。

视频请见:淘特 Flutter 流畅度优化实践

除了数据上的明显提升,体感上,旧版快滑卡顿明显,画面突变明显,新版则基本消除明显的卡顿,画面连续平稳。

问题

回到技术本身,Flutter为什么会卡顿、帧率低?总的来说均为以下2个原因:

  1. UI线程慢了-->渲染指令出的慢

  2. GPU线程慢了-->光栅化慢、图层合成慢、像素上屏慢

那么,怎么解上述的 2 个问题是咱们所关心的重点。既然知道某块有问题,我们自然要有工具系统化的度量问题水平,以及系统化的理论支撑实践,且看以下2节,过程中穿插相关策略在淘特的实践, 理论与实践结合理解更透。

怎么解

解法 - 案例

降低setState的触发节点

大家都知道Flutter的刷新机制,在越高的Widget树层级触发setState标脏Element,“脏树越大”,在越低层级越局部的Widget触发状态更新,“脏树越小”,被标记为脏树后将触发Element.Rebuild,遍历组件树。原理请看下图“Flutter页面刷新机制源码解析”:




“Element.updateChild源码分析”请见下文优化二。

实际应用淘特为例。直播Tab的视频预览功能为例,最初直播Tab的视频播放index通过状态层层传递给子组件,一旦状态变更,顶层setState触发播放index更新, 造成整个页面刷新。但实际整个页面需要更新状态的只有“需要暂停的原VideoWidget”和“待播放的VideoWidget”, 我们改为监听机制,页面中的所有VideoWidget注册监听,顶层用EventBus统一分发播放index至各VideoWidget,局部Widget Check后改变自身状态。

再比如详情页,由于使用了“上一个页面借图”的功能,监听到滚动后隐藏借的图,但setState的调用节点放在了详情顶层Widget,造成了全局刷新。实际该监听刷新逻辑可下放至“借图组件”,降低“脏树”的大小。



缓存不变的Widget

缓存不变的Widget有2大好处。1.被缓存的Widget将无需重复创建, 虽然Flutter官方认为Widget是一种非常轻量级的对象,在实际业务中,Build耗时过高仍是一种常见现象。2.返回相同引用的Widget将使Flutter停止该子树后续遍历, 即Flutter认为该子树无变化无需更新。原理请看下图“Element.updateChild源码分析”


应用场景以淘特实际页面为例。详情页部分组件使用了DXWidget,理论上组件内容一经创建后当次页面生命周期不会再有变化,此种情况即可缓存不变的Widget,避免重复动态渲染DX,停止子树遍历。

Feed流的Item组件,布局复杂,创建成本较高,理论上创建一次后内容也不会再变化,但item可能被删除,此时应该用Objectkey唯一标识组件,防止状态错位。



减少不必要的build(setState)

直播Tab用到一个埋点曝光组件,经过DevTools检查,发现其在每一次进度回调中重新创建itemWidget,虽然这不会造成业务异常,但理论上itemWidget只需被创建一次,这块经排查是使用组件时误传了builder函数,而不是直接传itemWidget实例。

详情页的逻辑非常复杂,AppBar根据滚动距离实时计算透明度,这会导致高频的setState,实际上透明度变化前后应该满足一个差值后才应刷新一次状态, 为了性能考量,透明度应该只有少数几种值变更。



多变图层与不变图层分离

在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。以淘特为例。

直播Feed中的Gif图是不断高频跳动,这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。

同理, 秒杀倒计时也是电商常见场景, 该组件也适用于RepaintBoundary场景。



避免频繁的triggerGC

因为AliFlutter的关系,我们得以主动触发DartGC,但GC同样也是有消耗的,高频的GC更是如此。淘特之前因为iOS的内存压力,在列表滚动停止时ScrollEndNotification则会触发GC,ScrollEndNotification在每一次手Down->up事件后都会触发一次,如果用户多次触摸,则会较为频繁的触发GC,实测影响Y67 4帧左右的性能,这块增加页面不可见时GC 和在Y67等android低端机关闭滑动GC,提高滑动性能。


大JSON解析子线程化

Flutter的isolate默认是单线程模型,而所有的UI操作又都是在UI线程进行的,想应用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI线程”, 所以在大Json解析或大量的channel调用时,一定要观测对UI线程的消耗情况。在淘特中,我们在低端机开启json解析compute化,不阻塞UI线程。

尽量减少或降级Clip、Opacity等组件的使用

Flutter中,Clip主要用于裁剪,裁矩形、圆角矩形、圆形。一旦调用,后续所有的绘图指令都会受其Clip影响。有些ClipRRect可以用ShapeDecoration代替,Opacitiy改用AnimatedOpacity, 针对图片的Clip裁切,可以走定制图片库Transform实现。


降级CustomScrollView预渲染区域为合理值

默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,即如双列瀑布流,当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(加载更多时),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,淘特在低端机降级该区域距离为0或较小值。


高频埋点 Channel批量化操作

在组件曝光时上报埋点是很常见的行为,但在快速滚动的场景下, 瞬间10+ item的略过,20+ channel的调用同样会占用一定的UI线程资源和Native UI线程资源。这里淘特针对部分场景做了批量、定时上传, 维护一个埋点队列,默认定时3S或50条,业务不可见时上报,合并20+channel调用为单次。业务也可在合适时机点强制flush队列上报, 同时在Native侧,将埋点行为切换至子线程进行。

其他有效优化措施

部分业务特效,业务繁忙度在低端机上都是可以适度降级的,如淘特将Feed视频预览播放延迟时间从500ms降为1.5S,Feed流预加载阈值距离从2000+降为500,图片圆角降直角等降级措施的核心思路都是先保证最低端的用户也能用的顺畅,再美化细节锦上添花。

Flutter在无障碍开启情况下,快速滚动场景存在性能问题,如确定业务无需无障碍或用户误触发无障碍,可添加ExcludeSemantics Widget屏蔽无障碍。

通过DevTools检测,发现high_available高可用帧率检测在老版本存在性能问题,这块可升级插件版本或低端机屏蔽该检测。

解法 - 优化案例总结

上述十条优化实践,抛开细节看原理,大致分为以下几类, 融会贯通,实践出真知。

如何提高UI线程性能:

  • 如何提高build性能

    • 降低遍历出发点,降低setState的触发节

    • 停止树的遍历,不变的内容,返回同样的组件实例、Flutter将停止遍历该树(SlideTransition)

    • 减少非必要的build(setState)

  • 如何提高layout性能

    • layout暂时不太容易出问题

  • 如何提高paint性能

    • RepaintBoundary分离多变和不变的图层,如Gif、动画, 但多图层的合成也是有开销的

  • 其他

    • 耗时方法如大JSON解析用compute子线程化

    • 减少不必要的channel调用或批量合并

    • 减少动画

    • 减少Release时的log

    • 提高UI线程在Android/iOS的优先级

    • 列表组件支持局部build

    • 较小的cacheExtent值,减少渲染范围

如何提高GPU线程性能:

  1. 谨慎saveLayer

  2. 尽量少ClipPath、一旦调用,后续所有绘图指令需与Path做相交。(ClipRect、ClipRRect等)

  3. 减少毛玻璃BackdropFilter、阴影boxShadow

  4. 减少Opacity使用,必要时用AnimatedOpacity

解法 - 测量工具

工欲善其事,必先利其器。工具主要分为以下两块。

  1. 流畅度检测:无需侵入代码的流畅度检测方案有几种, 既可以通过adb取surfaceflinger数据, 也可以基于VirtualDisplay做图像对比,或者使用官方DevTools。第三方比较成熟的如PerfDog

  2. 卡顿排查:DevTools是官方的开发配套工具,非常实用

    1. Performance检测单帧CPU耗时(build、layout、paint)、GPU耗时、Widget Build次数

    2. CPUProfiler 检测方法耗时

    3. Flutter Inspector观察不合理布局

    4. Memory 监控Dart内存情况

DevTools

Flutter分为三种编译模式,Debug/Release大家都很熟悉,Debug最大特性为HotReload可调试,Release为最高性能,Profile模式则取其中间,专用于性能分析,其产物以AOT模式无限接近Release性能运行,又保留了丰富的性能分析途径。

如何以Profile模式运行flutter?

如果是混合工程,android为例,在app/build.gradle添加profile{init with debug}即可, 部分应用资源区分debug/profile,也可Copy一份profile。当然,更hack更彻底的方式,可直接修改$flutterRoot/packages/flutter_tools/gradle/flutter.gradle文件中buildModeFor方法,默认返回想要的Profile/Release模式。

如何在Profile模式下打开DevTools?

推荐使用IDE的flutter attach 或者 命令行采用flutter pub global run devtools,填入observatory的地址,即可开始使用DevTools。

Flutter Performance&Inspector

以AS为例,右侧会出现Flutter Performance和Inspector2个功能区。Performance功能区如下图:


Overlay效果如下图。可以看到有2排柱状图,上方为GPU帧耗时,下方为CPU耗时,实时显示最近300帧情况,当当前帧耗时超过16ms时,绿色扫描线会变红色, 此图常用于观察动态过程中的“瞬时卡顿点”。


Inspector较为简单,可观看Widget树结构和实际的Render Tree结构,包含基本的布局信息,DevTools中Inspector包含更详细信息。



DevTools&Flutter Inspector


DevTools&Performance



Performance功能是性能优化的核心工具,这里可以分析出大部分UI线程、GPU线程卡顿的原因。为方便分析,此图用Debug模式得来,实际性能分析以Profile模式为准。

如上图1所示,Build函数耗时明显过长,且连续数十帧如此,必然是Build的逻辑有严重问题。理论上Widget创建一次后状态未改变时无需重建。由前文淘特案例可以发现,这里实际是业务错误的在滚动进度回调中重复创建Widget所致。实际的Build应只在瀑布流Layout逻辑中创建执行2次。

Paint函数详情可在debug模式通过debugProfilePaintsEnabled=true开启。当多变的元素与不变的元素混在同一图层时可造成图层整体的过度重复绘制, 如元素内容无变化,Paint函数中也不应出现多余元素的绘制耗时。通过前面提及的Repain RainBow开关或debugRepaintRainbowEnabled=true, 可实时观察重绘情况,如下图所示。

每一个图层都有对应的不同颜色框体。只有发生Repaint的图层颜色会发生变化,多余的图层变色,我们就要排查是否正常。


GPU耗时过多一般源于重量级组件的过度使用如Clip、Opacity、阴影, 这块发现耗时过多可参考前文解法进行优化或降级, 关于GPU更多的优化可参考liyuqian的高性能图形引擎分享。

在图1最下方的CPU Profile即代表当帧的CPU耗时情况,BottomUp方便查找最耗时的方法。

DevTools&CPU Profiler


在Performance的隔壁是CPU Profiler,这里用于统计一段时间内CPU的耗时情况,一般根据方法名结合经验判断是业务异常还是正常耗时,根据visitChilddren-->getScrollRenderObject方法名搜索,发现高可用帧率监控存在性能问题。

Devtools还有内存、Debugger、网络、日志等功能模块,这块流畅度优化中使用不多,后续有更好的经验再和大家分享。

DebugFlags&Build


上图是一张针对build阶段常见的debug功能表, debugPrintRebuildDirtyWidgets开关将在控制台打印什么树当前正在被重建,debugProfileBuildsEnabled作用同Performance的Track Widget Builds,监控Build函数详情。前3个字段在debug模式使用,最后一个可在Profile模式使用。

DebugFlag&Paint

上图是一张针对Paint阶段常见的debug功能表。debugDumpLayerTree()函数可用于打印layer树,debugPaintLayerBordersEnabled可在每一个图层周围形成边界(框),debugRepaintRainbowEnabled作用同Inspector中的RainBow Enable, 图层重绘时边框颜色将变化。debugProfilePaintsEnabled前文已提到,方便分析paint函数详情。


展望

以上便是淘特Flutter流畅度优化第一期实践,也是体感优化最明显的的一期优化。但距离极致的用户体验目标仍有不小的差距。集团同学提供了很多秀实践学习。如UC Hummer的Engine流畅度优化, 闲鱼的局部刷新复用列表组件PowerScrollView、线上线下的高精准多维度检测卡顿,及如何防止流畅度优化不恶化的方案, 淘特也在不断学习成长挑战极限,在二期实践中,为了最极致的体验,淘特将结合Hummer引擎,深度优化高性能图片库、高性能流式容器、建立全面的线下线上数据监控体系,做一个”让用户爽的淘特App“。

参考资料

收起阅读 »

React Native JSI:将BridgeModule转换为JSIModule

我们原有的项目中有大量的使用OC或者Java编写的原生模块,其中的一些可以使用C++重写,但大多数模块使用了平台特有的API和SDK,他们没有对应的C++实现。 在本文中,将带领大家如何将原有的模块转化为JSI模块。本文不再讲解基础概念,如果你有不明白的地方请...
继续阅读 »

我们原有的项目中有大量的使用OC或者Java编写的原生模块,其中的一些可以使用C++重写,但大多数模块使用了平台特有的API和SDK,他们没有对应的C++实现。


在本文中,将带领大家如何将原有的模块转化为JSI模块。本文不再讲解基础概念,如果你有不明白的地方请参考上一篇文章


使用JSI实现js与原生交互

上图描述了两端是如何进行交互的,这里面没有了React Native 的 Bridge,而是使用了C++作为中介。



  1. 在iOS端可以很简单的实现,因为OC和C++可以混编。

  2. 在Android端要麻烦一些,需要通过JNI进行C++ 与 Java的交互。


iOS端实现


首先我们在SimpleJsi.mm 中增加 getModelsetItemgetItem 用以模拟原生模块。这些方法都使用到了平台特有的API。


- (NSString *)getModel {

struct utsname systemInfo;

uname(&systemInfo);

return [NSString stringWithCString:systemInfo.machine
encoding:NSUTF8StringEncoding];
}

- (void)setItem:(NSString *)key :(NSString *)value {

NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];

[standardUserDefaults setObject:value forKey:key];

[standardUserDefaults synchronize];
}

- (NSString *)getItem:(NSString *)key {

NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];

return [standardUserDefaults stringForKey:key];
}


接下来我们需要实现一个新的install方法:


static void install(facebook::jsi::Runtime &jsiRuntime, SimpleJsi *simpleJsi) {

auto getDeviceName = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getDeviceName"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {

facebook::jsi::String deviceName =
convertNSStringToJSIString(runtime, [simpleJsi getModel]);

return Value(runtime, deviceName);
});
jsiRuntime.global().setProperty(jsiRuntime, "getDeviceName", move(getDeviceName));
}

这个方法接收两个参数。其中SimpleJsi 用来调用 getModel 方法。这个方法的返回值是NSString。我们需要将其转化为JSI认识的String类型。这里我们使用了convertNSStringToJSIString 方法。这个放开来自开源代码YeetJSIUtils


然后,我们在修改RN端,修改APP.js


const press = () => {
// setResult(global.multiply(2, 2));
// global.multiplyWithCallback(4, 5, alertResult);
alert(global.getDeviceName());
};

执行结果。


执行结果

同理,我们适配一下其他两个方法。


关键的地方还是参数的获取与转换。


auto setItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "setItem"), 2,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {
NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
NSString *value =
convertJSIStringToNSString(runtime, arguments[1].getString(runtime));

[simpleJsi setItem:key :value];

return Value(true);
});
jsiRuntime.global().setProperty(jsiRuntime, "setItem", move(setItem));


auto getItem = Function::createFromHostFunction(
jsiRuntime, PropNameID::forAscii(jsiRuntime, "getItem"), 0,
[simpleJsi](Runtime &runtime, const Value &thisValue,
const Value *arguments, size_t count) -> Value {

NSString *key =
convertJSIStringToNSString(runtime, arguments[0].getString(runtime));
facebook::jsi::String value =
convertNSStringToJSIString(runtime, [simpleJsi getItem:key]);

return Value(runtime, value);
});
jsiRuntime.global().setProperty(jsiRuntime, "getItem", move(getItem));

修改App.js


const press = () => {
global.setItem('RiverLi', '大前端');
setTimeout(() => {
alert(global.getItem('RiverLi'));
}, 300);
};

执行结果


image-20210816113702360

总结


使用JSI进行moudle开发虽然看着有些复杂,但还是值得我们花时间去研究的。因为它的性能是最佳的,没有不必要的转换,所有的操作都是那么直接的发生在一层上。





作者:RiverLi
链接:https://juejin.cn/post/6999799689155444773

收起阅读 »

ReactNative在游戏营销场景中的实践和探索-新架构介绍

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在A...
继续阅读 »

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在App中支撑了千万级DAU,也慢慢将ReactNative跨端方案运用到了游戏,来提升开发、迭代效率。本次文章我们会分5个章节介绍我们在游戏中的一些探索和实践,相信大家也能从中有所收获:

前面章节介绍了我们使用ReactNative在游戏中的一些实践,通过不断的迭代,我们完成了游戏平台的搭建,整体性能和稳定性已经达到了最优,算得上是一个比较成熟的平台了,当然该平台同样适用于现在的客户端开发,集成成本很低。但是框架本身的设计缺陷还是没有办法解决,在复杂的交互性很强的UI场景中,渲染瓶颈很明显,在游戏中也能深刻的体验到。


相信大家也看过我的另外一篇关于ReactNative架构重构的文章《庖丁解牛!深入剖析React Native下一代架构重构》,Facebook 在 2018 年 6 月官方宣布了大规模重构 React Native 的计划及重构路线图。目的是为了让 ReactNative 更加轻量化、更适应混合开发,接近甚至达到原生的体验。文章写的时间比较久了,笔者一直忙于其他事情,对于新进展更新较少,而且最初也只是初步分析了下Facebook的设计想法,经过这么久的迭代新架构有了很多进展,或者说无限接近正式release了,很值得和大家分享分享,这篇文章会向大家更深层次介绍新架构的现状和开发流程。


下面我们会从原理上简单介绍新架构带来的一些变化,下图是新老架构的变化对比:



相信大家也能从中发现一些区别,原有架构JS层与Native的通讯都过多的依赖bridge,而且是异步通讯,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,而新架构正是从这点,对bridge这层做了大量的改造,使得UI和API调用,从原有异步方式,调整到可以同步或者异步与Native通讯,解决了需要频繁通讯的瓶颈问题。




  1. 旧架构设计




在了解新架构前,我们还是先聊下目前的ReactNative框架的主要工作原理,这样也方便大家了解整体架构设计,以及为什么facebook要重构整个框架:



  • ReactNative是采用前端的方式及UI渲染了原生的组件,他同时提供了API和UI组件,也方便开发者自己设计、扩展自己的API,提供了ReactContextBaseJavaModule、ViewGroupManager,其中ReactNative的UI是通过UIManger来管理的,其实在Android端就是UIManagerModule,原理上也是一个BaseJavaModule,和API共享一个native module。

  • ReactNative页面所有的API和UI组件都是通过ReactPackageManger来管理的,引擎初始化instanceManager过程中会读取注入的package,并根据名称生成对应的NativeModule和Views,这里还仅仅是Java层的,实际在C++层会对应生成JNativeModule

  • 切换到以上架构图的部分来看,Native Module的作用就是打通了前端到原生端的API调用,前端代码运行在JSC的环境中,采用C++实现,为了打通到native调用,需要在运行前注入到global环境中,前端通过global对象来操作proxy Native Module,继而执行了JNativeModule





  • 前端代码render生成UI diff树后,通过ReactNativeRenderer来完成对原生端的UIManager的调用,以下是具体的API,主要作用是通知原生端创建、更新View、批量管理组件、measure高度、宽度等:




  • 通过上述一系列的API操作后,会在原生端生成shadow tree,用来管理各个node的关系,这点和前端是一一对应的,然后待整体UI刷新后,更新这些UI组件到ReactRootView


通过上面的分析,不难发现现在的架构是强依赖nativemodule,也就是大家通常说的bridge,对于简单的Native API调用来说性能还能接受,而对于UI来说,每次的操作都是需要通过bridge的,包括高度计算、更新等,且bridge限制了调用频率、只允许异步操作,导致一些前端的更新很难及时反应到UI上,特别是类似于滑动、动画,更新频率较高的操作,所以经常能看到白屏或者卡顿。





  1. 新架构设计




旧的架构JS层与Native的通讯都太依赖bridge,导致一些通讯频率较高的交互和设计就很难实现,同时也影响了渲染性能,这就是Facebook这次重构的主要目标,在新的设计上,ReactNative提出了几个新的概念和设计:



  1. JSI(javascript interface):这是本次架构重构的核心重点,也正是因为这层的调整,将原有重度依赖的native bridge架构解耦,实现了自由通讯。

  2. Fabric:依赖JSI的设计,并将旧架构下的shadow tree层移到C++层,这样可以透过JSI,实现前端组件对UI组件的一对一控制,摆脱了旧架构下对于UI的异步、批量操作。

  3. TuborModule:新的原生API架构,替换了原有的java module架构,数据结构上除了支持基础类型外,开始支持JSI对象,让前端和客户端的API形成一对一的调用

  4. 社区化:在不断迭代中,facebook团队发现,开源社区提供的组件和API越来越多,而且很多组件设计和架构上比ReactNative要好,而且官方组件因为资源问题,投入度并不够,对于一些社区问题的反馈,响应和解决问题也不太及时。社区化后,大量的系统组件会开放到社区中,交个开发者维护,例如现在的webview组件


上面这些概念其实在架构图上已经体现了,主要用于替换原有的bridge设计,下面我们将重点剖析这些模块的原理和作用:


JSI :


JSI在0.60后的版本就已经开始支持,它是Facebook在js引擎上设计的一个适配架构,允许我们向 Javascript 运行时注册方法的 Javascript 接口,这些方法可通过 Javascript 世界中的全局对象获得,可以完全用 C++ 编写,也可以作为一种与 iOS 上的 Objective C 代码和 Android 中的 Java 代码进行通信的方式。任何当前使用Bridge在 Javascript 和原生端之间进行通信的原生模块都可以通过用 C++ 编写一个简单的层来转换为 JSI 模块



  • 标准化的JS引擎接口,ReactNative可以替换v8、Hermes等引擎。




  • 它是架起 JS 和原生 java 或者 Objc 的桥梁,类似于老的 JSBridge架构的作用,但是不同的是采用的是内存共享、代理类的方式,JS所有的运行环境都是在 JSRuntime 环境下的,为了实现和 native 端直接通讯,我们需要有一层 C++ 层实现的 JSI::HostObject,该数据结构只有 get、set 两个接口,通过 prop 来区分不同接口的调用。




  • 原有JS与Native的数据沟通,更多的是采用json和基础类型数据,但有了JSI后,数据类型更丰富,支持JSI object。


所以API调用流程: JS->JSI->C++->JNI->JAVA,每个API更加独立化,不再全部依赖native module,但这也带来了另外一个问题,相比以前的设计更复杂了,设计一个API,开发者需要封装JS、C++、JNI、Java等一套接口。当然Facebook早已经想到了这个问题,所以在设计JSI的时候,就提供了一个codegen模块,帮忙大家完成基础代码和环境的搭建,以下我们会简单为大家介绍怎么使用这些工具:



  1. Facebook提供了一个脚手架工程,方便大家创建Native Module 模块,需提前增加npx命令


npx create-react-native-library react-native-simple-jsi


前面的步骤更多的是在配置一些模块的信息,值得注意的是在选择模块的开发语言时要注意,这边是支持很多种类型的,针对原生端开发我们用Java&OC比较多,也可以选择纯JS 或者C++的类型,大家根据自己的实际情况来选择,完成后需要选择是UI模块还是API模块,这里我们选择API(Native Module)来做测试:



以上是完成后的目录结构,大家可以看到这是个完整的ReactNative App工程,相应的API需要开发者在对应的Android、iOS目录中开发。



下面我们看下C++ Moulde的模式,相比Java模式,多了cpp 模块,并在Moudle中以Native lib的方式加载so:





  1. 其实到这里我们还是没有创建JSI的模块,删掉删掉example目录后,运行下面命令,完成后在Android studio中导入 example/android,编译后app 工程,就能打包我们cpp目录下的C++文件到so


npx react-native init example
cd example
yarn add ../



  1. 到这里我们完成了C++库的打包,但是不是我们想要的JSI Module,需要修改Module模块,代码如下,从代码中我们可以看到,不再有reactmethod标记,而是直接的一些install方法,在这个JSI Module 创建的时候调用注入环境


public class NewswiperJsiModule extends ReactContextBaseJavaModule {
public static final String NAME = "NewswiperJsi";
public NewswiperJsiModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
@NonNull
public String getName() {
return NAME;
}

static {
try {
// Used to load the 'native-lib' library on application startup.
System.loadLibrary("cpp");
} catch (Exception ignored) {
}
}

private native void nativeInstall(long jsi);

public void installLib(JavaScriptContextHolder reactContext) {
if (reactContext.get() != 0) {
this.nativeInstall(
reactContext.get()
);
} else {
Log.e("SimpleJsiModule", "JSI Runtime is not available in debug mode");
}
}
}

public class SimpleJsiModulePackage implements JSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
reactApplicationContext.getNativeModule(SimpleJsiModule.class).installLib(jsContext);
return Collections.emptyList();
}
}


  1. 后面就是我们要创建JSI Object了,用来直接和JS通讯,主要是通过createFromHostFunction 来创建JSI的代理对象,并通过global().setProperty注入到JS运行环境


void install(Runtime &jsiRuntime) {
auto multiply = Function::createFromHostFunction(jsiRuntime,
PropNameID::forAscii(jsiRuntime,
"multiply"),
2,
[](Runtime &runtime,
const Value &thisValue,
const Value *arguments,
size_t count) -> Value {
int x = arguments[0].getNumber();
int y = arguments[1].getNumber();

return Value(x * y);

});

jsiRuntime.global().setProperty(jsiRuntime, "multiply", move(multiply));

global.multiply(2,4) // 8

到这里相信大家知道了怎么通过JSI完成JSIMoudle的搭建了,这也是我们TurboModule和Fabric设计的核心底层设计。


Fabric :


Fabric是新架构的UI框架,和原有UImanager框架是类似,前面章节也说明UIManager框架的一些问题,特别在渲染性能上的瓶颈,似乎基于原有架构已经很难再有优化,体验上与原生端组件和动画的渲染性能还是差距比较大的,举个比较常见的问题,Flatlist快速滑动的状态下,会存在很长的白屏时间,交互比较强的动画、手势很难支持,这也是此次架构升级的重点,下面我们也从原理上简单说明下新架构的特点:



  1. JS层新设计了FabricUIManager,目的是支持Fabric render完成组件的更新,它采用了JSI的设计,可以和cpp层沟通,对应C++层UIManagerBinding,其实每个操作和API调用都有对应创建了不同的JSI,从这里就彻底解除了原有的全部依赖UIManager单个Native bridge的问题,同时组件大小的measure也摆脱了对Java、bridge的依赖,直接在C++层shadow完成,提升渲染效率


export type Spec = {|
+createNode: (
reactTag: number,
viewName: string,
rootTag: RootTag,
props: NodeProps,
instanceHandle: InstanceHandle,
) => Node,
+cloneNode: (node: Node) => Node,
+cloneNodeWithNewChildren: (node: Node) => Node,
+cloneNodeWithNewProps: (node: Node, newProps: NodeProps) => Node,
+cloneNodeWithNewChildrenAndProps: (node: Node, newProps: NodeProps) => Node,
+createChildSet: (rootTag: RootTag) => NodeSet,
+appendChild: (parentNode: Node, child: Node) => Node,
+appendChildToSet: (childSet: NodeSet, child: Node) => void,
+completeRoot: (rootTag: RootTag, childSet: NodeSet) => void,
+measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
+measureInWindow: (
node: Node,
callback: MeasureInWindowOnSuccessCallback,
) => void,
+measureLayout: (
node: Node,
relativeNode: Node,
onFail: () => void,
onSuccess: MeasureLayoutOnSuccessCallback,
) => void,
+configureNextLayoutAnimation: (
config: LayoutAnimationConfig,
callback: () => void, // check what is returned here
// This error isn't currently called anywhere, so the `error` object is really not defined
// $FlowFixMe[unclear-type]
errorCallback: (error: Object) => void,
) => void,
+sendAccessibilityEvent: (node: Node, eventType: string) => void,
|};

const FabricUIManager: ?Spec = global.nativeFabricUIManager;

module.exports = FabricUIManager;

if (methodName == "createNode") {
return jsi::Function::createFromHostFunction(
runtime,
name,
5,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const &thisValue,
jsi::Value const *arguments,
size_t count) noexcept -> jsi::Value {
auto eventTarget =
eventTargetFromValue(runtime, arguments[4], arguments[0]);
if (!eventTarget) {
react_native_assert(false);
return jsi::Value::undefined();
}
return valueFromShadowNode(
runtime,
uiManager->createNode(
tagFromValue(arguments[0]),
stringFromValue(runtime, arguments[1]),
surfaceIdFromValue(runtime, arguments[2]),
RawProps(runtime, arguments[3]),
eventTarget));
});
}


  1. 有了JSI后,以前批量依赖bridge的UI操作,都可以同步的执行到c++层,而在c++层,新架构完成了一个shadow层的搭建,而旧架构是在java层实现,以下也重点说明下几个重要的设计:



  • FabricUIManager (JS,Java) ,JS 端和原生端 UI 管理模块。




  • UIManager/UIManagerBinding(C++),C++中用来管理UI的模块,并通过binding JNI的方式通过FabricUIManager(Java)管理原生端组件




  • ComponentDescriptor (C++) ,原生端组件的唯一描述及组件属性定义,并注册在CoreComponentsRegistry模块中




  • Platform-specific




  • Component Impl (Java,ObjC++),原生端组件Surface,通过FabricUIManager来管理




  1. 新架构下,开发一个原生组件,需要完成Java层的原生组件及ComponentDescriptor (C++) 开发,难度相较于原有的viewManager有所提升,但ComponentDescriptor本身很多是shadow层代码,比较固定,Facebook后续也会提供codegen工具,帮助大家完成这部分代码的自动生成,简化代码难度



TurboModule:


实际上0.64版本已经支持TurboModule,在分析它的设计原理前,我们先说明下设计这个模块的目的,从上面架构图来看,主要用来替换NativeModule的重要一环:



  1. NativeModule 会包含很多我们初始化过程中就需要注册的的API,随着开发迭代,依赖NativeMoude的API和package会越来越多,解析及校验这些pakcages的时间会越来越长,最终会影响TTI时长

  2. 另外Native module其实大部分都是提供API服务,其实是可以采用单例子模式运行的,而不用跟随bridge的关闭打开,创建很多次


TurboModule的设计就是为了解决这些问题,原理上还是采用JSI提供的能力,方便JS可以直接调用到c++ 的host object,下面我们从代码层简单分析原理:



上面代码就是目前项目里面给出的一个例子,通过实现TurboModule来完NativeModule的开发,其实代码流程和原有的BaseJavaModule大致是一样的,不同的是底层的实现:



  1. 现有版本可以通过 ReactFeatureFlags.useTurboModules来打开这个模块功能

  2. TurboModule 组件是通过TurboModuleManager.java来管理的,被注入的modules可以分为初始化加载的和非初始化加载的组件

  3. 同样JNI/C++层也有一层TurboModuleManager用来管理注册java/C++的module,并通过TurboModuleBinding C++层的proxy moudle注入到JS层,到这里基本就和上面说的基础架构JSI接上轨了,js中可以通过代理的__turboModuleProxy来完成c++层的module调用,c++层透过jni最终完成对java代码的执行,这里facebook设计了两种类型的moudles,longLivedObject 和 非常驻的,设计思路上就和我们上面要解决的问题吻合了


void TurboModuleBinding::install(
jsi::Runtime &runtime,
const TurboModuleProviderFunctionType &&moduleProvider) {
runtime.global().setProperty(
runtime,
"__turboModuleProxy",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "__turboModuleProxy"),
1,

// Create a TurboModuleBinding that uses the global
// LongLivedObjectCollection
[binding =
std::make_shared<TurboModuleBinding>(std::move(moduleProvider))](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
return binding->jsProxy(rt, thisVal, args, count);
}));
}

const NativeModules = require('../BatchedBridge/NativeModules');
import type {TurboModule} from './RCTExport';
import invariant from 'invariant';

const turboModuleProxy = global.__turboModuleProxy;

function requireModule<T: TurboModule>(name: string): ?T {
// Bridgeless mode requires TurboModules
if (!global.RN$Bridgeless) {
// Backward compatibility layer during migration.
const legacyModule = NativeModules[name];
if (legacyModule != null) {
return ((legacyModule: $FlowFixMe): T);
}
}

if (turboModuleProxy != null) {
const module: ?T = turboModuleProxy(name);
return module;
}

return null;
}

CodeGen:



  1. 新架构UI增加了C++层的shadow、component层,而且大部分组件都是基于JSI,因而开发UI组件和API的流程更复杂了,要求开发者具有c++、JNI的编程能力,为了方便开发者快速开发Facebook也提供了codegen工具,帮助生成一些自动化的代码,具体工具参看:github.com/facebook/re…

  2. 以下是代码生成的大概流程,因codegen目前还没有正式release,关于如何使用的文档几乎没有,但也有开发者尝试使用生成了一些代码,可以参考github.com/karol-biszt…





  1. 总结:




上面我们从API、UI角度重新学习了新架构,JSI、Turbormodule已经在最新的版本上已经可以体验,而且开发者社区也用JSI开发了大量的API组件,例如以下的一些比较依赖C++实现的模块:

























从最新的代码结构来看,新架构离发布似乎已经进入倒计时了,作为一直潜心学习、研究ReactNative的开发者相信一定和我一样很期待,从Facebook官方了解到Facebook App已经采用了新的架构,预计今年应该就能正式release了,这一次我们可以相信ReactNative应该要正式进入1.0版本了吧,reactnative.dev/blog/2021/0…





链接:https://juejin.cn/post/7000634295668703246

收起阅读 »

ReactNative在游戏营销场景中的实践和探索-性能优化

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在A...
继续阅读 »

客户端跨端框架已经发展了很多年了,最近比较流行的小程序、Flutter、ReactNative,都算是比较成功、成熟的框架,面向的开发者也不一样,很多大型App都广泛的使用了,笔者有幸很早就参与学习使用了这些优秀的跨端方案,在这几年的开发和架构设计中,除了在App中支撑了千万级DAU,也慢慢将ReactNative跨端方案运用到了游戏,来提升开发、迭代效率。本次文章我们会分5个章节介绍我们在游戏中的一些探索和实践,相信大家也能从中有所收获:

(随着版本不断迭代完善,基本具有大量上线游戏的能力,随着游戏业务越来越多,在不同的游戏环境中,也碰到不少问题,这也从侧面体现出了游戏场景和架构的复杂性,主要核心问题还是在于ReactNative的沉浸式体验、启动性能、内存、渲染性能问题等,似乎这些问题也是ReactNative的通病,为了解决这些问题,我们开始专项优化。


1. 启动性能优化


针对启动性能问题,我们也测试列大量数据,ReactNative在纯客户端App中,性能表现还算不错,但在游戏低内存、cpu过度占用的情况下,该问题显得格外突出,要解决这些问题,首先我们需要了解ReactNative加载的主要时间消耗,可以参考下图:



整体页面渲染显示前,需要首先加载加载初始化 React Native Core Bridge,主要包含ReactNative的运行环境、UI和API组件功能等,然后才能运行业务的 JS,执行render绘制UI,完成后,React Native 才能将 JS 的组件渲染成原生的组件。因页面的加载流程是固定不变的,所以我们可以采用了提前预加载Core bridge的方案来提升加载性能,当游戏营销页面启动前,预先加载好原生端bridge,这样在打开业务是指需要运行前端JS代码渲染,设计思路上我们也根据业务场景设计了模式:



  • 预加载业务包:提前加载好完整的业务包到内存,生成并缓存ReactInstanceManager对象,在业务启动时,从内存缓存中获取该对象,并直接运行绑定rootview,经过改造,该方案能提升整体的打开速度30%-50%左右,游戏环境下,手机设备基本都达到秒开,模拟器设备在2s内,但这种通过内存换取速度的方法,在业务量大后,很明显是不可取的,所以整包预加载的局限性比较强。




  • Common包预加载:针对全包预加载的局限性,我们提出了分包方案,预加载common包,研究发现ReactNative打包生成的业务包其实有两部分内容,一部分是公共的基础组件、API包,统称common包,一部分是业务的核心逻辑包。改造打包方式,可以把原有的全包模式分离成common+bussiness,在多业务包模式下,可以共享统一的common包,在打开业务前,我们会优先预加载common包,并缓存对应的ReactInstanceManager对象,用户触发打开业务后,再加载bussiness 包,该方案相对于全包预加载性能略差,但比不预加载能提升15%-20%左右,同时支持多业务运行环境,具体思路可以参考开源项目react-native-multibundler




  • 从时序运行上,除了core bridge的初始化外,js 运行到页面显示,实际上也占用了不少时间,在预加载core bridge上,我们更近一步,支持了预加载rootview,提前将要渲染页面的rootview运行起来缓存在内存,当然这里加载的还是基础模块,在业务打开时,路由触发展示页面即可,可以做到页面无延时打开,但是对内存的开销,比预加载core bridge 更高。


当然上述方案都是通过内存换性能,不同的加载方式都做到了云控,随时切换、关闭。除了这些方案外同样还有其他方式能优化启动性能:



  1. Lazy module,将引擎自定义的API Native Module改造成懒加载方式,整体性能提升在5% 左右。

  2. 业务代码做到按需require,不需要展示的部分,采用lazy require,提升页面的显示、渲染速度。

  3. 裁剪业务包,将业务代码没有用到React的module、API、组件删除,减少业务包大小来提升启动性能。

  4. 分包方案,从测试数据来看,业务包越小,启动性能越好,包大小无法减小后,将业务包按照路由拆分为子包,也能立竿见影的解决启动速度问题。将业务包按照路由页面和功能分成多个子的业务子包,让首屏业务逻辑包变小,做到按需加载其他业务包,提升首页启动性能。


这些方案都从引擎加载的角度解决了启动性能慢,做到了按需加载,整体性能达到了最优化。但是在游戏中,业务页面的显示还是太依赖服务度请求来完成页面的渲染,所以在逐步优化后,发现网络请求对于页面的显示也占了很大一部分,为了进一步提升首屏显示,我么增加了网络请求预拉取、图片预缓冲方案:



  1. 网络预拉取,对于一些对首屏显示影响较大的网络请求,在引擎加载后,在合适时机从云控平台获取后,根据配置拉取并缓存到内存,打开业务后,优先从缓存中读取网络接口内容并显示。

  2. 图片预缓存,对于一些加载较慢的图片,将链接配置到云端后,在合适时机提前预加载到Fresco内存,页面打开后Fresco会从缓存中直接读取bitmap


除了这些方案外,替换JSC引擎到hermes,也能很好的解决启动性能问题,后面章节会重点介绍。


2. 内存优化


以上所有的优化更多是针对启动性能的优化设计,也是业内用于提升加载性能的方案,在游戏的复杂环境下,除了性能外,对于内存的要求也是很严格的,游戏启动后,本身对于内存的消耗就比一般的原生app高,所以在内存使用上会更精确和严格,那ReactNative是怎么优化内存的:

分包方案,分包方案除了在启动速度上有很大优化外,实现了按需加载,对于内存来说也做到了最优化。

字体加载,因游戏字体库无法和原生字体共享,导致在ReactNative页面使用字体会大大增加整体的内存,为了降低字体的内存,我们支持了字体的裁剪方案,按需打入字体,删掉一些生僻的字,大大降低了字体包的大小。另外字体文件对于业务包大小影响也比较大,我们支持字体的动态下发和加载。

图片优化,除了业务UI和JS本身占用的内存外,内存上占用比较大的是图片,而且图片有缓存,为了降低图片的内存消耗,我们支持了webp、gif等格式的图片,有损压缩,同时对于网络图片做到了按手机分辨率下发。另外提供API到前端业务,按需清理不使用的图片,及时释放内存,并控制图片缓存大小。


3. 渲染性能


除了内存、启动性能外,在游戏中的渲染性能也至关重要,ReactNative受限于游戏内的内存和CPU负载高,同等复杂度页面,表现不如原生App。为了能优化这些指标,我们对ReactNative的渲染流程做了分析和优化,支持静止状态下帧率基本达到了60fps,大致优化如下:

ReactNative是前端事件驱动原生UI渲染的,所以设计上ReactNative会在Frame Buffer每一帧绘画结束后的回调在UI线程中处理UI更新,即使没有更新的情况下也会空运转,这在UI线程负载本就较高的游戏中,增加了UI的负担

动画、点击事件都是同样的设计,会不断的有任务空转占用UI线程,增加了UI线程每次绘制的时间

解决这个问题,就是要支持资源的按需加载,我们将动画、UI更新事件放到了消息map,每次一帧渲染完成后,我们会检查map消息,是否有需要处理的消息,没有后续就不再在一帧渲染完成后调度UI线程,当用户触发了动画或者UI更新,会发送消息map,并注册帧渲染的callback,在callback中检查map消息更新UI


另外ReactNative采用的是原生UI渲染,在打开硬件加速的情况,整体渲染性能表现比较高,但是在游戏环境中,大部分游戏都是不开硬件加速的(自渲染组件和引擎的缘故),对于比较复杂的ReactNative UI,更新UI时整体FPS会偏低,UI响应会比较慢,特别是在模拟器(限制fps30)的情况下,渲染性能更加差强人意。在复杂交互的情况,要怎么提升性能?

简单的UI设计,没有大图背景的情况下,不开硬件加速,整体渲染性还不算差,但有大的背景情况下,UI性能表现尤其差,所以解决渲染问题,其实更多的是要解决大图渲染的问题

ReactNative 提供了renderToHardwareTextureAndroid 来用native内存换渲染的性能,导致的问题是内存消耗较高,对于图片不是太多、内存限制不是很严格的业务,可以采用该方式提升性能

对于大量使用图片的业务,我们设计一套采用opengl渲染方式的组件,支持纹理图(比较通用的etc1),从内存和渲染性能上,明显都得到了很大的提升,但这种模式依赖硬件加速,所以一般是在Dialog窗口模式中使用,具体的实现原理,大家可以关注作者文章,后面会详细和大家分享


核心示例代码:


 /* GLES20.glCompressedTexImage2D(target, 0, ETC1.ETC1_RGB8_OES , bitmap.getWidth(), bitmap.getHeight(), 0, etc1tex.getData().capacity(), etc1tex.getData());*/

链接:https://juejin.cn/post/7000631869628743688

收起阅读 »

ReactNative——react-native-video实现视频全屏播放

react-native-video是github上一个专用于React Native做视频播放的组件。这个组件是React Native上功能最全最好用的视频播放组件,还在持续开发之中,虽然还有些bug,但基本不影响使用,强力推荐。 本篇文章主要介绍下怎么使...
继续阅读 »

react-native-video是github上一个专用于React Native做视频播放的组件。这个组件是React Native上功能最全最好用的视频播放组件,还在持续开发之中,虽然还有些bug,但基本不影响使用,强力推荐。


本篇文章主要介绍下怎么使用react-native-video播放视频,以及如何实现全屏播放,屏幕旋转时视频播放器大小随之调整,显示全屏或收起全屏。


首先来看看react-native-video有哪些功能。


基本功能



  1. 控制播放速率

  2. 控制音量大小

  3. 支持静音功能

  4. 支持播放和暂停

  5. 支持后台音频播放

  6. 支持定制样式,比如设置宽高

  7. 丰富的事件调用,如onLoad,onEnd,onProgress,onBuffer等等,可以通过对应的事件进行UI上的定制处理,如onBuffer时我们可以显示一个进度条提示用户视频正在缓冲。

  8. 支持全屏播放,使用presentFullscreenPlayer方法。这个方法在iOS上可行,在android上不起作用。参看issue#534,#726也是同样的问题。

  9. 支持跳转进度,使用seek方法跳转到指定的地方进行播放

  10. 可以加载远程视频地址进行播放,也可以加载RN本地存放的视频。


注意事项


react-native-video通过source属性设置视频,播放远程视频时使用uri来设置视频地址,如下:


source={{uri: "http://www.xxx.com/xxx/xxx/xxx.mp4"}}

播放本地视频时,使用方式如下:


source={require('../assets/video/turntable.mp4')}

需要注意的是,source属性不能为空,uri或本地资源是必须要设置的,否则会导致app闪退。uri不能设置为空字符串,必须是一个具体的地址。


安装配置


使用npm i -S react-native-videoyarn add react-native-video安装,完成之后使用react-native link react-native-video命令link这个库。


Android端在执行完link命令后,gradle中就已经完成了配置。iOS端还需要手动配置一下,这里简单说一下,与官方说明不同的是,我们一般不使用tvOS的,选中你自己的target,在build phases中先移除掉自动link进来的libRCTVideo.a这个库,然后点击下方加号重新添加libRCTVideo.a,注意不要选错。


视频播放


实现视频播放其实很简单,我们只需要给Video组件设置一下source资源,然后设置style调整Video组件宽高就行了。



    ref={(ref) => this.videoPlayer = ref}
source={{uri: this.state.videoUrl}}
rate={1.0}
volume={1.0}
muted={false}
resizeMode={'cover'}
playWhenInactive={false}
playInBackground={false}
ignoreSilentSwitch={'ignore'}
progressUpdateInterval={250.0}
style={{width: this.state.videoWidth, height: this.state.videoHeight}}
/>

其中videoUrl是我们用来设置视频地址的变量,videoWidth和videoHeight是用来控制视频宽高的。


全屏播放的实现


视频全屏播放其实就是在横屏情况下全屏播放,竖屏一般都是非全屏的。要实现设备横屏时视频全屏显示,说起来很简单,就是通过改变Video组件宽高来实现。


上面我们把videoWidth和videoHeight存放在state中,目的就是为了通过改变两个变量的值来刷新UI,使视频宽高能随之改变。问题是,怎样在设备的屏幕旋转时及时获取到改变后的宽高呢?


竖屏时我设置的视频初始宽度为设备屏幕的宽度,高度为宽度的9/16,即按16:9的比例显示。横屏时视频的宽度应为屏幕的宽度,高度应为当前屏幕的高度。由于横屏时设备宽高发生了变化,及时获取到宽高就能及时刷新UI,视频就能全屏展示了。


刚开始我想到的办法是使用react-native-orientation监听设备转屏的事件,在回调方法中判断当前是横屏还是竖屏,这个在iOS上是可行的,但是在Android上横屏和竖屏时获取到宽高值总是不匹配的(比如,横屏宽384高582,竖屏宽582高384,显然不合理),这样就无法做到统一处理。


所以,监听转屏的方案是不行的,不仅费时还得不到想要的结果。更好的方案是在render函数中使用View作为最底层容器,给它设置一个"flex:1"的样式,使其充满屏幕,在View的onLayout方法中获取它的宽高。无论屏幕怎么旋转,onLayout都可以获取到当前View的宽高和x、y坐标。


/// 屏幕旋转时宽高会发生变化,可以在onLayout的方法中做处理,比监听屏幕旋转更加及时获取宽高变化
_onLayout = (event) => {
//获取根View的宽高
let {width, height} = event.nativeEvent.layout;
console.log('通过onLayout得到的宽度:' + width);
console.log('通过onLayout得到的高度:' + height);

// 一般设备横屏下都是宽大于高,这里可以用这个来判断横竖屏
let isLandscape = (width > height);
if (isLandscape){
this.setState({
videoWidth: width,
videoHeight: height,
isFullScreen: true,
})
} else {
this.setState({
videoWidth: width,
videoHeight: width * 9/16,
isFullScreen: false,
})
}
};

这样就实现了屏幕在旋转时视频也随之改变大小,横屏时全屏播放,竖屏回归正常播放。注意,Android和iOS需要配置转屏功能才能使界面自动旋转,请自行查阅相关配置方法。


播放控制


上面实现了全屏播放还不够,我们还需要一个工具栏来控制视频的播放,比如显示进度,播放暂停和全屏按钮。具体思路如下:



  1. 使用一个View将Video组件包裹起来,View的宽高和Video一致,便于转屏时改变大小

  2. 设置一个透明的遮罩层覆盖在Video组件上,点击遮罩层显示或隐藏工具栏

  3. 工具栏中要显示播放按钮、进度条、全屏按钮、当前播放时间、视频总时长。工具栏以绝对位置布局,覆盖在Video组件底部

  4. 使用react-native-orientation中的lockToPortrait和lockToLandscape方法强制旋转屏幕,使用unlockAllOrientations在屏幕旋转以后撤销转屏限制。


这样才算是一个有模有样的视频播放器。下面是竖屏和横屏的效果图




再也不必为presentFullscreenPlayer方法不起作用而烦恼了,全屏播放实现起来其实很简单。具体代码请看demo:github.com/mrarronz/re…


总结



  1. react-native-orientation和react-native-video都还有缺陷,但是已经可以运用到项目中了

  2. 有时候解决问题要换种思路,不能一棵树上吊死。坐下来喝杯茶,换种心态、换个搜索关键词说不定就得到了你想要的答案。

作者:不變旋律
链接:https://juejin.cn/post/6844903570999869448

收起阅读 »

被React Native插件狂虐2天之后,写下c++_share.so冲突处理心路历程

为了应对活体检测客户 react-native 端的支持,需要开发 react-native 插件供客户使用。关于react-native 插件开发具体可以参考react官网: reactnative.cn/docs/native… reactnative....
继续阅读 »

为了应对活体检测客户 react-native 端的支持,需要开发 react-native 插件供客户使用。关于react-native 插件开发具体可以参考react官网:



具体包含两部分



  1. ViewManager:包装原生的 view 供 react-native 的 js 部分使用

  2. NativeModule:提供原生的 api 能力供 react-native 的 js 部分调用


心路历程


参考着官方事例,插件代码很快就完成。开开心心把插件发布到 github 之后试用了一下就遇到了第一个问题


image.png


看错误很容易发现是 so 冲突了,也就是说 react-native 脚手架创建的项目原本就存在libc++_share.so,正好我们的活体检测 sdk 也存在 libc++_shared.so。冲突的解决方法也很简单,在 android 域中添加如下配置:


packagingOptions {
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
}

这边顺便解释下packagingOptions中几个关键字的意思和作用

关键字含义实例
doNotStrip可以设置某些动态库不被优化压缩doNotStrip '*/arm64-v8a/libc++_shared.so'
pickFirst匹配到多个相同文件,只提取第一个pickFirst 'lib/arm64-v8a/libc++_shared.so'
exclude过滤掉某些文件或者目录不添加到APK中exclude 'lib/arm64-v8a/libc++_shared.so'
merge将匹配的文件合并添加到APK中merge 'lib/arm64-v8a/libc++_shared.so'

上述例子中处理的方式是遇到冲突取第一个libc++_shared.so。冲突解决之后继续运行,打开摄像头过一会儿就崩溃了,报错如下:


com.awesomeproject A/libc: Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 30755 (work), pid 30611 (.awesomeproject)

从报错信息来看只知道错误的地方在jni部分,具体在什么位置?哪行代码?一概不知。从现象来看大致能猜到错误的入口,于是逐行代码屏蔽去试,最后定位到报错的代码竟然是:


std::cout << "src: (" << h << ", " << w << ")" << std::endl;

仅仅是简单的c++输出流,对功能本来没有影响。很好奇为什么会崩溃,查了好久一无所获。既然不影响功能就先删掉了这行代码,果然就不报错了,功能都能正常使用了,开开心心的交给测试回归。一切都是好好的,直到跑在arm64-v8a的设备上,出现了如下报错:


1e14141e-51c8-42e7-a5c7-440905742247.png


这次有明显的报错信息,意思是当运行opencv_java3.so的时候缺少_sfp_handler_exception函数,这个函数实际上是在c++_shared.so库中的。奇怪的是原生代码运行在arm64-v8a的设备上是好的,那怎么跑在react-native环境就会缺少_sfp_handler_exception函数了呢?
直到我在原生用ndk20a编译代码报了同样的错误,才意识到一切问题的源头是pickFirst引起的。


4a1a64b0-296d-4753-abc1-92da09d60cde.png


a4d4f827-ccea-4817-9175-e47458f1c917.png


可以明显的看到react-native和原生环境跑出来的apk包中c++_shared.so的大小是不同的。
也就是说pickFirst是存在安全隐患的,就拿这个例子来说,假如两个c++_shared.so是用不同版本的ndk打出来的,其实内部的库函数是不一样的,pickFirst贸然选择第一个必然导致另外的库不兼容。那么是不是可以用merge合并两个c++_shared.so,试了一下针对so merge失效了,只能是另辟蹊径。
如果我们的sdk只有一个库动态依赖于c++_shared.so,大可把c++_shared.so以静态库的方式打入,这样就不会有so冲突问题,同时也解决了上述问题。配置如下:


externalNativeBuild {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a"
}
cmake {
cppFlags "-std=c++11 -frtti -fexceptions"
arguments "-DANDROID_STL=c++_shared" //shared改为static
}
}

可惜的是例子中的sdk不止一个库动态依赖于c++_shared.so,所以这条路也行不通。那么只能从react-native侧出发寻找方案。


方案一(推荐)


找出react-native这边的c++_shared.so是基于什么ndk版本打出来的,想办法把两端的ndk版本保持统一,问题也就迎刃而解了。


b2d4115d-0316-47c5-a14f-3dd5daf167f9.png


从react-native对应的android工程的蛛丝马迹中发现大概是基于ndk r20b打出来的。接下来就是改造sdk中c++_shared.so基于的ndk版本了。



  1. 基于ndk r20b版本重新编译opencv库

  2. 把opencv库连接到项目,基于ndk r20b版本重新编译alive_detected.so库


把编译好的sdk重新导入插件升级,运行之后果然所有的问题得以解决。


方案二


去除react-native中的c++_shared.so库,react-native并不是一开始就引入了c++_shared.so。从React Native版本升级中去查看c++_shared.so是哪个版本被引入的,可以发现0.59之前的版本是没有c++_shared.so库的,详见对比:


bd504920-1855-445d-8f8f-cf4b6e4feabd.png


4a77bb53-f0ad-45b4-862c-2e264b88db9d.png


那么我们把react-native版本降级为0.59以下也能解决问题,降级步骤如下:



  1. 进入工程


cd Temple


  1. 指定版本


npm install --save react-native@0.58.6


  1. 更新


react-native upgrade


  1. 一路替换文件


fdf99f54-b121-4321-8956-6e3bce7efb99.png


总结


Android开发会面临各种环境问题,遇到问题还是要从原理出发,理清问题发生的根源,这样问题就很好解决。



链接:https://juejin.cn/post/6976473129262514207

收起阅读 »

React Native 团队怎么看待 Flutter 的?终于有官方回复了

昨天 React Native 官方团队在 reddit 上发起了一次 AUA(ask us anything)活动,地址在文末。看到这个活动的时候,我脑海里想到的第一个问题就是,他们怎么看待 Flutter 的?结果打开活动后,发现已经有人问了,而且还得到了...
继续阅读 »

昨天 React Native 官方团队在 reddit 上发起了一次 AUA(ask us anything)活动,地址在文末。看到这个活动的时候,我脑海里想到的第一个问题就是,他们怎么看待 Flutter 的?结果打开活动后,发现已经有人问了,而且还得到了官方的回复。



提问者:



你们是怎么看待 Flutter 的,和 Flutter 比起来 React Native 有什么优劣?



官方回复:



我认为 React Native 和 Flutter 的目标是完全不同的,因此在实现上也采取了完全不同的方法,所以如何看待二者,就取决于你要达到什么样的目的了。举例来说,React Native 更倾向于将每个平台各自的特性和组件样式进行保留,而 Flutter 是通过自己的渲染引擎去渲染组件样式,以代替平台原生的效果。这取决于你想做什么以及想做成什么样,这个应该就是你最需要考虑的事情了。



话里有话:



看完了也没说哪里好,哪里不好,很标准的官方回复。看来是早就想好了答案,算准了肯定会有人问这个。而且看完这个回复,我感觉像是在说:“小孩才做选择,大人就都要!”





除了这个绕不开的问题以外,还有一个我认为比较关键的问题,就是关于 React Native 未来的发展。当然,这个问题也有人问了,就排在热门第一个。



提问者:



React Native 已经发布了有 4 年之久了,想问下你们对它未来 4 年的发展有什么想法呢?



官方回复:



我认为未来 React Native 的发展将有两个阶段。




在第一个阶段发展结束的时候,我认为 React Native 将成为一个把 React 语法带到任何一个原生平台上的框架。现在我们已经可以看到,通过 Fabric 以及 TurboModules 会让 React Naitve 变得更易用更通用。我希望 React Native 可以支持任何移动、桌面、AR/VR 平台。目前我们应该也可以看到,公司希望 React Native 能运行在除了 Android 和 iOS 以外的设备上。




在我开始讲述第二阶段前,首先需要明白我们要通过 React Native 达到什么目的是非常重要的,我们在尝试把 React 带到原生界面开发中。我们认为 React 在表现力、直观性以及灵活性之间,做到了一个非常好的平衡,以提供良好的性能和灵活的界面。




在第二阶段发展结束的时候,我认为 React Native 将会重新回归 "React",这意味着很多事情,并且他的定位也会更加模糊。但是,这意味着在 React Native 和 React for web 之间更加聚合与抽象。这可能意味着会将抽象的级别提高到目前开发人员熟悉的 Web 水平上来。然而有趣的是,Twitter 整个网站已经使用 React Native(react-native-web)编写了。虽然这看起来像“代码共享”的 holy grail。但其实没有必要,我相信它可以在任何平台上都能带来高质量的体验。



话里有话:



这段话的大概意思就是,未来,第一阶段,React Native 计划先把 React 搬到所有原生平台上,然后第二阶段,就是逐渐抹平 React Native 和 React for web 之间的区别,代码会朝着 Web 开发者熟悉的方向进行抽象和聚合




从这段话中,给我的感觉像是在说,React Native 是 React 的扩充而已,不要老拿我们和 Flutter 比,我们不一样,OK?至于未来怎么发展,那肯定是不会脱离我们庞大的 React 用户群体的。这本来就不是开发出来给你们原生开发者用的,而是给 Web 开发者扩充技能栈的。这么说,可能也是想避开和 Flutter 的正面交锋吧?毕竟在原生开发领域,Google 的技术积累比 Facebook 还是要深厚。





现在这个活动已经有超过 200 多条回复了,其中有很多大家比较关心的问题,我觉得所有在用 React Native 的开发者都可以去看一下。由于内容实在是太多了,我也就不逐一翻译了。


还有一点需要特别提一下,React Native 为什么要在这个时候搞这次 AUA 活动呢?正如他们在活动详情里提到的,因为 RN0.59 正式版马上就要发布了,官方宣称这次更新带来了“非常值得期待”的更新,所以可能是想出来好好宣传一下吧。


如果你也有关注 React Native 开发,可以关注我的公众号,会不定时分享一些国内外的动态,当然不只有 React Native,也会分享一些关于移动开发的其他原创内容。




围观地址:(要梯子)


https://www.reddit.com/r/reactnative/comments/azuy4v/were_the_react_native_team_aua/


收起阅读 »

RN几种脚手架工具的使用和对比(react-native-cli、create-react-native-app、exp)

1、react-native-cli 无法使用exp服务 react-native init program-name #初始化项目 npm start(react-native start) #在项目目录下启动 js service react-nat...
继续阅读 »

1、react-native-cli



无法使用exp服务



react-native init program-name  #初始化项目
npm start(react-native start) #在项目目录下启动 js service
react-native run-android #已连接真机或开启模拟器前提下,启动项目
react-native run-ios #已连接真机或开启模拟器前提下(仅支持mac系统),启动项目

2、create-react-native-app



create-react-native-app是React 社区孵化出来的一种无需构建配置就可以创建>RN App的一个开源项目,一个创建react native应用的脚手架工具(最好用,无需翻墙



初始化后项目可使用exp服务




安装使用


npm install -g create-react-native-app #全局安装

使用create-react-native-app来创建APP


create-react-native-app program-name #初始化项目
cd program-name #进入项目目录
npm start #启动项目服务

create-react-native-app常用命令


npm start  #启动本地开发服务器,这样一来你就可以通过Expo扫码将APP运行起来了
npm run ios #将APP运行在iOS设备上,仅仅Mac系统支持,且需要安装Xcode
npm run android #将APP运行在Android设备上,需要Android构建工具
npm test # 运行测试用例


如果本地安装了yarn管理工具,会提示使用yarn命令来启动管理服务




运行项目



Expo App扫码启动项目服务屏幕上自动生成的二维码,program-name就可以运
行在Expo App上



expo下载配置参考下一条


3、Expo



Expo是一组工具、库和服务,可以通过编写JavaScript来构建本地的ios和Android应用程序
需翻墙使用,下载资源速度慢



安装使用



PC上通过命令行安装expo服务



1、npm install exp --global #全局安装 简写: npm i -g exp


手机上安装Expo Client App(app store上叫Expo Client)
安装包下载地址:expo官网
手机安装好后注册expo账号(必须,后续用于PC expo 服务直接通过账号将项目应用于expo app




提示:为了确保Expo App能够正常访问到你的PC,你需要确保你的手机和PC处于同一网段内或者他们能够联通



初始化一个项目(Create your first project)


2、exp init my-new-project  #初始化项目,会要求你选择模板


The Blank project template includes the minimum dependencies to run and an empty root component 空白项目模板包含运行的最小依赖项和空白根组件


The Tab Navigation project template includes several example screens Tab Navigation项目模板包含几个示例屏幕




报错:



Set EXPO_DEBUG=true in your env to view the stack trace. 报错如下图
解决方法:下载Expo XDE(PC客户端使用) --初始化项目需翻墙





注:使用命令行初始化项目可能会卡在下载react-native资源,可转换成XDE初始化项目,再使用命令行启动项目并推送



3、cd my-new-project #进入项目目录
4、exp start #启动项目,推送至手机端

启动项目后会要求你输入你在App上注册的Expo账号和密码




初始化后项目结构



主要windows下android目录结构


|- program-name             | 项目工作空间
|- android | android 端代码
|- app | app 模块
|- build.gradle | app 模块 Gradle 配置文件
|- progurad-rules.pro | 混淆配置文件
|- src/main | 源代码
|- AndroidManifest.xml | APK 配置信息
|- java | 源代码
|- 包名 | java 源代码
|- MainActivity.java | 界面文件, (加载ReactNative源文件入口)
|- MainApplication.java | 应用级上下文, (ReactNative 插件配置)
|- res | APK 资源文件
|- gradle | Gradle 版本配置信息
|- keystores | APK 打包签名文件(如果正式开发需要自己定义修改签名文件)
|- gradlew | Gradle运行脚本, 与 react-native run-android 有关
|- gradlew.bat | Gradle运行脚本, 与 react-native run-android 有关
|- gradle.properties | Gradle 的配置文件, 正常是 AndroidHome, NDK, JDK, 环境变量的配置
|- build.gradle | Gradle的全局配置文件, 主要是是配置编译 Android 的 Gradle 插件,及配置 Gradle仓库
|- settings.gradle | Gradle模块配置
|- ios | iOS 端代码
|- node_modules | 项目依赖库
|- package.json | node配置文件, 主是要配置项目的依赖库,
|- index.android.js | Android 项目启动入口
|- index.ios.js | iOS 项目启动入口


package.json文件说明



dependencies




  • 项目的依赖配置

    • 依赖配置,配置信息配置方式

      • “version” 强制使用特定版本

      • “^version” 兼容版本

      • “git…” 从 git版本控制地址获取依赖版本库

      • “path/path/path” 指定本地位置下的依赖库

      • “latest” 使用最新版本

      • “>version” 会在 npm 库中找最新的版本, 并且大于此版本

      • “>=version” 会在 npm 库中找最新的版本, 并且大于等于此版本“







devDependencies



  • 开发版本的依赖库




version




  • js 版本标志



description




  • 项目描述, 主要使用于做第三方支持库时,对库的描述信息



main




  • 项目的缺省入口



engines




  • 配置引擎版本信息, 如 node, npm 的版本依赖



**index.*.js
新版RN统一入口:index.js




  • 正常只作为项目入口,不做其他业务代码处理


注:
1、虚拟机上很消耗电脑内存, 建议使用真机进行安装测试



链接:https://juejin.cn/post/6844903599793766413

收起阅读 »

IPFS对标HTTP,IPFS的优势是什么?

FIL
区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存...
继续阅读 »

区块链技术的高速发展,离不开底层技术的支持,而且肯定先于区块链技术的发展。目前来看,IPFS—Filecoin是最有可能成为区块链底层基础设施的技术。这也表明IPFS—Filecoin必然会随之快速发展。造成这一现象的原因之一在于区块链技术本身的限制,它不能存储存储数据,这也是自区块链技术诞生之后限制区块链技术发展的重要原因之一。IPFS矿机布局,避免踩坑(FIL37373)

Filecoin与IPFS(InterPlanetary File System,星际文件系统)是近两年来非常热门的概念。所谓IPFS是一个基于内容寻址的、分布式的、新型超媒体传输协议。IPFS支持创建完全分布式的应用。它旨在使用网络更快、更安全、更开放。IPFS是一个分布式文件系统,它的目标是将所有计算设备连接到同一个文件系统,从而成为一个全球统一的储存系统。而Filecoin是IPFS的激励层。

IPFS对标HTTP,IPFS的优势是什么?

IPFS星际文件存储系统,是一种p2p协议。相对于传统云存储有以下几个优点:

1. 便宜。IPFS存储空间不由服务商提供,而是接入网络的节点来提供,可以说是任何人都可以成为节点的一部分,所以非常便宜。

2. 速度快。IPFS协议下,文件冗余存储在世界各地,类似于CDN一样。当用户发起下载请求时,附近的借点都会收到信息并传送文件给你,而你只接收最先到达的文件。而传统云服务依赖于中心服务器到你的主机的线路和带宽。IPFS矿机布局,避免踩坑(FIL37373)

3. 安全性高。目前没有任何云存储敢保证自己的服务器不会遭到黑客袭击并保证数据安全。但是IPFS协议下文件在上传的时候会在每个节点保留其记录,系统检测单到文件丢失的时候会自动恢复。且由于其分布性存储的特征,黑客无法同时攻击所有节点。IPFS矿机布局,避免踩坑(FIL37373)

4.隐私保护。对于加密文件的上传使用非对称加密的方式,即除非对方掌握了私钥,否则无法破解。

IPFS分布式存储结构,各项数值优于HTTP,且发布区块链项目Filecoin,能够为IPFS技术存储提供足够的微型存储空间(节点),IPFS,与Filecoin即形成紧密的共生关系,相辅相成。

IPFS网络要想稳定运行需要用户贡献他们的存储空间、网络带宽,如果没有恰当的奖励机制,那么巨大的资源开销很难维持网络持久运转。受到比特币网络的启发,将Filecoin作为IPFS的激励层就是一种解决方案了。对于用户而言,Filecoin能够提高存取速度和效率,能带来去中心化的应用;对于矿工,贡献网络资源可以获得一笔不错的收益。

收起阅读 »

基于环信 sdk 在uni-app框架中快速集成开发的一款多平台社交Demo

说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我 Git 源码地址: https:/...
继续阅读 »

说在前面:此款 demo 是基于 环信sdk 开发的一款具有单聊、群聊、聊天室、音视频等功能的应用。在此之前我们已经开发完 Vue、react(web端)、微信小程序。这三个热门领域的版本,如有需要源码可以留言给我


Git 源码地址: https://github.com/easemob/webim-uniapp-demo


一、安装开发工具


我们选用微信小程序来用做示例(如果选择百度、支付宝安装对应开发者工具即可)、


微信开发者工具建议还是安装最新版的。uni-app的开发也必须安装HBuilderX工具,这个是捆绑的,没得选择。要用uni-app,你必须得装!


工具安装:


微信开发者工具


HBuilderX


项目demo介绍:



项目demo启动预览:



快速集成环信 sdk:


1、复制整个utils文件



如果你想具体了解主要配置文件 请看这个链接:


https://docs-im.easemob.com/im/web/intro/start


2、如何使用环信的appkey ,可以在环信 console 后台注册一个 账号申请appkey ,可以参考这里 ,获取到 appkey 以后添加到配置文件中 ,如下图所示:



以上两个重要的配置准备完成之后就可以进行一系列的操作了(收发消息、好友申请、进群入群通知等)


在uni-app中 使用环信 sdk 实现添加、删除好友:


1、在全局 App.vue 文件 钩子函数 onLaunch() 中监听各种事件 (好友申请、收到各类消息等)如图:



发送好友请求:



onPresence(message)事件中接收到好友消息申请:



同意好友请求:



拒绝好友请求:



实现收发消息:


1、给好友发送消息:



2、接收到消息:


onTextMessage(message)事件中接收到好友消息,然后做消息上屏处理(具体消息上屏逻辑可看demo中代码示例):



以上展示的仅仅为基本业务场景,更多的业务逻辑详情请看demo示例。api具体详情可以查看 环信sdk 文档


                                  


PS:对于安卓、iOS移动端,我们已经兼容完成。想通过uni-app生成安卓、ios应用的小伙伴们可以愉快的使用起来了~~~


基于uni-app的开发其中也趟了不少坑,在这里就不多赘述了。回归到框架的选型来讲,选用uni-app开发小程序,可同时并行多端小程序,这点是真香,一次开发多端发布。至于审核嘛~ 时快时慢


最后的最后:如果你喜欢,请拒绝白嫖,点赞三连转发!


                                               

收起阅读 »

【含视频、课件下载】一天开发一款灵魂社交APP

视频回放: 课件下载:社交应用开发分享.pptx零开发基础、源码共享 内容介绍:从互联网诞生之日起,社交需求就一直作为一种刚需存在,在人际过载与信息过载时代,微信已经不再能承载我们最简单、纯粹、美好的社交需求,在社交疲态和用户迁移的产品契机下,陌生人...
继续阅读 »


视频回放:


课件下载:

社交应用开发分享.pptx

零开发基础、源码共享

 

内容介绍:

从互联网诞生之日起,社交需求就一直作为一种刚需存在,在人际过载与信息过载时代,微信已经不再能承载我们最简单、纯粹、美好的社交需求,在社交疲态和用户迁移的产品契机下,陌生人社交领域逐渐孕育出“陌陌、探探、SOUL”等社交APP新贵。随着5G时代的到来,一波音视频社交领域的创业窗口期又重新打开。

本次课程,环信生态开发者“穿裤衩闯天下”将给我们带来一款基于环信即时通讯云(环信音视频云)开发的免费开源灵魂社交APP,分享其开发过程和项目源码,助力程序员高效开发,快速集成。

 

直播大纲:

1)项目介绍

国内首个程序猿非严肃婚恋交友应用——猿匹配

(2)开发环境

在最新的Android开发环境下开发,使用Java8的一些新特性,比如Lambda表达式等

· Mac OS 10.14.4

· Android Studio 3.3.2

(3)功能介绍

· IM功能

会话与消息功能,包括图片、文本、表情等消息,还包括语音实时通话与视频实时通话功能的开发等

· APP功能

包括聊天、设置、社区等板块开发

· 发布功能

含多渠道打包、签名配置、开发与线上环境配置、敏感信息保护等

(4)配置运行


提供一些地址:

自定义工具库:https://github.com/lzan13/VMLibrary

 

收起阅读 »