Flutter 多引擎支持 PlatformView 以及线程合并解决方案

flutter中使用android原生视图 虚拟显示模式(Virtual displays) 将 android.view.View 实例渲染为纹理,不会嵌入到 Activity 的视图层次结构中。 键盘处理和辅助功能可能无法工作。 混合集成模式(Hybrid composition) 需要 Flutter 1.22(推荐 1.22.2)。 将原生的 android.view.View 附加到视图层次结构中。键盘处理和无障碍功能是开箱即用的。 Android 10 之前,这个模式会大大降低 Flutter UI 的帧率(FPS)。此widget继承S 阅读详情


作者:字节移动技术-李皓骅

摘要

本文介绍了 Flutter 多引擎下,使用 PlatformView 场景时不能绕开的一个线程合并问题,以及它最终的解决方案。最终 Pull Request 已经 merge 到 Google 官方 Flutter 仓库:

https://github.com/flutter/engine/pull/27662

本文关键点:

  1. 线程合并,实际上指的并不是操作系统有什么高级接口,可以把两个 pthread 合起来,而是 flutter 引擎中的四大 Task Runner 里,用一个 Task Runner 同时消费处理两个 Task Queue 中排队的任务。
  2. 线程合并问题,指的是 Flutter 引擎四大线程(Platform 线程、UI 线程、Raster 线程、IO 线程)其中的 Platform 线程和 Raster 线程在使用 PlatformView 的场景时需要合并和分离的问题。之前的官方的线程合并机制,只支持一对一的线程合并,但多引擎场景就需要一对多的合并和一些相关的配套逻辑。具体请看下文介绍。
  3. 关于 Flutter 引擎的四大 Task Runner 可以参考官方 wiki 中的 Flutter Engine 线程模型 : https://github.com/flutter/flutter/wiki/The-Engine-architecture#threading
  4. 本文介绍的线程合并操作(也就实现了一个 looper 消费两个队列的消息的效果),见如下的示意图,这样我们可以有个初步的印象:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FIt1i6la-1631187770198)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/0.png)]

背景介绍

什么是 PlatformView?

首先,介绍下 PlatformView 是什么,其实它简单理解成——平台相关的 View 。也就是说,在Android 和 iOS 平台原生有这样的控件,但是在Flutter的跨平台控件库里没有实现过的一些Widget,这些控件我们可以使用Flutter提供的PlatformView的机制,来做一个渲染和桥接,并且在上层可以用Flutter的方法去创建、控制这些原生View,来保证两端跨平台接口统一。

比如WebView,地图控件,第三方广告SDK等等这些场景,我们就必须要用到PlatformView了。

举一个例子,下图就是 Android 上使用 PlatformView 机制的 WebView 控件和 Flutter控件的混合渲染的效果:

img

可以看到Android ViewTree上确实存在一个WebView。

下面是一个Flutter的使用WebView的上层代码示例:

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

// .. 省略App代码
class _BodyState extends State<Body> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('InAppWebView Example'),
      ),
      body: Expanded(
        child: WebView(
          initialUrl: 'https://flutter.dev/',
          javascriptMode: JavascriptMode.unrestricted,
        ),
      ),
    );
  }
}

黄色背景内容是使用WebView的方法,可以看到,经过 WebView 插件的封装,虽然背后是 Android 平台或者 iOS 平台本身的 WebView,但是就像使用 Flutter Widget 一样方便。

其实在Flutter历史演进过程中,对于 PlatformView 的处理曾经有过两种方案,分别是:

Flutter 1.20版本之前的 VirtualDisplay 方式,和 Flutter 1.20 之后推荐使用的 HybridComposition 方式。现在官方推荐 HybridComposition 的 embedding 方式,可以避免很多之前的 bug 和性能问题,具体不再赘述,可以参考官方文档。

官方的PlatformView介绍文档:在 Flutter 应用中使用集成平台视图托管您的原生 Android 和 iOS 视图

Flutter 引擎线程模型

要理解下文的线程合并,首先我们需要了解下Flutter 引擎的线程模型。

Flutter Engine 需要提供4个 Task Runner,这4个 Runner 默认的一般情况下分别对应分别着4个操作系统线程,这四个 Runner 线程各司其职:

Task Runner作用
Platform Task RunnerApp 的主线程,用于处理用户操作、各类消息和 PlatformChannel ,并将它们传递给其他 Task Runner 或从其他 Task Runner 传递过来。
UI Task RunnerDart VM 运行所在的线程。运行 Dart 代码的线程,负责生成要传递给 Flutter 引擎的 layer tree。
GPU Task Runner (Raster Task Runner)与 GPU 处理相关的线程。它是使用 Skia 最终绘制的过程相关的线程(OpenGL 或 Vulkan 等等)
IO Task Runner执行涉及 I/O 访问的耗时过程的专用线程,例如解码图像文件。

如下图所示:

img

线程合并

关于线程合并,我们可能有下面几个疑问:

  1. 为什么不用 platform view 的时候,两种多引擎工作的好好的?
  2. 为什么使用 platform view 的时候,iOS 和 Android 两端,都需要 merge 么,能不能不 merge ?
  3. merge 以后,在不使用 platform view 的 flutter 页面里,还会取消 merge 还原回来么?

我们来怀着这几个疑问去分析问题。

为什么要线程合并?

为什么在使用PlatformView的时候,需要把 Platform 线程和 Raster 线程合并起来?

简单的说就是:

  1. 所有 PlatformView 的操作需要在主线程里进行(Platform线程指的就是App的主线程),否则在 Raster 线程处理 PlatformView 的 composition 和绘制等操作时,Android Framework 检查到非 App 主线程,会直接抛异常;
  2. Flutter 的 Raster渲染操作和 PlatformView 的渲染逻辑是各自渲染的,当他们一起使用的时候每一帧渲染时候,需要做同步,而比较简单直接的一种实现方式就是把两个任务队列合并起来,只让一个主线程的 runner 去逐个消费两个队列的任务;
  3. Skia和GPU打交道的相关操作,其实是可以放在任意线程里的,合并到App主线程进行相关的操作是完全没有问题的

那么,Platform Task Runner在合并GPU Task Runner后,主线程也就包揽并承担了原本两个Runner的所有任务,参考下面的示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YD2wkTFV-1631187770203)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/3.png)]

我们分析external_view_embedder.cc相关的代码也可以看到合并的操作:

// src/flutter/shell/platform/android/external_view_embedder/external_view_embedder.cc
// |ExternalViewEmbedder|
PostPrerollResult AndroidExternalViewEmbedder::PostPrerollAction(
    fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
  if (!FrameHasPlatformLayers()) {
    // 这里判断当前frame有没有platform view,有就直接返回
    return PostPrerollResult::kSuccess;
  }
  if (!raster_thread_merger->IsMerged()) { 
    // 如果有platform view并且没merger,就进行merge操作
    // The raster thread merger may be disabled if the rasterizer is being
    // created or teared down.
    //
    // In such cases, the current frame is dropped, and a new frame is attempted
    // with the same layer tree.
    //
    // Eventually, the frame is submitted once this method returns `kSuccess`.
    // At that point, the raster tasks are handled on the platform thread.
    raster_thread_merger->MergeWithLease(kDefaultMergedLeaseDuration);
    CancelFrame();
    return PostPrerollResult::kSkipAndRetryFrame;
  }

  // 扩展并更新租约,使得后面没有platform view并且租约计数器降低到0的时候,开始unmerge操作
  raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
  // Surface switch requires to resubmit the frame.
  // TODO(egarciad): https://github.com/flutter/flutter/issues/65652
  if (previous_frame_view_count_ == 0) {
    return PostPrerollResult::kResubmitFrame;
  }
  return PostPrerollResult::kSuccess;
}

也就是说,我们有两种情况,一种是当前layers中没有 PlatformView ,一种是开始有PlatformView,我们分析下各自的四大线程的运行状态:

  1. 首先没有PlatformView的时候的情况下,四大 Task Runner 的状态:

Platform ✅ / UI ✅ / Raster ✅ / IO ✅

  1. 使用PlatformView的时候的情况下,四大 Task Runner 的状态:

Platform ✅(同时处理Raster线程的任务队列) / UI ✅ / Raster ❌(闲置) / IO ✅

merge 和 unmerge 操作,可以如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYWxLtuO-1631187770204)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/4.png)]

一个 runner 如何消费两个任务队列?

关键的两个点就是:

  1. TaskQueueEntry 类中有两个成员变量,记录了当前队列的上游和下游的queue_id
  2. 在 TaskQueueRunner 取下一个任务的时候(也就是PeekNextTaskUnlocked函数)做了特殊处理:

TaskQueueEntry类的这两个成员的声明和文档:

/// A collection of tasks and observers associated with one TaskQueue.
///
/// Often a TaskQueue has a one-to-one relationship with a fml::MessageLoop,
/// this isn't the case when TaskQueues are merged via
/// \p fml::MessageLoopTaskQueues::Merge.
class TaskQueueEntry {
 public:
  // ....
  std::unique_ptr<TaskSource> task_source;
  // Note: Both of these can be _kUnmerged, which indicates that
  // this queue has not been merged or subsumed. OR exactly one
  // of these will be _kUnmerged, if owner_of is _kUnmerged, it means
  // that the queue has been subsumed or else it owns another queue.
  TaskQueueId owner_of;
  TaskQueueId subsumed_by;
  // ...
};

取下一个任务的PeekNextTaskUnlocked的逻辑(参考注释):

// src/flutter/fml/message_loop_task_queues.cc
const DelayedTask& MessageLoopTaskQueues::PeekNextTaskUnlocked(
    TaskQueueId owner,
    TaskQueueId& top_queue_id) const {
  FML_DCHECK(HasPendingTasksUnlocked(owner));
  const auto& entry = queue_entries_.at(owner);
  const TaskQueueId subsumed = entry->owner_of;
  if (subsumed == _kUnmerged) { // 如果没merge的话,就取自己当前的top任务
    top_queue_id = owner;
    return entry->delayed_tasks.top();
  }

  const auto& owner_tasks = entry->delayed_tasks;
  const auto& subsumed_tasks = queue_entries_.at(subsumed)->delayed_tasks;

  // we are owning another task queue
  const bool subsumed_has_task = !subsumed_tasks.empty();
  const bool owner_has_task = !owner_tasks.empty();
  if (owner_has_task && subsumed_has_task) {
    const auto owner_task = owner_tasks.top();
    const auto subsumed_task = subsumed_tasks.top();
    // 如果merge了的话,根据标记判断,就取两个队列的top任务,再比较谁比较靠前
    if (owner_task > subsumed_task) {
      top_queue_id = subsumed;
    } else {
      top_queue_id = owner;
    }
  } else if (owner_has_task) {
    top_queue_id = owner;
  } else {
    top_queue_id = subsumed;
  }
  return queue_entries_.at(top_queue_id)->delayed_tasks.top();
}

问题与分析

遇到的问题

我们在使用官方引擎的过程中,分别在独立多引擎和轻量级多引擎两个场景下的PlatformView时,都遇到了线程合并的问题。

问题1:独立多引擎下的线程合并问题

最早是 webview 的业务方报告的 slardar 崩溃问题,当时写了一个 unable_to_merge_raster_demo的例子,然后给官方提交了一个issue:

https://github.com/flutter/flutter/issues/78946

也就是说,在独立的多引擎下,使用platform view的时候,会因为raster_thread_merger不支持多于一对一的合并(merge)操作而失败并报错。

崩溃的demo:

https://github.com/eggfly/unable_to_merge_raster_demo

看日志这是一个崩溃,然后接一个native的SIGABRT崩溃,日志如下:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Xiaomi/umi/umi:11/RKQ1.200826.002/21.3.3:user/release-keys'
Revision: '0'
ABI: 'arm64'
pid: 11108, tid: 11142, name: 1.raster  >>> com.example.unable_to_merge_raster_demo <<<
uid: 10224
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '[FATAL:flutter/fml/raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads
    x0  0000000000000000  x1  0000000000002b86  x2  0000000000000006  x3  0000007c684fd150
    // ... register values
backtrace:
      #00 pc 0000000000089acc  /apex/com.android.runtime/lib64/bionic/libc.so (abort+164) (BuildId: a790cdbd8e44ea8a90802da343cb82ce)
      #01 pc 0000000001310784  /data/app/~~W2sUpMihWXQXs-Yx0cuHWg==/com.example.unable_to_merge_raster_demo-IUwY4BX5gBqjR0Pxu09Pfw==/lib/arm64/libflutter.so (BuildId: 854273bae6db1c10c29f7189cb0cf640ad4db110)
      #02 pc 000000000133426c  /data/app/~~W2sUpMihWXQXs-Yx0cuHWg==/com.example.unable_to_merge_raster_demo-IUwY4BX5gBqjR0Pxu09Pfw==/lib/arm64/libflutter.so (BuildId: 854273bae6db1c10c29f7189cb0cf640ad4db110)
      // ... more stack frames
Lost connection to device.

问题2:轻量级多引擎下的线程合并问题

Flutter 2.0版本后引入了lightweight flutter engines,也就是轻量级引擎,可以通过FlutterEngineGroups和spawn()函数来生成一个轻量级引擎,官方轻量级相关的提交:

https://github.com/flutter/engine/pull/22975

我们在用官方的lightweight multiple engine的sample代码的时候,尝试在多引擎下加上PlatformView,也就是在main.dart里加上webview。

官方demo代码:https://github.com/flutter/samples/tree/master/add_to_app/multiple_flutters

运行起来会有这样的崩溃日志,这里的错误和问题1有一点区别:

[FATAL:flutter/fml/raster_thread_merger.cc(22)] Check failed: !task_queues_->Owns(platform_queue_id_, gpu_queue_id_). 

问题分析

分析1:独立多引擎线程合并问题

问题1是Flutter 1.22+独立引擎的问题,我在代码中搜索raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads其中raster_thread_merger.cc的48行这样的代码:

img

当success == false的时候会触发SIGABRT,看Merge()函数什么时候返回false:

bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
  if (owner == subsumed) {
    return true;
  }

  std::mutex& owner_mutex = GetMutex(owner);
  std::mutex& subsumed_mutex = GetMutex(subsumed);

  std::scoped_lock lock(owner_mutex, subsumed_mutex);

  auto& owner_entry = queue_entries_.at(owner);
  auto& subsumed_entry = queue_entries_.at(subsumed);

  if (owner_entry->owner_of == subsumed) {
    return true;
  }

  std::vector<TaskQueueId> owner_subsumed_keys = {
      owner_entry->owner_of, owner_entry->subsumed_by, subsumed_entry->owner_of,
      subsumed_entry->subsumed_by};

  for (auto key : owner_subsumed_keys) {
    if (key != _kUnmerged) {
      return false; // <--- 这里是返回false唯一的可能
    }
  }

  owner_entry->owner_of = subsumed;
  subsumed_entry->subsumed_by = owner;

  if (HasPendingTasksUnlocked(owner)) {
    WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));
  }

  return true;
}

Merge函数看起来是把两个task_queue合并到一起的关键逻辑,通过设置entry->owner_of和subsumed_by来实现的。参考上面TaskQueueEntry类的声明代码。

那么在owner_subsumed_keys这个vector的四个元素里打上log看一下,for循环的本意是检查owner和上游和下游,以及subsumed的上游和下游,加起来这四个id的任意元素里如果出现一个不等于_kUnmerged的就会检查失败,进而不进行后面的merge和赋值操作,直接返回false。

通过log可以看出:

img

E/flutter: ::Merge() called with owner=0, subsumed=2
E/flutter: [0]=18446744073709551615 [1]=18446744073709551615 [2]=18446744073709551615 [3]=18446744073709551615
E/flutter: ::Merge() called with owner=0, subsumed=5
E/flutter: [0]=2 [1]=18446744073709551615 [2]=18446744073709551615 [3]=18446744073709551615
A/flutter: Check failed: success. Unable to merge the raster and platform threads.

可以看到Merge调用了两次,并且第二次调用的第0个元素是2,印证了上面for循环出现不等于unmerge常量的情况了。

其中的2和5分别是引擎1和引擎2的raster线程,通过

 adb root
 adb shell kill -3 $pid 

再 adb pull /data/anr/trace_00 拉出来看真实的线程也可以看到1.ui, 2.ui, 1.raster, 2.raster, 1.io, 2.io这样的被设置了名字线程(有pthread_setname之类的函数):

img

在Google搜索这个Unable to merge the raster and platform threads在也可以搜到一个提交:

https://github.com/flutter/engine/pull/23733

提交介绍说:

This will make sure that people don’t use platform view with flutter engine groups until we’ve successfully accounted for them.

所以它在做第1次merge的时候,设置了block_merging标记,第二次以及后面的merge操作会失败并打印一个日志:

img

所以,在官方那是一个todo,是待实现的feature。

分析2:轻量级多引擎线程合并问题

问题2是Flutter 2.0+轻量级引擎下的问题,直接看轻量级多引擎下,检查失败的那一行的源码:

img

很明显,和上面的独立多引擎不同,这里在创建RasterThreadMerger的构造函数的FML_CHECK检查就失败了,证明platform和raster已经是merge的状态了,所以这里也是SIGABRT并且程序退出了。

通过打印log看到两个引擎的platform和raster的id是共享的,引擎1和引擎2的platform_queue_id都是0,raster_queue_id都是2

小结:多对一合并是官方未实现的feature

很容易我们可以推理得到,多引擎的每个引擎都需要有一套四大线程,它们可以选择公用,或者也可以选择创建自己独立的线程。

我们通过之前的log打印的task_queue_id,分析一下两个问题唯一的区别:

  1. 在问题1(两个独立引擎中)的情况是这样的(四大线程除了platform,其他三个线程不共享):
独立引擎1独立引擎2
platform_task_queue_id00
ui_task_queue_id14
raster_task_queue_id25
io_task_queue_id36
  1. 在问题2(两个轻量级引擎中)的情况是这样的(四大线程全部共享):
轻量级引擎1轻量级引擎2
platform_task_queue_id00
ui_task_queue_id11
raster_task_queue_id22
io_task_queue_id33

所以相对来讲,感觉问题2更容易解决,并且我们使用flutter 2.0和卡片方案的业务,马上就将要遇到这个问题。

官方的轻量级引擎有一个TODO列表,把这个问题标记成Cleanup的任务:

https://github.com/flutter/flutter/issues/72009

img

官方标记了P5优先级:

img

因为业务需要所以直接就不等了,我们干脆自己实现它。

线程合并解决方案

快速解决问题2:解决轻量级引擎的问题

既然在轻量级引擎下,platform 线程和 raster 线程都是共享的,只是 engine 和 rasterizer 的对象是分开的,并且现在的逻辑是分别在两个引擎里,new 了自己的 RasterThreadMerger对象,进行后续的 merge 和 unmerge 操作。并且在 merge 的时候做是否Owns的检查。

那我们可以简单的做这几件事:

  1. 改成去掉 Owns() 的检查和相关线程检查
  2. 共享一个 RasterThreadMerger 对象进行 merge 和 unmerge 操作
  3. 先不管那个 lease_term (租约)计数器,留下后续处理

修改方案基本是坤神(我们Flutter组的战友)的 prototype 提交的方案,并且加一些边角的处理即可。

Prototype原型的关键修改的地方:

img

每个带title的都是一个FlutterView,终于不崩溃了:

效果截图:

img

但是这只是一个原型,很多状态问题和merge的逻辑我们没有处理的很好,问题包括:

  1. 我们不能像原型一样,在字节的Flutter引擎里直接hardcode写死共享一个merger对象,所以2.0之前的独立多引擎仍旧会有问题
  2. 我们没正确处理IsMerged函数的正确返回结果
  3. 我们还没有正确处理lease_term的计数器,lease_term计数器降到0的时候,应该unmerge
  4. 我们假象有一种case: 引擎1需要unmerge,但是引擎2还需要渲染platformview,这时候1的unmerge不能立刻调用,需要等所有引擎都没有merge的需求的时候,再去把platform和raster脱离开

所以我们需要有一套真正的终极解决方案,最好能:覆盖两个raster同时merge到一个platform的情况,然后贡献给官方。

彻底解决问题1和2(最终方案)

解决思路

经过查看代码里raster_thread_merger对象是rasterizer的一个成员:

// src/flutter/shell/common/rasterizer.h
namespace flutter {
//----------------------------------------------------------------------------
class Rasterizer final : public SnapshotDelegate {
 public:
  //-------
 private:
  // ...省略
  fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger_;

以下都是 RasterThreadMerger 类里的成员函数,都是需要我们修改成一对多merge以后,也保证去维护正常调用时机的API:

// src/flutter/fml/raster_thread_merger.h
#ifndef FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_
#define FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_

// ... 省略 #include 
namespace fml {
class RasterThreadMerger
    : public fml::RefCountedThreadSafe<RasterThreadMerger> {
 public:
  // Merges the raster thread into platform thread for the duration of
  // the lease term. Lease is managed by the caller by either calling
  // |ExtendLeaseTo| or |DecrementLease|.
  // When the caller merges with a lease term of say 2. The threads
  // are going to remain merged until 2 invocations of |DecreaseLease|,
  // unless an |ExtendLeaseTo| gets called.
  //
  // If the task queues are the same, we consider them statically merged.
  // When task queues are statically merged this method becomes no-op.
  void MergeWithLease(size_t lease_term);

  // Un-merges the threads now, and resets the lease term to 0.
  //
  // Must be executed on the raster task runner.
  //
  // If the task queues are the same, we consider them statically merged.
  // When task queues are statically merged, we never unmerge them and
  // this method becomes no-op.
  void UnMergeNow();

  // If the task queues are the same, we consider them statically merged.
  // When task queues are statically merged this method becomes no-op.
  void ExtendLeaseTo(size_t lease_term);

  // Returns |RasterThreadStatus::kUnmergedNow| if this call resulted in
  // splitting the raster and platform threads. Reduces the lease term by 1.
  //
  // If the task queues are the same, we consider them statically merged.
  // When task queues are statically merged this method becomes no-op.
  RasterThreadStatus DecrementLease();

  bool IsMerged();

  // ... 省略一些接口
  bool IsMergedUnSafe() const;
};

}  // namespace fml

#endif  // FML_SHELL_COMMON_TASK_RUNNER_MERGER_H_

merger创建的时候,需要考虑某些情况下不支持merger需要保持merger不被创建出来(比如某些不支持的平台或者某些unittest):

// src/flutter/shell/common/rasterizer.cc
void Rasterizer::Setup(std::unique_ptr<Surface> surface) {
  // ... 省略
  if (external_view_embedder_ &&
      external_view_embedder_->SupportsDynamicThreadMerging() &&
      !raster_thread_merger_) {
    const auto platform_id =
        delegate_.GetTaskRunners().GetPlatformTaskRunner()->GetTaskQueueId();
    const auto gpu_id =
        delegate_.GetTaskRunners().GetRasterTaskRunner()->GetTaskQueueId();
    raster_thread_merger_ = fml::RasterThreadMerger::CreateOrShareThreadMerger(
        delegate_.GetParentRasterThreadMerger(), platform_id, gpu_id);
  }
  if (raster_thread_merger_) {
    raster_thread_merger_->SetMergeUnmergeCallback([=]() {
      // Clear the GL context after the thread configuration has changed.
      if (surface_) {
        surface_->ClearRenderContext();
      }
    });
  }
}

那么我们有一种选择是在每个engine各自的rasterizer的创建的时候,改改它的逻辑,在raster_queue_id相同的时候,复用之前的对象,听起来是个好办法。

实现的方案

画了个图作为两种情况的展示:

img

关于线程什么情况下允许合并,什么情况下不允许合并的示意图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eXiaP6lp-1631187770218)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/15.png)]

另外还有一种情况没有列出,自己merge到自己的情况:现在的代码默认返回true的。

总结一句话就是一个queue可以合并多个queue(可以有多个下游),但是一个queue不可以有多个上游。

此实现的设计:

  • 首先最重要的:将TaskQueueEntry中的成员owner_of从TaskQueueId改成std::set<TaskQueueId> owner_of,来记录这个线程所有merge过的subsumed_id(一对多的merge关系)
  • 代码的修改平台独立的,可以让Android和iOS共享相同的代码逻辑,确保不同平台的相关目录中的代码没有被更改(之前做过一个版本的方案,是Android、iOS分别修改了embedder类的逻辑)
  • 删除了之前官方禁用阻塞逻辑的代码(也就是revert了官方之前的这个提交:https://github.com/flutter/engine/pull/23733)
  • 为了减少现有代码的更改数量,把旧的RasterThreadMerger类视为proxy,并引入了一个新的SharedThreadMerger类,并且在引擎里记录parent_merger,在引擎的spawn函数里拿到父亲引擎的merger,看是否可以共享
  • 与merge相关的方法调用(包括MergeWithLease()、UnmergeNow()、DecrementLease()、IsMergeUnsafe()改成重定向到SharedThreadMerger内的方法,然后用一个std::map<ThreadMergerCaller, int>来记录合并状态和lease_term租约计数器
  • 将UnMergeNow()更改为UnMergeNowIfLastOne(),以记住所有merge的调用者,在调用Rasterizer::Teardown()的时候,并且它是在最后一个merger的时候,立刻unmerge,其他情况需要保持unmerge状态。
  • 在shell_unittest和fml_unittests中添加了更多的测试,并在run_tests.py中启用fml_unittests(之前被一个官方提交禁用了,发现改什么代码都不起作用,比较坑)

解决方案相关的代码

  1. TaskQueueEntry改成std::set的集合
class TaskQueueEntry {
 public:
  /// 省略
  /// Set of the TaskQueueIds which is owned by this TaskQueue. If the set is
  /// empty, this TaskQueue does not own any other TaskQueues.
  std::set<TaskQueueId> owner_of; // 原来是TaskQueueId owner_of;
  1. PeekNextTaskUnlocked新的逻辑:
// src/flutter/fml/message_loop_task_queues.cc
TaskSource::TopTask MessageLoopTaskQueues::PeekNextTaskUnlocked(
    TaskQueueId owner) const {
  FML_DCHECK(HasPendingTasksUnlocked(owner));
  const auto& entry = queue_entries_.at(owner);
  if (entry->owner_of.empty()) {
    FML_CHECK(!entry->task_source->IsEmpty());
    return entry->task_source->Top();
  }

  // Use optional for the memory of TopTask object.
  std::optional<TaskSource::TopTask> top_task;
  
  // 更新当前最小的任务的lambda函数
  std::function<void(const TaskSource*)> top_task_updater =
      [&top_task](const TaskSource* source) {
        if (source && !source->IsEmpty()) {
          TaskSource::TopTask other_task = source->Top();
          if (!top_task.has_value() || top_task->task > other_task.task) {
            top_task.emplace(other_task);
          }
        }
      };

  TaskSource* owner_tasks = entry->task_source.get();
  top_task_updater(owner_tasks);

  for (TaskQueueId subsumed : entry->owner_of) {
    TaskSource* subsumed_tasks = queue_entries_.at(subsumed)->task_source.get(); 
    // 遍历set中subsumed合并的任务队列,更新当前最小的任务
    top_task_updater(subsumed_tasks); 
  }
  // At least one task at the top because PeekNextTaskUnlocked() is called after
  // HasPendingTasksUnlocked()
  FML_CHECK(top_task.has_value());
  return top_task.value();
}
  1. merge和unmerge相关的检查(省略,详情可以参考 Pull Request中代码提交)

实现过程中的小坑

  1. 和官方一样,使用FlutterFragment的方式来嵌入多引擎的时候,FlutterSurfaceView会给surface设置ZOrder,这时候多个Surface会有ZOrder争抢top的问题
 private void init() {
    // If transparency is desired then we'll enable a transparent pixel format and place
    // our Window above everything else to get transparent background rendering.
    if (renderTransparently) {
      getHolder().setFormat(PixelFormat.TRANSPARENT);
      setZOrderOnTop(true);
    }

需要在创建的时候,去掉Transparent的flag,需要这样改:(这个问题被坑了很久,差点没让我放弃这个提交)

val flutterFragment =
    FlutterFragment.withCachedEngine(i.toString())
        // Opaque is to avoid platform view rendering problem due to wrong z-order
        .transparencyMode(TransparencyMode.opaque) // this is needed
        .build<FlutterFragment>()
  1. 在iOS做unittest的时候,发现有相应的崩溃,也是没有崩溃的stack和详细log,后来发现iOS目录下有一个README,提到了使用xcode可以打开unittest工程,开启模拟器自动测试,并且发现可以直接在我没有attach的情况下,自动attach lldb并且定位到崩溃的那一行代码:

img

  1. 官方review代码的时候,提出的最大问题是之前用了map做一个全局static的std::map<Pair<QueueId, QueueId>, SharedThreadMerger>的字典static变量,用来取platform&raster这一个pair的merger,但是老外扔给我一个google c++规范,明确写了non-trivial的类型才允许保存为全局变量,官方规范文档:https://google.github.io/styleguide/cppguide.html#Static_and_Global_Variables

最终通过把merger作为Shell类的成员变量来解决这个生命周期的问题。

  1. 在测试的时候发现macOS、Linux的engine的unopt 目标的build和test都没问题,但是偏偏windows的引擎去测试host_debug_unopt的unittest的时候,出直接exit,exitcode不是0的

然后windows的崩溃栈默认不会打印到terminal:谷歌的luci平台上的失败信息:

img

可以看到什么log都没有。

困扰半天最终决定:装一个windows虚拟机!神奇的事情发生了,在我的windows 10 + flutter engine环境下编译然后运行我的test,结果全都过了。惊愕!最终还是通过两分法看修改,定位到了一个 unittest 的抽取的改法造成了问题。

留个题目:可以看出如下代码为什么windows会有问题吗?

/// A mock task queue NOT calling MessageLoop->Run() in thread
struct TaskQueueWrapper {
  fml::MessageLoop* loop = nullptr;

  /// 问题提示在这里:
  /// This field must below latch and term member, because
  /// cpp standard reference:
  /// non-static data members are initialized in the order they were declared in
  /// the class definition
  std::thread thread;
  
  /// The waiter for message loop initialized ok
  fml::AutoResetWaitableEvent latch;

  /// The waiter for thread finished
  fml::AutoResetWaitableEvent term;

  TaskQueueWrapper()
      : thread([this]() {
          fml::MessageLoop::EnsureInitializedForCurrentThread();
          loop = &fml::MessageLoop::GetCurrent();
          latch.Signal();
          term.Wait();
        }) {
    latch.Wait();
  }
  // .. 省略析构函数, term.Signal() 和 thread.join() 等等
};

img

  1. 跑起来两个 webview 的 demo 以后,点下面的键盘,会有一个 crash(下面的界面弹出键盘以后就崩了):

img

结果是java层对FlutterImageView的resize造成创建ImageReader的宽高为0了,Android不允许创建宽高是0的ImageReader:

img

所以又有一个bugfix的提交,已merge到官方 😂

https://github.com/flutter/engine/pull/27946

最终的 Pull Request

已合并到官方Flutter引擎: https://github.com/flutter/engine/pull/27662

给官方贡献代码的小经验

  1. 如果没有issue,最好创建一个issue,然后自己提 Pull Request 解决自己的issue😂
  2. 最好包含test,即使改了一行代码,也是可以写test的,而且他们一看到test就很放心,也能更好的让后面的人理解你的代码的意图,否则有一个机器人会说你没test,并且打上标签:

img

  1. 现在最新代码在 git push 的时候,会通过 git_hooks 自动检查所有类型的源码(包括iOS/Android/c++/gn等等)的格式和规范,有不符合规范的直接生成一个diff,作为修改建议。

这个还可以帮我们自动触发然后自动修改,命令是:

dart ci/bin/format.dart -f 其中-f是让它自动fix

img

  1. 官方 review 代码还是很严格的,比如对函数语义的修改,需要同步对docstring进行修改;又比如一言不合给你扔一个c++规范;或者代码进行重复的map[key] = value和map[key] 的取值,可以用 iterator 代替;auto关键词不能滥用,lambda 需要指定返回类型,等等

总结

作为 Flutter Infra 团队的开发,在和我们的业务部门去实践 Flutter 2.0轻量级引擎和卡片方案的落地的过程中,我们团队做了很多性能和稳定性的优化,包括空安全迁移、Image Cache共享、文本动态对齐、Platform View 的多引擎支持、过渡动画性能优化、大内存优化、官方 issue 和稳定性修复等很多工作。

我们在努力支持字节内部业务的同时,也会持续将其中比较通用的一些 fix 和优化方案的 Pull Request 提交给官方,和全世界开发者一起共建更好的Flutter社区。

另外,我们也通过字节跳动的企业级技术服务平台火山引擎对外部客户提供 Flutter 全流程解决方案,助力使用 Flutter 技术栈的开发团队高效稳健地落地业务。

关于字节终端技术团队

字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。

就是现在!客户端/前端/服务端/端智能算法/测试开发 面向全球范围招聘!一起来用技术改变世界,感兴趣请联系 chenxuwei.cxw@bytedance.com,邮件主题 简历-姓名-求职意向-期望城市-电话。

火山引擎应用开发套件MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

flutter知识点--PlatformView 总结来说,Flutter 中的 PlatformView 技术旨在弥合 Flutter 与原生平台之间的鸿沟,使开发者能够在享受跨平台开发便利的同时,充分利用各平台的特性和既有资源。:随着时间推移,Flutter 团队致力于提升 PlatformView 在不同平台上的行为一致性,减少开发者在适配不同平台时的工作量,同时确保用户在不同设备上获得一致的体验。:随着越来越多的开发者在实际项目中采用 PlatformView,社区分享了大量的实践经验、示例代码和插件,促进了技术的成熟和普及。 阅读详情

相关推荐

Flutter中的异步和多进程

在 Flutter 中,由于它是单线程的,并且 UI 操作是线程绑定的,异步编程是确保应用响应性和流畅性的关键。在 Flutter 中,由于它通常是用来构建移动应用的,多进程不是常见的做法,因为移动操作系统通常为每个应用分配一个进程。最后,为了在Flutter应用中使用这个多进程功能,可以将上述代码封装到一个方法中,并在需要的地方调用它。在实际应用中,可以根据具体需求和场景选择合适的方法。异步编程在 Flutter 中非常重要,因为它确保了 UI 的流畅性,避免了因为长时间运行的任务而导致的界面卡顿。

wayne214的博客 1807

Flutter 多引擎渲染,外接纹理实践

在 Flutter add to App 混合开发中,资源在 Native 和 Flutter 重复加载,导致内存 double 的性能问题属于司空见惯的现象了。当然,这个是有“成熟”的解决方案的,各大厂在 Flutter 单引擎时代中,也都是推荐用 Texture 外接纹理的方式来缓解内存压力。理论上,多引擎应该比单引擎更需要外接纹理方案,毕竟在多引擎的机制下,FlutterEngine 和 FlutterEngine 之间也是不共享资源的,更容易导致内存浪费的问题。

金戈鐡馬 1362

树莓派与无人机协同实战:基于OpenCV的地标识别优化策略

本文深入探讨了基于树莓派与无人机的协同系统,重点分享了利用OpenCV优化地标识别以实现精准降落的实战策略。内容涵盖从应对复杂光照的图像预处理、鲁棒的轮廓几何过滤,到通过ROS进行稳定通信,以及实现分层控制与状态机设计,旨在提升系统在真实环境中的可靠性与性能。

y2z3a4b5c的博客 1086

【Flutter】使用PlatformView显示WebView

通过PlatformView实现Flutter插件,能够调用和显示Android的WebView等Native控件

移动端开发干货分享 4435

正交试验minitab

1.创建正交试验 2.根据自己的实验选择水平数和因子数,并选定一个正交表 3.得到正交表 4.填写实验结果 5.进行正交分析 点击结果,点击分析 6.得到分析结果 7.方差分析 选择响应变量与因子,点击确定 得到方差分析的结果 ...

a999999123的博客 1万+

从零到一:Mapviz实战部署与多源数据可视化全解析

本文详细解析了Mapviz在机器人开发中的实战部署与多源数据可视化应用。从安装配置、卫星地图接入,到GPS轨迹、相机图像与激光雷达点云的融合显示,提供了完整的操作指南。针对ROS1与ROS2环境的选择与迁移给出了明确建议,并分享了坐标系对齐、数据同步等常见问题的避坑方案,帮助开发者高效构建室外机器人的“上帝视角”调试平台。

weixin_29230773的博客 252

两张(多张)图片合成一张图片

下面向通过颜色创建了一张空白图片,然后将目标图片居中在上一个图片中,这里可以通过同样的方法绘制多张图片到一张图片上去- (UIImage *) drawRectForImage:(UIImage *) image { UIImage* rectImage = [UIImage imageWithColor:[UIColor whiteColor] size:CGSizeMake(self.f

reylen的专栏 1万+

[GXYCTF2019]BabyUpload1 -- .htaccess文件解析与蚁剑实战指南

本文深入解析了[GXYCTF2019]BabyUpload1文件上传漏洞的实战利用。通过绕过后缀名与内容检测,上传包含PHP代码的.jpg文件,并利用.htaccess文件强制服务器将其解析为PHP脚本。文章详细介绍了.htaccess的原理、构造方法、Content-Type绕过技巧,并演示了如何使用中国蚁剑连接Webshell获取Flag,为Web安全学习提供了完整实战指南。

weixin_29016315的博客 509

混合开发框架|Flutter多引擎&dart多入口设计实现

仿照工行投资理财页面,使用Flutter实现。通过此讲述混合开发中,项目设计中,需求下如何创建多个Flutter引擎。Flutter引擎如何对应各自的dart入口,并实现每个Flutter引擎下路由切换页面。从而完成Flutter项目架构的设计搭建。

文化园 2542

SXU—软件测试方法和技术-朱少民-第三部分 软件测试方法大题汇总-附考点和相关习题及解答

软件测试方法和技术-朱少民-第三部分 软件测试方法—-最少测试用例个数,强一般弱一般强健壮、边界值分析、ns流程图、白盒逻辑覆盖代码、等价类划分,逻辑组合(条件)覆盖,计算圈复杂度等—附考点和相关习题及解答

weixin_63597914的博客 2211

使用CloudCompare和CANUPO进行点云分类的编程指南

在本文中,我们将介绍如何使用CloudCompare和CANUPO库进行点云分类的编程。CloudCompare是一款功能强大的开源点云处理软件,而CANUPO是一个用于点云分类和分割的C++库。这只是一个简单的点云分类示例,CANUPO库还提供了其他更高级的功能和算法,以满足各种点云分类需求。该函数接受点云数据、地面点掩码和非地面点掩码作为输入,并将分类结果存储在这两个掩码向量中。完成安装后,我们可以开始编写点云分类的代码。最后,我们遍历点云数据,并根据地面点和非地面点的掩码将结果打印出来。

ByteWhisper的博客 1058

耗时8个月,2960页,58万字,全网最全的Android开发笔记

笔记特点:内容详实,涵盖Android开发各个内容。条理清晰,系统化地阐述了Android开发各个知识点。 内容概要:一共七个模块,包括全方面性能调优、Android框架体系架构、flutter技术进阶、NDK模块开发、架构师筑基必备技能、设计思想解读开源框架、微信小程序。 由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。如有需要获取资料文档的朋友 可以关注+点赞后台简信我【笔记】免费获取。 360°全方面性能调优 一、设计思想与代码质量优化 二、程序性能优化 三、开发效率优化

Aerfa789的博客 1933

YOLOv8 更换损失函数DIoU、EIoU、GIoU...Inner-IoU...

介绍了一些损失函数,并将其应用到yolov8训练中

分享一些自己改进代码的实例 888

Android进阶——借鉴FlutterEngine,实现子进程渲染视图

前言 最近看到一个多进程的方案,通过启动Service,在子进程渲染视图的方案,在子进程渲染视图会减少和主进程的内存和主进程的渲染,类似于多进程方案,此方案借鉴FlutterEngine的源码,内容使用到Flutter的代码和AIDL的知识,不懂AIDL的同学可以看我的博客,先学习AIDL跨进程通讯 方案 方案实现起来内容不多,暂时只针对渲染的视图来做,如果需要触摸等事件,需要进一步阅读FlutterEngine源码进行适配 创建SurfaceView 创建Service 在Service创建Virtua

点击置顶文章查看博客目录(全站式导航) 994

CarSim仿真快速入门(十八)-CarSim2021中的混合动力和纯电动系统

CarSim和TruckSim包括用于电动汽车(BEV)和混合电动汽车(HEV)的动力总成系统。电动和混合电动系统可与所有现有驱动系统配合使用,例如:FWD,RWD,AWD和多轴驱动系统。混合动力/电动系统模型还包括电池管理控制。CarSim或TruckSim中的默认动力总成是为闭环速度控制器提供的备用动力总成。要安装和使用完整的动力总成,请从Vehicle: Assembly库或Vehicle: Lead Unit 库链接到Powertrain数据集。在“动力总成”界面上,从下拉列表控件中选择以下三个选项

小明师兄的博客 7455

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Flutter 3.0 之 PlatformView :告别 VirtualDisplay ,拥抱 TextureLayer

在 Flutter 3.0 发布之前,我们通过 《Flutter 深入探索混合开发的技术演进》 盘点了 Flutter 混合开发的历史进程, 在里面就提及了第一代 PlatformView 的实现 VirtualDisplay 即将被移除,而随着最近 Flutter 3.0 的发布,这个变更正式在稳定版中如期而至,所以今天就详细分析一下,新的 TextureLayer 如何替代 PlatformView 。 首先,如下图所示,简单对比 VirtualDisplay 和 TextureLayer 的

恋猫de小郭的博客 3678

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

【Flutter原理】平台视图整合Hybrid composition分析,Android开发知识体系

算法知识点繁多,企业考察的题目千变万化,面对越来越近的“金九银十”,我给大家准备好了一套比较完善的学习方法,希望能帮助大家在有限的时间里尽可能系统快速的恶补算法,通过高效的学习来提高大家面试中算法模块的通过率。这一套学习资料既有文字档也有视频,里面不仅仅有关键知识点的整理,还有案例的算法相关部分的讲解,可以帮助大家更好更全面的进行学习,二者搭配起来学习效果会更好。部分资料展示:有了这套学习资料,坚持刷题一周,你就会发现自己的算法知识体系有明显的完善,离大厂Offer的距离更加近。【GitHub】

m0_61549740的博客 1164

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

Flutter混合栈管理方案对比

flutter_thiro是哈喽单车团队提供的一个解决方案,其与flutter_boost的主要不同是,flutter_boost的导航切换都是由native侧驱动,每次页面切换native侧都会创建一新的页面放到导航栈中,而flutter_thrio在native之间及native和flutter之间的页面切换同样由native侧驱动,但flutter页面内部的切换由flutter自带的Navigator来管理,native侧导航栈不创建对应的页面容器。,并且由于pop出的页面就会销毁,

老猿的自留地 2699

Flutter 深入探索混合开发的技术演进

关于 Flutter 混合 PlatformView 的实现已经介绍过两次,随着 5 月份谷歌 IO 的接近,新的 PlatformView 实现应该也会随之而来,本次就从头到尾来一个详细的关于 PlatformView 的演进总结。 Flutter 作为新一代的跨平台框架,通过自定义渲染引擎的创新大大提高了跨平台的性能和一致性,但也正是因为这点, 相比之下 Flutter 在混合开发时对于原生控件的支持成本更高。 Flutter 混合开发的难点 首先 Flutter 在混合开发中最大的难点就在于它

恋猫de小郭的博客 4521

Flutter 1.20之Hybrid Composition混合图层的原理分析

介绍 在 Flutter 1.20, 官方推出了Hybrid Composition,此方法仿照ios端的platform view 在flutter中的显示方式,相较于原来的AndroidView性能要更好。 此文将分析Hybrid Composition 在绘制方面的工作流程。 在此之前建议先了解使用一下,具体使用方法,可点击下方链接: 官方使用文档 此文将会横跨 flutter、android和engine,我会尽量简要以缩短篇幅。 你可能还需要对surface有一定的了解。 我们就从Flutte

ljq5945的博客 1763

android单选框!来一份全面的面试宝典练练手,挥泪整理面经

并发编程之线程的中止 万事万物有始就有终,当我们新启了一个线程后,线程自然也有结束的那一刻。一般来说,线程如果是run方法执行完成了,或者是抛出了一个未处理的异常导致线程提前结束,我们都称为线程的自然终止。对于我们来说,如何才能安全自行中止我们的线程呢? 面试中面试官的问题,也很可以直观的让你了解到对方公司的技术水平和业务需求,而换工作对我更多考虑的是公司发展与团队氛围。 一个公司的发展与否直接关系到你付出的回报,而一个团队的氛围决定你的工作是在成长还是在“扯皮”。其次身体健康和家庭也很重要,在衡量一份工作

253

Flutter Platform View 使用及原理简析

什么是 platform view? 由于 Flutter 诞生于 Android 、iOS 非常成熟的时代背景,为了能让一些现有的 native 控件直接引用到 Flutter app 中,Flutter 团队提供了 AndroidView 、UIKitView 两个 widget 来满足需求,比如说 Flutter 中的 Webview、MapView,暂时无需使用 Flutter 重新开发...

QiwooMobile的博客 8742

【自学Flutter】3.3 图片的混合模式和重复

3.3 图片的混合模式和重复 1.源代码 import 'package:flutter/material.dart'; void main () => runApp(MyApp()); class MyApp extends StatelessWidget{ @override Widget build(BuildContext context ){ return Ma...

技术宅-Nopi的博客 1203

Flutter深入理解-Flutter面试

未来可能要准备Flutter的面试机会,所以在网上找了一圈与Flutter相关的面试题,提前准备下。 一、Dart 部分 Dart 语言综合了动态语言和静态语言的特性,动态语言指的是可以例如 dynamic 可以在运行用不同的类型数据给变量赋值,var 可以在运行时确定真正的数据类型,而不用在声明的时候就确定了将来要赋值的数据类型。同时,Dart 也是单线程的,跟 JS 类似,通过事件驱动模型来运行整个代码的执行过程。下面提出一些有意思的东西或者概念。 1. Dart 语言类型 Dart 属于强类型

好好学习,天天向上 2684

Flutter开发使用PlatformView显示iOS原生View(50)

我们在使用Flutter开发跨平台开发移动APP时,会遇到Flutter的组件满足不了原生的效果,部分控件不如原生控件好用时,就想在Flutter 的Widget 中使用iOS原生View来组合实现良好的效果。PlatformView是 Flutter 官方提供的一个可以嵌入 Android 和 iOS 平台原生 view 的小部件。 什么是 PlatformView? PlatformView...

沐雨07的专栏 3951

【Flutter原理】平台视图整合Hybrid composition分析

总而言之,成功是留给准备好的人的。无论是参加什么面试,都要做好充足的准备,注意好面试的礼仪和穿着,向面试官表现出自己的热忱与真诚就好。即使最后没有过关,也要做好经验的总结,为下一次面试做好充足准备。这里我为大家准备了一些我在面试后整理的面试专题资料,除了面试题,还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家,希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

783
上一篇: 一起来找茬:记一起 clang 开启 -Oz 选项引发的血案
下一篇: 应用性能前端监控,字节跳动这些年经验都在这了
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值