赞
踩
作者:字节移动技术-李皓骅
本文介绍了 Flutter 多引擎下,使用 PlatformView 场景时不能绕开的一个线程合并问题,以及它最终的解决方案。最终 Pull Request 已经 merge 到 Google 官方 Flutter 仓库:
https://github.com/flutter/engine/pull/27662
本文关键点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FIt1i6la-1631187770198)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/0.png)]
首先,介绍下 PlatformView 是什么,其实它简单理解成——平台相关的 View 。也就是说,在Android 和 iOS 平台原生有这样的控件,但是在Flutter的跨平台控件库里没有实现过的一些Widget,这些控件我们可以使用Flutter提供的PlatformView的机制,来做一个渲染和桥接,并且在上层可以用Flutter的方法去创建、控制这些原生View,来保证两端跨平台接口统一。
比如WebView,地图控件,第三方广告SDK等等这些场景,我们就必须要用到PlatformView了。
举一个例子,下图就是 Android 上使用 PlatformView 机制的 WebView 控件和 Flutter控件的混合渲染的效果:
可以看到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 Engine 需要提供4个 Task Runner,这4个 Runner 默认的一般情况下分别对应分别着4个操作系统线程,这四个 Runner 线程各司其职:
Task Runner | 作用 |
---|---|
Platform Task Runner | App 的主线程,用于处理用户操作、各类消息和 PlatformChannel ,并将它们传递给其他 Task Runner 或从其他 Task Runner 传递过来。 |
UI Task Runner | Dart VM 运行所在的线程。运行 Dart 代码的线程,负责生成要传递给 Flutter 引擎的 layer tree。 |
GPU Task Runner (Raster Task Runner) | 与 GPU 处理相关的线程。它是使用 Skia 最终绘制的过程相关的线程(OpenGL 或 Vulkan 等等) |
IO Task Runner | 执行涉及 I/O 访问的耗时过程的专用线程,例如解码图像文件。 |
如下图所示:
关于线程合并,我们可能有下面几个疑问:
我们来怀着这几个疑问去分析问题。
为什么在使用PlatformView的时候,需要把 Platform 线程和 Raster 线程合并起来?
简单的说就是:
那么,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,我们分析下各自的四大线程的运行状态:
Platform ✅ / UI ✅ / Raster ✅ / IO ✅
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)]
关键的两个点就是:
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时,都遇到了线程合并的问题。
最早是 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.
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是Flutter 1.22+独立引擎的问题,我在代码中搜索raster_thread_merger.cc(48)] Check failed: success. Unable to merge the raster and platform threads
其中raster_thread_merger.cc的48行这样的代码:
当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可以看出:
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之类的函数):
在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操作会失败并打印一个日志:
所以,在官方那是一个todo,是待实现的feature。
问题2是Flutter 2.0+轻量级引擎下的问题,直接看轻量级多引擎下,检查失败的那一行的源码:
很明显,和上面的独立多引擎不同,这里在创建RasterThreadMerger的构造函数的FML_CHECK检查就失败了,证明platform和raster已经是merge的状态了,所以这里也是SIGABRT并且程序退出了。
通过打印log看到两个引擎的platform和raster的id是共享的,引擎1和引擎2的platform_queue_id都是0,raster_queue_id都是2
很容易我们可以推理得到,多引擎的每个引擎都需要有一套四大线程,它们可以选择公用,或者也可以选择创建自己独立的线程。
我们通过之前的log打印的task_queue_id,分析一下两个问题唯一的区别:
独立引擎1 | 独立引擎2 | |
---|---|---|
platform_task_queue_id | 0 | 0 |
ui_task_queue_id | 1 | 4 |
raster_task_queue_id | 2 | 5 |
io_task_queue_id | 3 | 6 |
轻量级引擎1 | 轻量级引擎2 | |
---|---|---|
platform_task_queue_id | 0 | 0 |
ui_task_queue_id | 1 | 1 |
raster_task_queue_id | 2 | 2 |
io_task_queue_id | 3 | 3 |
所以相对来讲,感觉问题2更容易解决,并且我们使用flutter 2.0和卡片方案的业务,马上就将要遇到这个问题。
官方的轻量级引擎有一个TODO列表,把这个问题标记成Cleanup的任务:
https://github.com/flutter/flutter/issues/72009
官方标记了P5优先级:
因为业务需要所以直接就不等了,我们干脆自己实现它。
既然在轻量级引擎下,platform 线程和 raster 线程都是共享的,只是 engine 和 rasterizer 的对象是分开的,并且现在的逻辑是分别在两个引擎里,new 了自己的 RasterThreadMerger对象,进行后续的 merge 和 unmerge 操作。并且在 merge 的时候做是否Owns的检查。
那我们可以简单的做这几件事:
修改方案基本是坤神(我们Flutter组的战友)的 prototype 提交的方案,并且加一些边角的处理即可。
Prototype原型的关键修改的地方:
每个带title的都是一个FlutterView,终于不崩溃了:
效果截图:
但是这只是一个原型,很多状态问题和merge的逻辑我们没有处理的很好,问题包括:
所以我们需要有一套真正的终极解决方案,最好能:覆盖两个raster同时merge到一个platform的情况,然后贡献给官方。
经过查看代码里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-eXiaP6lp-1631187770218)( https://lf3-client-infra.bytetos.com/obj/client-infra-images/eggfly/215a5ce14fe240e88df1f44e8d665887/2021-09-09/15.png)]
另外还有一种情况没有列出,自己merge到自己的情况:现在的代码默认返回true的。
总结一句话就是一个queue可以合并多个queue(可以有多个下游),但是一个queue不可以有多个上游。
此实现的设计:
owner_of
从TaskQueueId
改成std::set<TaskQueueId> owner_of
,来记录这个线程所有merge过的subsumed_id(一对多的merge关系)std::map<ThreadMergerCaller, int>
来记录合并状态和lease_term租约计数器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;
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(); }
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>()
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类的成员变量来解决这个生命周期的问题。
然后windows的崩溃栈默认不会打印到terminal:谷歌的luci平台上的失败信息:
可以看到什么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() 等等 };
结果是java层对FlutterImageView的resize造成创建ImageReader的宽高为0了,Android不允许创建宽高是0的ImageReader:
所以又有一个bugfix的提交,已merge到官方
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。