其他
干货 | Flutter 地图在携程的最佳实践
作者简介
Leo,携程高级移动开发工程师,关注跨端技术,致力于高效、高性能开发。
Jarmon,携程高级移动开发工程师,专注 Flutter、iOS 开发。
一、背景
官方提供的插件成熟度不够,有一些 Native 已有的 API 在 Flutter 上不支持;
目前接入 Flutter 地图插件的应用很少,我们需要去蹚雷。
由于官方适配的是纯 Flutter 项目,混合工程可能遇到很多未知棘手问题。
Native 地图成熟,不会遇到很大的坑;
主要问题在于业务在 Flutter上,Flutter 需要大量的和地图组件进行交互、请求数据、联动。需要通过大量的桥方法去传递操作数据;
要嵌套 Native 地图需要定制容器,Android 和 IOS 上各自得实现一遍桥、容器和地图逻辑,增加了维护成本。
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) {
NSDictionary<NSString*, id>* args = [call arguments];
long viewId = [args[@"id"] longValue];
NSObject<FlutterPlatformView>* embedded_view = [factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:params]; // 初始化
UIView* platform_view = [embedded_view view];
FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
initWithEmbeddedView:platform_view
platformViewsController:GetWeakPtr()
gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]]
autorelease];
ChildClippingView* clipping_view =
[[[ChildClippingView alloc] initWithFrame:CGRectZero] autorelease];
[clipping_view addSubview:touch_interceptor];
root_views_[viewId] = fml::scoped_nsobject<UIView>([clipping_view retain]); // 缓存
}
void PlatformViewLayer::Preroll(PrerollContext* context,
const SkMatrix& matrix) {
set_paint_bounds(SkRect::MakeXYWH(offset_.x(), offset_.y(), size_.width(),
size_.height()));
context->has_platform_view = true;
set_subtree_has_platform_view(true); // 标记当前帧存在Platform View
std::unique_ptr<EmbeddedViewParams> params =
std::make_unique<EmbeddedViewParams>(matrix, size_,
context->mutators_stack); context->view_embedder->PrerollCompositeEmbeddedView(view_id_,
std::move(params));
}
在原生开发中,我们知道UI操作不能在其他线程执行,会出现帧不同步的问题。flutter Engine 中有 platform、ui、raster、io四个线程,native view 是在 Platform Thread(主线程)渲染,而 flutter 渲染正常情况在 Raster Thread 执行的,flutter 又是如何保证帧同步的呢?
PostPrerollResult FlutterPlatformViewsController::PostPrerollAction(
fml::RefPtr<fml::RasterThreadMerger> raster_thread_merger) {
if (!HasPlatformViewThisOrNextFrame()) { // 没有Platform View不用处理
return PostPrerollResult::kSuccess;
}
if (!raster_thread_merger->IsMerged()) { // 线程还没有并不用处理
CancelFrame(); // 取消绘制当前帧
return PostPrerollResult::kSkipAndRetryFrame; // 合并后完成当前帧
}
BeginCATransaction();
raster_thread_merger->ExtendLeaseTo(kDefaultMergedLeaseDuration);
return PostPrerollResult::kSuccess;
}
// 合并队列
bool MessageLoopTaskQueues::Merge(TaskQueueId owner, TaskQueueId subsumed) {
if (owner == subsumed) {
return true;
}
std::lock_guard guard(queue_mutex_);
auto& owner_entry = queue_entries_.at(owner);
auto& subsumed_entry = queue_entries_.at(subsumed);
auto& subsumed_set = owner_entry->owner_of;
if (subsumed_set.find(subsumed) != subsumed_set.end()) {
return true;
}
owner_entry->owner_of.insert(subsumed);
subsumed_entry->subsumed_by = owner;
if (HasPendingTasksUnlocked(owner)) {
WakeUpUnlocked(owner, GetNextWakeTimeUnlocked(owner));
}
return true;
}
四、问题及解决方案
void SceneBuilder::pushTransform(Dart_Handle layer_handle,
tonic::Float64List& matrix4,
fml::RefPtr<EngineLayer> oldLayer) {
SkMatrix sk_matrix = ToSkMatrix(matrix4);
auto layer = std::make_shared<flutter::TransformLayer>(sk_matrix);
PushLayer(layer);
// matrix4 has to be released before we can return another Dart object
matrix4.Release();
EngineLayer::MakeRetained(layer_handle, layer);
if (oldLayer && oldLayer->Layer()) {
layer->AssignOldLayer(oldLayer->Layer().get());
}
}
- (void)viewDidLayoutSubviews {
...
if (firstViewBoundsUpdate && applicationIsActive && _engine) {
[self surfaceUpdated:YES];
}
...
}
- (void)surfaceUpdated:(BOOL)appeared {
if (appeared) {
[self installFirstFrameCallback];
[_engine.get() platformViewsController]->SetFlutterView(_flutterView.get());
[_engine.get() platformViewsController]->SetFlutterViewController(self);
[_engine.get() iosPlatformView]->NotifyCreated();
}
}
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) {
final float density = context.getResources().getDisplayMetrics().density;
}
public void detach() {
context = null;
}
从B页面 返回A页面
2022-08-22 15:13:08.126 21878-21878/ctrip.flutter.demo D/PlatformViewsController: B===>detach()
2022-08-22 15:13:08.135 21878-21878/ctrip.flutter.demo D/PlatformViewsController: A====>attach()
2022-08-22 15:13:08.249 21878-21878/ctrip.flutter.demo D/PlatformViewsController: A=====>detach()
void onDetach() {
if (host.shouldAttachEngineToActivity()) {
if (host.getActivity().isChangingConfigurations()) {
flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges();
} else {
flutterEngine.getActivityControlSurface().detachFromActivity();
}
}
3)解决问题
public boolean shouldAttachEngineToActivity() {
return false;
}
4.3 Android 地图内存溢出问题
2)分析问题
debugImplementation'com.squareup.leakcanary:leakcanary-android:2.6'
3)解决问题
public void detach() {
if (platformViewsChannel != null) {
platformViewsChannel.setPlatformViewsHandler(null);
}
}
public void setPlatformViewsHandler(@Nullable PlatformViewsHandler handler) {
if(handler == null && viewIdSet != null && viewIdSet.size() > 0) {
needReset = true;
return;
}
this.handler = handler;
}
Future<Uint8List?> customMark(String name, BuildContext context) async {
final scale = MediaQuery.of(context).devicePixelRatio;
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint();
final textPainter = TextPainter(textDirection: TextDirection.ltr);
...
final path = Path();
canvas.drawPath(path, paint);
// 绘制图片
final imageInfo = await UIImageLoader.imageInfoByAsset(HotelListImage.mapPoiMark);
paintImage(canvas: canvas,rect: rect,image: imageInfo.image);
// 生成绘制图片
final image = await recorder.endRecording().toImage(
width.toInt(), (textBgHeight + arrowHeight + iconHeight + 2).toInt());
final data = await image.toByteData(format: ImageByteFormat.png);
return data?.buffer.asUint8List();
}
class WeakPtr {
T* operator->() const {
CheckThreadSafety();
return get();
}
}
if (0 == pthread_getname_np(current_thread, actual_thread,
buffer_length) &&
0 == pthread_getname_np(self_, expected_thread, buffer_length)) {
FML_DLOG(ERROR) << "IsCreationThreadCurrent expected thread: '"
<< expected_thread << "' actual thread:'" // Object被创建的线程
<< actual_thread << "'"; // 实际执行线程
}
BMFCoordinateBounds? getMarkersVisibleMapBounds(List<BMFMarker> markers) {
if (markers.isEmpty) return null;
final firstPosition = markers.first.position;
double maxLatitude = firstPosition.latitude;
double minLatitude = firstPosition.latitude;
double maxLongitude = firstPosition.longitude;
double minLongitude = firstPosition.longitude;
for (final marker in markers) {
final lat = marker.position.latitude;
final lon = marker.position.longitude;
maxLatitude = max(maxLatitude, lat);
minLatitude = min(minLatitude, lat);
maxLongitude = max(maxLongitude, lon);
minLongitude = min(minLongitude, lon);
}
return BMFCoordinateBounds(
northeast: BMFCoordinate(maxLatitude, maxLongitude),
southwest: BMFCoordinate(minLatitude, minLongitude));
}
Future<bool> setAllMarkersVisibleWithPadding(
List<BMFMarker> markers,
BuildContext context, {
EdgeInsets insets = const EdgeInsets.all(20.0),
}) async {
final bounds = getMarkersVisibleMapBounds(markers);
if (bounds == null) return false;
if (Util.isAndroid()) {
final scale = MediaQuery.of(context).devicePixelRatio;
insets = EdgeInsets.only(
top: insets.top * scale,
bottom: insets.bottom * scale,
left: insets.left * scale,
right: insets.right * scale);
}
return await setVisibleMapRectWithPadding(
visibleMapBounds: bounds, insets: insets, animated: true);
}
七、总结
干货 | 携程酒店 Flutter 性能优化实践 干货 | 携程机票 App KMM iOS 工程配置实践 开源 | 携程机票跨端 Kotlin DSL 数据库框架 SQLlin 干货 | 降低 20% 链路耗时,Trip.com APP QUIC 应用和优化实践
“携程技术”公众号
分享,交流,成长