最新要闻
- 国际原子能机构扩大扎波罗热核电站检查范围
- 开发区184项高频政务服务实现“跨省通办”只进“一扇门”办好“两地事”
- 最难相处的星座
- 视频|上海南京路“快乐倒计时”的交管小哥火了!本人回应:想给大家活跃活跃气氛
- 李家超来贵阳夜市喝奶茶吃鸡蛋仔,说要买贵州非遗产品送太太
- 美新型毒品泛滥吸食者如丧尸围城,仿若现实版“生化危机”
- 佳禾食品(605300.SH):目前咖啡豆和咖啡粉毛利率差别不大
- 陆家嘴10亿元中票拟于7月20日付息 利率2.90%
- “趁热”来贴!金山区“冬病夏治”门诊安排来了
- 持续小阴,还需耐心
- 大模型时代的AI十大趋势报告发布,大模型技术驱动“人机共生”丨2023世界人工智能大会
- 我市2名小学生夺得陕西省小学生趣味编程竞赛一等奖
- 重庆建筑科技职业学院环境艺术学院举办“繁星璀璨,童心筑梦” 2023年大学生暑期“三下乡”活动
- 自动驾驶“车内无人”商业化试点在北京开放
- 可乐酱油鹌鹑蛋做法?
- 世界人工智能大会在沪举办 30余款新品首发首展
手机
Flutter 地图在携程的最佳实践
曲江新区2021年后备人才“星火训练营”行动学习论文评审会暨结训仪式圆满举办!
- Flutter 地图在携程的最佳实践
- 曲江新区2021年后备人才“星火训练营”行动学习论文评审会暨结训仪式圆满举办!
- 中国人民银行发布《中央银行存款账户管理办法》
- 江西省统计局发布声明:防范假冒省统计局名义征订书籍
- 上海静安启动下半年重大项目建设推进工作,将新建一座工人文化宫
- 走近内地“快乐不知时日过” 参访浙江港澳青年高铁踏返程
家电
Flutter 地图在携程的最佳实践
作者简介
Leo,携程高级移动开发工程师,关注跨端技术,致力于高效、高性能开发。
(资料图)
Jarmon,携程高级移动开发工程师,专注 Flutter、iOS 开发。
本文将重点突出基于 flutter-boost 的混合工程,单引擎模式下接入 Flutter 地图插件遇到的问题和解决方案。
一、背景随着各种多端技术的蓬勃发展,项目主体从纯 Native 项目,到 Native+RN,到现在的 Native+RN+Flutter。基于我们的业务都在 Flutter 技术栈上面,这要求我们需要嵌套展示地图。目前,实现嵌套展示地图的主要方案有二个:
接入官方提供的 Flutter 地图插件,主要面临的问题有:
官方提供的插件成熟度不够,有一些 Native 已有的 API 在 Flutter 上不支持;目前接入 Flutter 地图插件的应用很少,我们需要去蹚雷。由于官方适配的是纯 Flutter 项目,混合工程可能遇到很多未知棘手问题。直接在 Flutter 页面上展示 Native 的地图:
Native 地图成熟,不会遇到很大的坑;主要问题在于业务在 Flutter上,Flutter 需要大量的和地图组件进行交互、请求数据、联动。需要通过大量的桥方法去传递操作数据;要嵌套 Native 地图需要定制容器,Android 和 IOS 上各自得实现一遍桥、容器和地图逻辑,增加了维护成本。考虑维护成本、权衡再三我们还是选择接入 Flutter 地图插件。为了能更好的定制一些 API 和更快速的修复一些官方没有及时更新的问题。我们采用的是源码接入 Flutter 地图插件。本文将重点突出基于 flutter-boost 的混合工程,单引擎模式下接入 Flutter 地图插件遇到的问题和解决方案。
二、如何源码集成在混合项目中集成插件主要分 flutter 和原生两侧,集成 Flutter 插件时,官方 demo中可以直接下载到插件的源码。本文以接入 flutter 地图插件 3.3.1 版本示例。
2.1 Flutter 端集成
获取到官方 demo 后在该目录下执行 flutter pub get,然后去 flutter SDK 下找到 pub-cache 依赖缓存文件目录,根据业务需要将每个插件 src 文件下的代码导入到 flutter 工程中。
2.2 IOS 端集成
执行完 flutter pub get 后,根据需要将每个插件 iOS/Classes/ 目录下的代码导入工程中。
2.3 Android 端集成
Android 的 Native 侧的集成和 IOS 端是类似的。在 Native 工程中新建一个地图 Module。把地图 Demo 中的地图插件源码 Android 部分放入工程即可。
三、地图插件实现原理:platformView地图插件按功能分为 Map、Search、Util 等模块,其基本实现类似,使用 MethodChannel 与 native 通信,我们以 Map 为例分析其实现。插件使用了 PlatformView 将原生地图嵌入到 flutter 页面中,在 flutter 层为 UIKitView、AndroidView,native 在生成地图后根据 viewId 初始化 BMFMapViewController,包含对应的 MethodChannel。BMFMapViewController 聚合了对地图操作,派发到不同模块调用地图 native 方法。
3.1 什么是PlatformView
PlatformView 是允许原生组件嵌入到 Flutter 页面的一种技术,能够让我们将一些原生成熟组件、flutter UI 框架难以实现的地图、WebView 等组件展示在 flutter 页面中。
Flutter 提供了 Virtual Display、Hybrid Composition 两种方式实现 PlatformView。Virtual Display 模式将 native view 加载到内存当中,随着 flutter Widget 一起渲染出来。Hybrid Composition 模式是直接将 native view 添加到 flutter view 图层上。iOS采用了 Hybrid Composition 模式,Android 采用了 Virtual Display 和 Hybrid Composition 两种模式。
3.2 PlatformView 实现原理
1)flutter 渲染流程
在介绍 Hybrid Composition 实现之前,先通过下图大致了解下 flutter 的渲染流程。
在收到 VSync 信号之后,Dart 层在 UI Thread 完成 Widget Tree、Element Tree、RenderObject Tree 三棵树的更新与生成,然后生成包含绘制信息的 layer Tree 交给 Engine 去渲染,最后在 GPU Thread 经历 Compositor、Skia 将 flutter 视图渲染出来。
2)Hybrid Composition 模式分析
以 iOS 为例逐步分析 Hybird Composition 模式执行流程。首先 Dart 层提供了 UIKitView 组件来展示 native view,didChangeDependencies 方法中通过 channel 初始化一次 native view,生成唯一标识 native view 的 viewId,并将 native view 缓存在 root_views_ 中。在实际组装 layer 层时,dart 层会传输给 engine 展示 native view 的坐标和大小,并生成一个 PlatformViewLayer,也就是说 native view 的位置、大小信息是由 dart 层控制的。
void FlutterPlatformViewsController::OnCreate(FlutterMethodCall* call, FlutterResult& result) { NSDictionary* args = [call arguments]; long viewId = [args[@"id"] longValue]; NSObject* 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([clipping_view retain]); // 缓存}
生成当前帧的 Layer Tree 之后,会进入到 Rasterizer 流程。首先会调用 BeginFrame 渲染一帧,触发 PlatformViewLayer::Preroll,PlatformViewLayer 标记出当前帧有 PlatformView ,然后调用 FlutterPlatformViewsController::PrerollCompositeEmbeddedView 更新 view_params_,包含 Platform View 坐标、size 等信息,最后在 SubmitFrame 方法中取出 native view 添加到 flutter view 中,完成渲染。
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 params = std::make_unique(matrix, size_, context->mutators_stack); context->view_embedder->PrerollCompositeEmbeddedView(view_id_, std::move(params));}
3.3 PlatformView 是如何实现帧同步?
在原生开发中,我们知道UI操作不能在其他线程执行,会出现帧不同步的问题。flutter Engine 中有 platform、ui、raster、io四个线程,native view 是在 Platform Thread(主线程)渲染,而 flutter 渲染正常情况在 Raster Thread 执行的,flutter 又是如何保证帧同步的呢?
flutter 解决帧同步是通过线程合并的方案。上图 Raster 流程 PostPrerollAction 方法中,会判断如果有 PlatformView 存在,在接下来的绘制过程中 Raster Thread 与 Platform Thread 会合并,将 Raster 队列任务放到 Platform 队列中。这样所有的渲染任务都在 Platform Thread 中执行,保证了画面的同步。
PostPrerollResult FlutterPlatformViewsController::PostPrerollAction( fml::RefPtr 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;}
四、问题及解决方案4.1 IOS 页面切换 Map 组件白屏问题
在使用 flutter_boost 混合开发时,当 A 页面中使用 platformview,开启新容器跳转到 flutter B 页面,platformView 会出现短暂的白屏,从 A 页面跳转 native 页面不会出现。根据表象首先猜测是单引擎导致的。flutter A页面跳转到其他页面时都会触发 SceneBuilder::pushTransform 重新渲染一次 A 页面。
void SceneBuilder::pushTransform(Dart_Handle layer_handle, tonic::Float64List& matrix4, fml::RefPtr oldLayer) { SkMatrix sk_matrix = ToSkMatrix(matrix4); auto layer = std::make_shared(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()); }}
flutter A页面在创建新容器 push 到 flutter B 页面时,首先会触发 viewDidLayoutSubviews,方法内部会修改 engine 对应的 viewController flutterView,SceneBuilder::pushTransform 是在 viewDidLayoutSubviews 之后还会触发,而 platformView 是在 native 渲染,重新渲染 A 页面时就找不到对应的 platformView,导致白屏的问题。push 到非 flutter 页面时不会触发 surfaceUpdated,所以不会出现该问题。
- (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(); }}
一开始的方案是在 viewWillAppear 中调用 sufaceUpdated,但是在 release 环境中会出现卡死的现象。另一方案是 [super bridge_viewWillAppear:animated]; 改为 [super viewWillAppear:animated]; [super viewWillAppear:animated]; 会调用父类的方法,父类方法又会调用 sufaceUpdated,就可以解决白屏的问题。
4.2 Android 地图卡死不能操作问题
1)问题描述
A 页面内嵌地图,跳转到 B 页面。然后返回 A 页面,地图就不能滑动。
结合上文提到的 Flutter 地图插件其实是通过 MathodChannel 将操作传递到 Native 的地图视图处理的。我们调试 Native 的代码发现 PlatformViewsController 类里面的 onTouch()方法中,context 报了一个Attempt to invoke virtual method "android.content.res.Resources android.content.Context.getResources()" on a null object reference。
public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { final float density = context.getResources().getDisplayMetrics().density; }
2)分析问题
由于 context 对象被回收,造成的报错。现在我们只有分析出来为什么 context 对象会被回收掉了就能找出问题了,读源码发现只有在 detach() 方法中才会回收 context 对象。
public void detach() { context = null; }
结合日志输出,确实发现回到 A 页面是执行了 attach() 方法,但是马上又执行了 detach() 方法。现在就是要找出,为什么 A 页面的 PlatformViewsController 会被执行 datach()。
从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()
查看调用链:
逐个类读源码我们发现在 FlutterActivityAndFragmentDelegate的OnDetach() 方法中如果引擎的生命周期和 Activity 的生命周期是绑定的。页面结束时,引擎就会被销毁掉。
void onDetach() { if (host.shouldAttachEngineToActivity()) { if (host.getActivity().isChangingConfigurations()) {flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges();} else {flutterEngine.getActivityControlSurface().detachFromActivity(); } }
3)解决问题
设置shouldAttachEngineToActivity返回 flase 使得 Flutter 引擎将在应用程序的整个生命周期内持久化存在,并独立于 Activity,当 Activity 被销毁时,Flutter 引擎不被销毁 。问题就解决了。产生问题的原因是我们新开 B 页面是通过新开容器的方式创建的。B 页面 FlutterFragment 中 onDetach() 方法在 A 页面 onAttach() 之后被执行的。纯 Flutter 工程或者是采用 Push 的方式打开新页面,不新开容器都能规避掉这个问题。
public boolean shouldAttachEngineToActivity() { return false; }
4.3 Android 地图内存溢出问题
1)问题描述
多次打开 Android Flutter 地图页面会越来越卡,到后面整个地图都黑一下,显然是有内存溢出了。通过 Android Studio IDE 自带的内存工具 Android Profiler 可以很明显的看出来,每打开一次页面,内存占有都会上升,结束页面内存没有得到释放。
2)分析问题
Flutter Boost 和地图插件如此大量的第三方代码,我们如何去定位问题呢?是插件引起的,还是框架引起的呢?借助 LeakCanary 就能很好的找到内存泄露的地方了。
接入也非常的简单,在 Android build.gradle引入leakcanary。
debugImplementation"com.squareup.leakcanary:leakcanary-android:2.6"
然后运行应用,反复操作问题复现流程,直到 LeakCanary 提示。查看 leaks 内存溢出的堆栈信息。是由于 SingleViewPresentation 一直持有了容器 TripFlutterActivity 的 context 对象。怀疑是 MapView 的生命周期有问题。是不是没有执行 dispose。调试下来的情况 PlatformViewsHandler handler 对象空了,后面的流程都不会执行。
3)解决问题
查看源码只有 PaltformViewsController detach() 方法会把 handler 设置为 null。
public void detach() { if (platformViewsChannel != null) { platformViewsChannel.setPlatformViewsHandler(null); } }
调试下来 FlutterActivity 容器结束,调用了 onDestroy() 方法的时候 PaltformViewsController detach() 就已经被执行了。容器的 onDestroy() 在 MapView 的 dispos e之前,造成了 handler 对象空了。
解决问题的思路很简单,在 onDestroy() 的时候先保留 handler 对象,然后找个时机清除一下。采用 viewIdSet 自己维护一份 View 的数据。在 creat 方法中 disposeArgs.get("id") 执行过 dispose 方法的就删除掉 viewIdSet.remove(viewId)。setPlatformViewsHandler 为空的情况判断一下,有没有执行 dispose 的 view handler 先不回收。如下:
public void setPlatformViewsHandler(@Nullable PlatformViewsHandler handler) { if(handler == null && viewIdSet != null && viewIdSet.size() > 0) { needReset = true; return; } this.handler = handler; }
目前是执行 dispose 的时候 needReset 为 true 时会将 handler 设置为 null。为什么官方的 Demo 是没有问题的呢?主要原因还是我们接入了 FlutterBoost 默认是单引擎的,官方 Demo 是的纯 Flutter 项目多引擎。页面结束,通过销毁 engine 把问题覆盖了,所以内存回收表现的很平滑。
五、自定义文本 BitMap Marker地图业务中自定义 marker 是比较常见的需求,由于地图是通过 PlatformView 实现的,最容易想到的做法是,通过 Channel 传入 marker 对应的样式 Id 和展示所需数据,在各端绘制 marker,这种做法会增加人工成本,样式也可能存在不一致的情况,失去了 flutter 框架的优势。
地图插件在 v3.0(v3.0 之前需要自己实现)提供了 iconData 参数传入图片 data 信息,在 flutter 侧将文本、图片绘制出来生成一张图,将生成图片 Data 传递给原生,该实现并不需要改动各端代码,绘制时要注意视图大小是物理像素点,而不是逻辑像素点。
Future 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();}
从 flutter 2 升级到 flutter 3 出现了小插曲,iOS debug 环境调用 toImage 进程会被终止。flutter 升级之后对弱引用指针调用做了线程检查,创建和使用不是在同一线程在 debug 环境进程会被终止。toImage() 方法内使用了 fml::WeakPtr
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 << """; // 实际执行线程}
六、自定义让 Marker 展示在可见范围在地图上添加 marker 之后,将已添加的 marker 全部展示在可视范围内也是常见的需求。插件提供了支持 iOS 的 showmarkers 方法,这显然不能够满足需求。我们思考通过 setVisibleMapRectWithPadding 指定显示地图地理范围,该方法要求我们传入参数 visibleMapBounds,设置地理范围的东北坐标、西南坐标。由于右上角、左下角经纬度分为可视地理范围最大、最小,即可拿到东北、西南坐标。
BMFCoordinateBounds? getMarkersVisibleMapBounds(List 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));}
随着业务的迭代,需要将大地图融合到列表中。为了将大地图与小地图切换动画更加流畅,当小地图被加载时,地图 size 实际已经渲染成和大地图同样大小,下半部分被列表遮挡。这意味小地图需要设置可见范围的偏移量,但 inserts 参数 iOS、Android 计算方式不一样,iOS 是根据 point 计算,Android 是通过 pixel 计算,要区分平台做一次转换。
Future setAllMarkersVisibleWithPadding( List 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 地图插件基于Native地图 Android 和 iOS SDK 二次封装而成,通过在 Flutter 使用MethodChannel交互实现地图的显示、交互、覆盖物绘制和事件响应等功能。混合项目接入Flutter地图容易发生问题的点,基本集中在PlatformView这一块。通常是容器和View的事件、生命周期同步问题。
本文主要介绍FlutterBoost的混合工程,在接入Flutter地图插件遇到的各种问题和解决方案。阐述了PlatformView的工作原理,方便我们更好的理解Flutter地图插件。同时也介绍了如何用Android Studio 自带的工具直观地看内存异常。并且推荐leakcanary定位内存溢出的类和方法,希望对你接入Flutter地图插件有一定的帮助。
关键词:
Flutter 地图在携程的最佳实践
歌手李玟因抑郁症离世引发关注 首个国产抗抑郁创新药已获批
卡戴珊参加名人派对,和姆巴佩合影,狂秀腹肌和钻石,大出风头
国际原子能机构扩大扎波罗热核电站检查范围
规范实习行为 保障医疗安全 信阳市人民医院举办2023年实习生岗前培训
“马路办公 马上办理”封丘县持续开展“万人进万企”工作
河南镇平:“15分钟政务服务办事圈”增加民生温度
开发区184项高频政务服务实现“跨省通办”只进“一扇门”办好“两地事”
高考志愿填报,别让骗子钻了空子
曲江新区2021年后备人才“星火训练营”行动学习论文评审会暨结训仪式圆满举办!
河北滦平农商银行被罚40万元:因贷款风险分类不准确
襄阳市举办首届集体协商技能竞赛决赛
有人的财物被盗了,旁人总是“出于好心”说,你怎么这么不小心
“数智安全 内生为本”2023北京网络安全大会在京举办
杭州:爱心凉茶摊 夏日送清凉
深圳国际(00152.HK):7月7日南向资金增持64.65万股
越秀地产(00123.HK):7月7日南向资金减持11.7万股
中国人民银行发布《中央银行存款账户管理办法》
太阳雨空气能采暖机成功入驻珠峰大本营 为登顶活动提供清洁采暖保障
厦门国际会展中心D馆竣工投用
广州消防通报荔湾区新基路火灾事件
腾讯会议限制免费会议数 调整跨App加入会议功能
央行发布办法 规范中央银行存款账户服务和管理
河北省住房城乡建设厅通报2023年第六批11起典型违法案件
IAEA总干事回应专家分歧传言,并称不为核污水排海背书
罗马诺:热刺仍有意勒沃库森后卫塔普索巴,同时推进范德文交易
江西九江:不再延长阶段性购房补贴、交易契税财政补贴政策
最难相处的星座
青海海东警方破获特大电诈案 涉案金额高达5540余万元
央行发布《中央银行存款账户管理办法》 加强关键环节风险管控
惠而浦:预计上半年净利润3000万元-4500万元
江西省统计局发布声明:防范假冒省统计局名义征订书籍
支持文生图 阿里云推出通义万相AI绘画大模型
茅台网易合资公司新增百货销售业务 茅台网易合资公司新增票务代理服务
鸿海将为日本机器人公司Telexistence代工新一代机器人
视频|上海南京路“快乐倒计时”的交管小哥火了!本人回应:想给大家活跃活跃气氛
@所有人!崇左花山日游花山仅需9.9元,还能免费领取...
V观财报|业绩快报相关信披不准确 东方生物及董事长等被警示
刚刚!兰州9所省级示范高中统招预估录取线公布
张玉霞你是我的眼下载(你是我的眼 张玉霞)
西安市雁塔区人力资源和社会保障局在哪个位置(西安市雁塔区人力资源和社会保障局在哪)
上海静安启动下半年重大项目建设推进工作,将新建一座工人文化宫
社区生活一屏掌握 岳阳电信助力打造新型“智慧社区”
引领药物研发范式变革!腾迈医药探索用智能计算工具拓宽药物发现边界
中国信通院徐菲:明年将启动5G-A设备测试
中骏集团:前6月合约销售金额200.84亿元
又一家氢能关键零部件研发生产企业揭牌成立!
2023陈奕迅要在成都开演唱会吗?
全国一体化算力网络国家枢纽节点建设与数字经济
三棵树:预计上半年净利润3亿元-3.3亿元
持续更新!西安市民办中小学招生计划公布
河南省科技馆需要预约展厅票吗
NYMEX原油上看73.14美元
伊朗与苏丹寻求“尽快”复交
豫园股份股东累计3.19亿股股份解质 另有3.45亿股遭质押
李家超来贵阳夜市喝奶茶吃鸡蛋仔,说要买贵州非遗产品送太太
美新型毒品泛滥吸食者如丧尸围城,仿若现实版“生化危机”
鹏华基金闫思倩:下半年仍以新能源作为投资主线
工业富联完成董事会换届选举 郑弘孟接替李军旗当选董事长
车顶维权女车主诉特斯拉案将二审 车顶维权女车主诉特斯拉案二审将开庭
第一届ft靓王杯候选环节(UP个人向)
7月7日基金净值:华夏量化优选股票A最新净值0.8438,跌0.38%
时尚科技 | 从AI生成的时装设计图到实体服装的生产落地到底有多远?
车道偏离系统有什么用(丰田车道偏离系统怎么开启?)
欣旺达(300207.SZ):小米是公司重要的客户及合作伙伴
2023国寿臻鑫传家终身寿险怎么样?10年交领多少钱?
瑞丰光电(300241.SZ):公司暂未涉及光模块业务
佳禾食品(605300.SH):目前咖啡豆和咖啡粉毛利率差别不大
佳禾食品(605300.SH):液态产品跟粉态产品不是共用产线
陕西出台六条措施支持高校毕业生就业创业
陆家嘴10亿元中票拟于7月20日付息 利率2.90%
纽约双层观光巴士与公交车相撞致数十人受伤
廉江实验中学贴吧(廉江实验中学)
在我国起诉离婚二次多久能宣判
香港6月底官方外汇储备资产为4173亿美元
网络蜘蛛搜索引擎 蜘蛛搜索引擎
“趁热”来贴!金山区“冬病夏治”门诊安排来了
一学生发布虐猫视频,重庆城市科技学院通报:学校高度重视,经核实,情况属实
营口市:加压奋进 项目攻坚如火如荼
横州市:文明城市创建 “新”风扑面
全国沙滩排球巡回赛(敦煌站)鸣沙山下鸣哨开赛
山东用电负荷保持高位 多措并举保供电
八步区:科技助农保丰收
郑俊英金钟民(金钟民玄英)
呼朋引伴的意思解释是什么字(呼朋引伴的意思)
近400名医务人员汇聚于此“充电”,只为提升基层诊疗服务
兰剑智能(688557.SH):4名股东合计减持达1%股份
走近内地“快乐不知时日过” 参访浙江港澳青年高铁踏返程
肥西警方:“网上警民议事厅”为民办实事
2018款ES300h座椅怎么样及2018雷克萨斯ES300h发动机怎么样
《真人快打1》将加入Smoke和Rain
9月,不能惹的三个星座,勾心斗角,妥妥的狠角色
湖北健康码app怎么办理健康证 鄂汇办办理健康证方法介绍
“出水芙蓉”成市场爆款 苏州鲜切荷花销售火爆
比利时探亲访友签证最长几年
李玟嫁富豪后抑郁去世,梁洛施没有嫁给李泽楷,其实反而是好事
ai里面怎么做立体效果(ai中怎么做立体阴影)
图片新闻
中冶华南召开协同办公系统培训会
张薇任职交通银行四川省分行行长资格获批