热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Flutter的原理及美团的实践(中)

flutter,的,原理,及,

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

外卖全品类页面实践

在调研了Flutter的各项特性和实现原理之后,外卖计划灰度上线Flutter版的全品类页面。对于将Flutter页面作为App的一部分这种集成模式,官方并没有提供完善的支持,所以我们首先需要了解Flutter是如何编译、打包并且运行起来的。

Flutter App构建过程

最简单的Flutter工程至少包含两个文件:

运行Flutter程序时需要对应平台的宿主工程,在Android上Flutter通过自动创建一个Gradle项目来生成宿主,在项目目录下执行flutter create .,Flutter会创建ios和android两个目录,分别构建对应平台的宿主项目,Android目录内容如下:

此Gradle项目中只有一个app module,构建产物即是宿主APK。Flutter在本地运行时默认采用Debug模式,在项目目录执行flutter run即可安装到设备中并自动运行,Debug模式下Flutter使用JIT方式来执行Dart代码,所有的Dart代码都会打包到APK文件中assets目录下,由libflutter.so中提供的DartVM读取并执行:

kernel_blob.bin是Flutter引擎的底层接口和Dart语言基本功能部分代码:

third_party/dart/runtime/bin/*.dart third_party/dart/runtime/lib/*.dart third_party/dart/sdk/lib/_http/*.dart third_party/dart/sdk/lib/async/*.dart third_party/dart/sdk/lib/collection/*.dart third_party/dart/sdk/lib/convert/*.dart third_party/dart/sdk/lib/core/*.dart third_party/dart/sdk/lib/developer/*.dart third_party/dart/sdk/lib/html/*.dart third_party/dart/sdk/lib/internal/*.dart third_party/dart/sdk/lib/io/*.dart third_party/dart/sdk/lib/isolate/*.dart third_party/dart/sdk/lib/math/*.dart third_party/dart/sdk/lib/mirrors/*.dart third_party/dart/sdk/lib/profiler/*.dart third_party/dart/sdk/lib/typed_data/*.dart third_party/dart/sdk/lib/vmservice/*.dart flutter/lib/ui/*.dart 

platform.dill则是实现了页面逻辑的代码,也包括Flutter Framework和其他由pub依赖的库代码:

flutter_tutorial_2/lib/main.dart flutter/packages/flutter/lib/src/widgets/*.dart flutter/packages/flutter/lib/src/services/*.dart flutter/packages/flutter/lib/src/semantics/*.dart flutter/packages/flutter/lib/src/scheduler/*.dart flutter/packages/flutter/lib/src/rendering/*.dart flutter/packages/flutter/lib/src/physics/*.dart flutter/packages/flutter/lib/src/painting/*.dart flutter/packages/flutter/lib/src/gestures/*.dart flutter/packages/flutter/lib/src/foundation/*.dart flutter/packages/flutter/lib/src/animation/*.dart .pub-cache/hosted/pub.flutter-io.cn/collection-1.14.6/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/meta-1.1.5/lib/*.dart .pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.4.2/*.dart 

kernel_blob.bin和platform.dill都是由flutter_tools中的bundle.dart中调用KernelCompiler生成。

在Release模式(flutter run --release)下,Flutter会使用Dart的AOT运行模式,编译时将Dart代码转换成ARM指令:

kernel_blob.bin和platform.dill都不在打包后的APK中,取代其功能的是(isolate/vm)_snapshot_(data/instr)四个文件。snapshot文件由Flutter SDK中的flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot命令生成,vm_snapshot_是Dart虚拟机运行所需要的数据和代码指令,isolate_snapshot_则是每个isolate运行所需要的数据和代码指令。

Flutter App运行机制

Flutter构建出的APK在运行时会将所有assets目录下的资源文件解压到App私有文件目录中的flutter目录下,主要包括处理字符编码的icudtl.dat,还有Debug模式的kernel_blob.bin、platform.dill和Release模式下的4个snapshot文件。默认情况下Flutter在Application#onCreate时调用FlutterMain#startInitialization来启动解压任务,然后在FlutterActivityDelegate#onCreate中调用FlutterMain#ensureInitializationComplete来等待解压任务结束。

Flutter在Debug模式下使用JIT执行方式,主要是为了支持广受欢迎的热刷新功能:

触发热刷新时Flutter会检测发生改变的Dart文件,将其同步到App私有缓存目录下,DartVM加载并且修改对应的类或者方法,重建控件树后立即可以在设备上看到效果。

在Release模式下Flutter会直接将snapshot文件映射到内存中执行其中的指令:

在Release模式下,FlutterActivityDelegate#onCreate中调用FlutterMain#ensureInitializationComplete方法中会将AndroidManifest中设置的snapshot(没有设置则使用上面提到的默认值)文件名等运行参数设置到对应的C++同名类对象中,构造FlutterNativeView实例时调用nativeAttach来初始化DartVM,运行编译好的Dart代码。

打包Android Library

了解Flutter项目的构建和运行机制后,我们就可以按照其需求打包成AAR然后集成到现有原生App中了。首先在andorid/app/build.gradle中修改:

简单修改后我们就可以使用Android Studio或者Gradle命令行工具将Flutter代码打包到aar中了。Flutter运行时所需要的资源都会包含在aar中,将其发布到maven服务器或者本地maven仓库后,就可以在原生App项目中引用。

但这只是集成的第一步,为了让Flutter页面无缝衔接到外卖App中,我们需要做的还有很多。

图片资源复用

Flutter默认将所有的图片资源文件打包到assets目录下,但是我们并不是用Flutter开发全新的页面,图片资源原来都会按照Android的规范放在各个drawable目录,即使是全新的页面也会有很多图片资源复用的场景,所以在assets目录下新增图片资源并不合适。

Flutter官方并没有提供直接调用drawable目录下的图片资源的途径,毕竟drawable这类文件的处理会涉及大量的Android平台相关的逻辑(屏幕密度、系统版本、语言等等),assets目录文件的读取操作也在引擎内部使用C++实现,在Dart层面实现读取drawable文件的功能比较困难。Flutter在处理assets目录中的文件时也支持添加多倍率的图片资源,并能够在使用时自动选择,但是Flutter要求每个图片必须提供1x图,然后才会识别到对应的其他倍率目录下的图片:

flutter: assets: - images/cat.png - images/2x/cat.png - images/3.5x/cat.png new Image.asset('images/cat.png'); 

这样配置后,才能正确地在不同分辨率的设备上使用对应密度的图片。但是为了减小APK包体积我们的位图资源一般只提供常用的2x分辨率,其他分辨率的设备会在运行时自动缩放到对应大小。针对这种特殊的情况,我们在不增加包体积的前提下,同样提供了和原生App一样的能力:

  1. 在调用Flutter页面之前将指定的图片资源按照设备屏幕密度缩放,并存储在App私有目录下。
  2. Flutter中使用时通过自定义的WMImage控件来加载,实际是通过转换成FileImage并自动设置scale为devicePixelRatio来加载。

这样就可以同时解决APK包大小和图片资源缺失1x图的问题。

Flutter和原生代码的通信

我们只用Flutter实现了一个页面,现有的大量逻辑都是用Java实现,在运行时会有许多场景必须使用原生应用中的逻辑和功能,例如网络请求,我们统一的网络库会在每个网络请求中添加许多通用参数,也会负责成功率等指标的监控,还有异常上报,我们需要在捕获到关键异常时将其堆栈和环境信息上报到服务器。这些功能不太可能立即使用Dart实现一套出来,所以我们需要使用Dart提供的Platform Channel功能来实现Dart→Java之间的互相调用。

以网络请求为例,我们在Dart中定义一个MethodChannel对象:

import 'dart:async'; import 'package:flutter/services.dart'; const MethodChannel _channel = const MethodChannel('com.sankuai.waimai/network'); Future> post(String path, [Map form]) async { return _channel.invokeMethod("post", {'path': path, 'body': form}).then((result) { return new Map.from(result); }).catchError((_) => null); } 

然后在Java端实现相同名称的MethodChannel:

public class FlutterNetworkPlugin implements MethodChannel.MethodCallHandler { private static final String CHANNEL_NAME = "com.sankuai.waimai/network"; @Override public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) { switch (methodCall.method) { case "post": RetrofitManager.performRequest(post((String) methodCall.argument("path"), (Map) methodCall.argument("body")), new DefaultSubscriber() { @Override public void onError(Throwable e) { result.error(e.getClass().getCanonicalName(), e.getMessage(), null); } @Override public void onNext(Map stringBaseResponse) { result.success(stringBaseResponse); } }, tag); break; default: result.notImplemented(); break; } } } 

在Flutter页面中注册后,调用post方法就可以调用对应的Java实现:

loadData: (callback) async { Map data = await post("home/groups"); if (data == null) { callback(false); return; } _data = AllCategoryResponse.fromJson(data); if (_data == null || _data.code != 0) { callback(false); return; } callback(true); }), 

SO库兼容性

Flutter官方只提供了四种CPU架构的SO库:armeabi-v7a、arm64-v8a、x86和x86-64,其中x86系列只支持Debug模式,但是外卖使用的大量SDK都只提供了armeabi架构的库。

虽然我们可以通过修改引擎src根目录和third_party/dart目录下build/config/arm.gnithird_party/skia目录下的BUILD.gn等配置文件来编译出armeabi版本的Flutter引擎,但是实际上市面上绝大部分设备都已经支持armeabi-v7a,其提供的硬件加速浮点运算指令可以大大提高Flutter的运行速度,在灰度阶段我们可以主动屏蔽掉不支持armeabi-v7a的设备,直接使用armeabi-v7a版本的引擎。

做到这点我们首先需要修改Flutter提供的引擎,在Flutter安装目录下的bin/cache/artifacts/engine下有Flutter下载的所有平台的引擎:

我们只需要修改android-arm、android-arm-profile和android-arm-release下的flutter.jar,将其中的lib/armeabi-v7a/libflutter.so移动到lib/armeabi/libflutter.so即可:

cd $FLUTTER_ROOT/bin/cache/artifacts/engine for arch in android-arm android-arm-profile android-arm-release; do pushd $arch cp flutter.jar flutter-armeabi-v7a.jar # 备份 unzip flutter.jar lib/armeabi-v7a/libflutter.so mv lib/armeabi-v7a lib/armeabi zip -d flutter.jar lib/armeabi-v7a/libflutter.so zip flutter.jar lib/armeabi/libflutter.so popd done 

这样在打包后Flutter的SO库就会打到APK的lib/armeabi目录中。在运行时如果设备不支持armeabi-v7a可能会崩溃,所以我们需要主动识别并屏蔽掉这类设备,在Android上判断设备是否支持armeabi-v7a也很简单:

public static boolean isARMv7Compatible() { try { if (SDK_INT >= LOLLIPOP) { for (String abi : Build.SUPPORTED_32_BIT_ABIS) { if (abi.equals("armeabi-v7a")) { return true; } } } else { if (CPU_ABI.equals("armeabi-v7a") || CPU_ABI.equals("arm64-v8a")) { return true; } } } catch (Throwable e) { L.wtf(e); } return false; } 

灰度和自动降级策略

Horn是一个美团内部的跨平台配置下发SDK,使用Horn可以很方便地指定灰度开关:

在条件配置页面定义一系列条件,然后在参数配置页面添加新的字段flutter即可:

因为在客户端做了ABI兜底策略,所以这里定义的ABI规则并没有启用。

Flutter目前仍然处于Beta阶段,灰度过程中难免发生崩溃现象,观察到崩溃后再针对机型或者设备ID来做降级虽然可以尽量降低影响,但是我们可以做到更迅速。外卖的Crash采集SDK同时也支持JNI Crash的收集,我们专门为Flutter注册了崩溃监听器,一旦采集到Flutter相关的JNI Crash就立即停止该设备的Flutter功能,启动Flutter之前会先判断FLUTTER_NATIVE_CRASH_FLAG文件是否存在,如果存在则表示该设备发生过Flutter相关的崩溃,很有可能是不兼容导致的问题,当前版本周期内在该设备上就不再使用Flutter功能。

除了崩溃以外,Flutter页面中的Dart代码也可能发生异常,例如服务器下发数据格式错误导致解析失败等等,Dart也提供了全局的异常捕获功能:

import 'package:wm_app/plugins/wm_metrics.dart'; void main() { runZoned(() => runApp(WaimaiApp()), onError: (Object obj, StackTrace stack) { uploadException("$obj\n$stack"); }); } 

这样我们就可以实现全方位的异常监控和完善的降级策略,最大程度减少灰度时可能对用户带来的影响。

分析崩溃堆栈和异常数据

Flutter的引擎部分全部使用C/C++实现,为了减少包大小,所有的SO库在发布时都会去除符号表信息。和其他的JNI崩溃堆栈一样,我们上报的堆栈信息中只能看到内存地址偏移量等信息:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' Revision: '0' Author: collect by 'libunwind' ABI: 'arm64-v8a' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<

单纯这些信息很难定位问题,所以我们需要使用NDK提供的ndk-stack来解析出具体的代码位置:

ndk-stack -sym PATH [-dump PATH] Symbolizes the stack trace from an Android native crash. -sym PATH sets the root directory for symbols -dump PATH sets the file containing the crash dump (default stdin) 

如果使用了定制过的引擎,必须使用engine/src/out/android-release下编译出的libflutter.so文件。一般情况下我们使用的是官方版本的引擎,可以在flutter_infra页面直接下载带有符号表的SO文件,根据打包时使用的Flutter工具版本下载对应的文件即可。比如0.4.4 beta版本:

$ flutter --version # version命令可以看到Engine对应的版本 06afdfe54e Flutter 0.4.4 • channel beta • https://github.com/flutter/flutter.git Framework • revision f9bb4289e9 (5 weeks ago) • 2018-05-11 21:44:54 -0700 Engine • revision 06afdfe54e Tools • Dart 2.0.0-dev.54.0.flutter-46ab040e58 $ cat flutter/bin/internal/engine.version # flutter安装目录下的engine.version文件也可以看到完整的版本信息 06afdfe54ebef9168a90ca00a6721c2d36e6aafa 06afdfe54ebef9168a90ca00a6721c2d36e6aafa 

拿到引擎版本号后在https://console.cloud.google.com/storage/browser/flutter_infra/flutter/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/ 看到该版本对应的所有构建产物,下载android-arm-release、android-arm64-release和android-x86目录下的symbols.zip,并存放到对应目录:

执行ndk-stack即可看到实际发生崩溃的代码和具体行数信息:

ndk-stack -sym flutter-production-syms/06afdfe54ebef9168a90ca00a6721c2d36e6aafa/armeabi-v7a -dump flutter_jni_crash.txt ********** Crash dump: ********** Build fingerprint: 'Rock/odin/odin:7.1.1/NMF26F/1527007828:user/dev-keys' pid: 28937, tid: 29314, name: 1.ui >>> com.sankuai.meituan.takeoutnew <<, void (blink::Paragraph::*)(double)>::Dispatch(void (blink::Paragraph::*)(double)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:150 Stack frame #06 pc 00067d5f /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine void tonic::DartCall(void (blink::Paragraph::*)(double), _Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../topaz/lib/tonic/dart_args.h:198 Stack frame #07 pc 003b1877 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::AutoScopeNativeCallWrapperNoStackCheck(_Dart_NativeArguments*, void (*)(_Dart_NativeArguments*)) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:198 Stack frame #08 pc 003b1db5 /data/app/com.sankuai.meituan.takeoutnew-1/lib/arm/libflutter.so: Routine dart::NativeEntry::LinkNativeCall(_Dart_NativeArguments*) at /b/build/slave/Linux_Engine/build/src/out/android_release/../../third_party/dart/runtime/vm/native_entry.cc:348 Stack frame #09 pc 0000241c /data/data/com.sankuai.meituan.takeoutnew/app_flutter/vm_snapshot_instr 

Dart异常则比较简单,默认情况下Dart代码在编译成机器码时并没有去除符号表信息,所以Dart的异常堆栈本身就可以标识真实发生异常的代码文件和行数信息:

FlutterException: type '_InternalLinkedHashMap' is not a subtype of type 'num' in type cast #0 _$CategoryGroupFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:29) #1 new CategoryGroup.fromJson (package:wm_app/all_category/model/category_model.dart:51) #2 _$CategoryListDataFromJson. (package:wm_app/lib/all_category/model/category_model.g.dart:5) #3 MappedListIterable.elementAt (dart:_internal/iterable.dart:414) #4 ListIterable.toList (dart:_internal/iterable.dart:219) #5 _$CategoryListDataFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:6) #6 new CategoryListData.fromJson (package:wm_app/all_category/model/category_model.dart:19) #7 _$AllCategoryResponseFromJson (package:wm_app/lib/all_category/model/category_model.g.dart:19) #8 new AllCategoryResponse.fromJson (package:wm_app/all_category/model/category_model.dart:29) #9 AllCategoryPage.build. (package:wm_app/all_category/category_page.dart:46)  #10 _WaimaiLoadingState.build (package:wm_app/all_category/widgets/progressive_loading_page.dart:51) #11 StatefulElement.build (package:flutter/src/widgets/framework.dart:3730) #12 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3642) #13 Element.rebuild (package:flutter/src/widgets/framework.dart:3495) #14 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2242) #15 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding&WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:626) #16 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:208) #17 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:990) #18 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:930) #19 _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:842) #20 _rootRun (dart:async/zone.dart:1126) #21 _CustomZone.run (dart:async/zone.dart:1023) #22 _CustomZone.runGuarded (dart:async/zone.dart:925) #23 _invoke (dart:ui/hooks.dart:122) #24 _drawFrame (dart:ui/hooks.dart:109) 

原文作者:美团技术团队
原文链接:https://zhuanlan.zhihu.com/p/41731950
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680


推荐阅读
  • Android中将独立SO库封装进JAR包并实现SO库的加载与调用
    在Android开发中,将独立的SO库封装进JAR包并实现其加载与调用是一个常见的需求。本文详细介绍了如何将SO库嵌入到JAR包中,并确保在外部应用调用该JAR包时能够正确加载和使用这些SO库。通过这种方式,开发者可以更方便地管理和分发包含原生代码的库文件,提高开发效率和代码复用性。文章还探讨了常见的问题及其解决方案,帮助开发者避免在实际应用中遇到的坑。 ... [详细]
  • 实验九:使用SharedPreferences存储简单数据
    本实验旨在帮助学生理解和掌握使用SharedPreferences存储和读取简单数据的方法,包括程序参数和用户选项。 ... [详细]
  • C#中使用Dotfuscator Pro进行代码混淆
    由于Visual Studio自带的混淆工具功能有限,本文将介绍如何使用Dotfuscator Pro进行更高级的代码混淆。包括设置字符串加密、添加可执行文件和动态链接库、调整混淆选项等步骤。 ... [详细]
  • 在本次学习中,主要通过外部中断来控制LED的亮灭。首先,先查看相关电路图。由图可知,当CC2530端口1的0号引脚输出低电平时࿰ ... [详细]
  • 本文介绍了 Android 开发中常用的滚动视图组件 ScrollView 和 HorizontalScrollView 的基本用法和注意事项,帮助开发者更好地处理屏幕内容超出显示范围的情况。 ... [详细]
  • This feature automatically validates new regions using the AWS SDK, ensuring compatibility and accuracy. ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本文介绍了一种自定义的Android圆形进度条视图,支持在进度条上显示数字,并在圆心位置展示文字内容。通过自定义绘图和组件组合的方式实现,详细展示了自定义View的开发流程和关键技术点。示例代码和效果展示将在文章末尾提供。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 在使用 Qt 进行 YUV420 图像渲染时,由于 Qt 本身不支持直接绘制 YUV 数据,因此需要借助 QOpenGLWidget 和 OpenGL 技术来实现。通过继承 QOpenGLWidget 类并重写其绘图方法,可以利用 GPU 的高效渲染能力,实现高质量的 YUV420 图像显示。此外,这种方法还能显著提高图像处理的性能和流畅性。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • 在Python中,是否可以通过使用Tkinter或ttk库创建一个具有自动换行功能的多行标签,并使其宽度能够随着父容器的变化而动态调整?例如,在调整NotePad窗口宽度时,实现类似记事本的自动换行效果。这种功能在设计需要显示长文本的对话框时非常有用,确保文本内容能够完整且美观地展示。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
  • 技术分享:深入解析GestureDetector手势识别机制
    技术分享:深入解析GestureDetector手势识别机制 ... [详细]
  • 探索聚类分析中的K-Means与DBSCAN算法及其应用
    聚类分析是一种用于解决样本或特征分类问题的统计分析方法,也是数据挖掘领域的重要算法之一。本文主要探讨了K-Means和DBSCAN两种聚类算法的原理及其应用场景。K-Means算法通过迭代优化簇中心来实现数据点的划分,适用于球形分布的数据集;而DBSCAN算法则基于密度进行聚类,能够有效识别任意形状的簇,并且对噪声数据具有较好的鲁棒性。通过对这两种算法的对比分析,本文旨在为实际应用中选择合适的聚类方法提供参考。 ... [详细]
author-avatar
leedaning
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有