前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Q音直播Flutter包裁剪方案(iOS)

Q音直播Flutter包裁剪方案(iOS)

作者头像
QQ音乐技术团队
发布2021-02-05 15:40:16
1.9K1
发布2021-02-05 15:40:16
举报

1、背景

Flutter作为一款优秀的跨平台方案,我们Q音团队一致保持高度关注,团队内部也一直在努力促进Flutter的应用框架建设。在Q音直播接入Flutter的过程中,需要解决的首要问题便是”Flutter包体积变大”。本文将一步步剖析Flutter的包体积问题,带领大家探寻每一个可能的包体积优化点,结合实际项目和引擎源码,最终给出详细的包体积优化实现方案。欢迎大家相互交流Flutter相关技术。

1.1 Flutter混合开发模式

一般的如果我们想在现有原生App中加入Flutter,需要通过以下两种方式对Flutter进行引入:

  • 将原生工程作为 Flutter 工程的子工程,由 Flutter 统一管理。这种模式,就是统一管理模式,即讲Flutter作为子工程集成到项目中。
  • 将 Flutter 工程作为原生工程共用的子模块,维持原有的原生工程管理方式不变。这种模式,就是三端分离模式,即Flutter单独作为一个端进行开发。

统一管理模式搭建简单,但是缺点明显,不仅三端(Android、iOS、Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,导致开发效率降低。所以这里使用三端代码分离的模式来进行依赖治理,实现了 Flutter 工程的轻量级接入。即 Android 侧使用 aar集成Flutter产物、iOS 使用 pod集成Flutter产物。

1.2 Flutter瘦身需求

当App引入Flutter带来一个明显问题,包体积增大。对于三端分离模式,包体积增量在Android上即为Flutter的aar产物,在iOS上表现为Flutter的framwork产物。因此,要解决包体积问题,需要对aar和framework的体积进行优化。

1.3 本文内容涉及的开发环境

  • Flutter 1.17.1 • channel stable
  • Mac OS X 10.15.4
  • Xcode - develop for iOS and macOS (Xcode 11.4)
  • Python 2.7.16

2、iOS framework产物分析

我们在实际工程中使用的是产物集成方式,Flutter代码会被编译打包成一个framework,即Flutter业务代码将会以framework的形式带入iOS宿主App。那么如何去对这个framework进行体积优化呢?下面我们首先对framework的内容进行详细分析。

2.1 framework结构

以Release模式下Futter产物为例,使用tree命令查看Release目录结构,我们可以看到iOS产物为两个framework,其中App.framework是Dart业务代码产物,Flutter.framework是从Flutter SDK中拷贝过来的引擎产物。下图中,我给体积占比大的文件添加了说明,其它Headers、和plist文件大小可以忽略不计。

代码语言:javascript
复制
Release
    ├── App.framework
    │   ├── App            //AOT Snapshot数据,由我们的Dart业务代码编译而成,Mach-O格式的动态链接库
    │   ├── Info.plist     
    │   └── flutter_assets //资源文件存放
    └── Flutter.framework
        ├── Flutter        //Flutter引擎,Mach-O格式的动态链接库
        ├── Headers
        ├── Info.plist
        ├── Modules
        ├── _CodeSignature
        └── icudtl.dat     //国际化支持相关文件

2.2 Framework体积分析(Release模式下)

我们将Release目录下的大文件以表格形式列出,这些文件即是我们去做体积优化的方向。

名称

大小

说明

App

7.3M

Dart业务代码AOT编译产物

flutter_assets

2M

图片、字体等资源文件

Flutter

11M

引擎

icudtl.dat

884k

国际化支持相关文件

其他第三方插件

800K

Flutter_boost等第三方插件

3、iOS减包思路

上一节我们分析了framework产物的组成占比,并且列出了体积最大的4个文件:Appflutter_assetsFluttericudtl.dat。下面我们继续对这4个文件进行深入分析,看看是否有优化空间。

优化思路分为3个方向

  • 缩。即自我数据压缩,这种方法能够减少framework的体积,但是对最终app打包出来的体积影响较小,因为打包也是进行了数据压缩。
  • 删。删除无用部分,或者不需要用到的部分。
  • 挪。如果不能删除,考虑是否能分离出来,通过下载的方式动态去加载分离的部分。

3.1 App.framework/flutter_assets

flutter_assets目录存放的资源文件,如果不想flutter_assets带入App,我们可以将其移出,在运行需要时动态下载。移除方法具体有以下两个方法。

  • 修改flutter_tools编译打包脚本。在生成framework过程中,就将flutter_asserts移除保存到别处,即生成的framework天生与flutter_asserts就是分开的。优点是得到即可用,隐藏中间过程。
  • 生成framework后进行二次处理。在生成framework后,通过脚本将framework中的flutter_asserts移除。优点是不需要修改打包工具flutter_tools源码,缺点是增加了脚本对framework的处理,拉长了工作流程。

当前使用的是第二种方法,直接对产物进行二次处理,只为一个flutter_assets修改打包源码有点得不偿失。

移除flutter_assets后对引擎启动是否有影响?查看源码发现flutter_assets在FlutterDartProject.mm的DefaultSettingsForProcess函数中被使用,初始化过程中会在app目录检查是否有flutter_assets,并且将路径保存在Settings的assets_path变量中,供引擎后续使用。因此我们得出结论:flutter_assets是放在Framework内部,还是动态下载下来的,对程序运行没有影响,只要将flutter_assets的正确位置的告知引擎即可。

代码语言:javascript
复制
//FlutterDartProject.mm// Checks to see if the flutter assets directory is already present.
  if (settings.assets_path.size() == 0) {
    NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];
    NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""];    if (assetsPath.length == 0) {
      assetsPath = [mainBundle pathForResource:assetsName ofType:@""];//主目录检查assetsPath
    }    if (assetsPath.length == 0) {
      NSLog(@"Failed to find assets path for \"%@\"", assetsName);
    } else {
      settings.assets_path = assetsPath.UTF8String;      // Check if there is an application kernel snapshot in the assets directory we could
      // potentially use.  Looking for the snapshot makes sense only if we have a VM that can use
      // it.
      if (!flutter::DartVM::IsRunningPrecompiledCode()) {
        NSURL* applicationKernelSnapshotURL =
            [NSURL URLWithString:@(kApplicationKernelSnapshotFileName)
                   relativeToURL:[NSURL fileURLWithPath:assetsPath]];        if ([[NSFileManager defaultManager] fileExistsAtPath:applicationKernelSnapshotURL.path]) {
          settings.application_kernel_asset = applicationKernelSnapshotURL.path.UTF8String;
        } else {
          NSLog(@"Failed to find snapshot: %@", applicationKernelSnapshotURL.path);
        }
      }
    }
  }
代码语言:javascript
复制
//这里列出settings.h中的相关Flutter配置代码struct Settings {
  Settings();

  Settings(const Settings& other);

  ~Settings();  //add by allentywang.  dart data path.
  std::string ios_vm_snapshot_data_path;  //自定义 vm data 路径
  std::string ios_isolate_snapshot_data_path; //自定义 isolate data 路径
  //end

  ...

  std::string icu_data_path; //这个是Flutter.framework中icudtl.dat的路径
  // Assets settings
  std::string assets_path; //这个是App.framework中flutter.assets的路径};

3.2 Flutter.framework/icudtl.dat

Flutter.framework中的icudtl.dat保存了引擎的国际化支持信息。通过查看源码发现,icudtl.dat在引擎初始化时被加载,依赖的只有Setting中的配置路径icu_data_path。因此这个也可以挪走。icudtl.dat大小固定为800多k,还是比较可观的。同样和flutter_assets一样,如果移走了icudtl.dat,我们需要在引擎初始化时指定外部的icudtl.dat路径。

代码语言:javascript
复制
  //settings.h中

  // The icu_initialization_required setting does not have a corresponding
  // switch because it is intended to be decided during build time, not runtime.
  // Some companies apply source modification here because their build system
  // brings its own ICU data files.
  bool icu_initialization_required = true;
  std::string icu_data_path; //icu路径
  MappingCallback icu_mapper;
代码语言:javascript
复制
//观察shell.cc源码,得出结论,可以将icudtl移除,只需要在初始化时重新指定settings中icu_data_path的路径即可if (settings.icu_initialization_required) {  if (settings.icu_data_path.size() != 0) {
    fml::icu::InitializeICU(settings.icu_data_path);//我们可以修改settings.icu_data_path路径,达到加载保存在外部的icu目的
  } else if (settings.icu_mapper) {
    fml::icu::InitializeICUFromMapping(settings.icu_mapper());
  } else {
    FML_DLOG(WARNING) << "Skipping ICU initialization in the shell.";
  }
}

3.3 App.Framework/App

我们App.Framework产物下有两个比较大的文件,一个是前面提到的flutter_assets,另外一个就是Appflutter_assets的优化前面已经讲过了,可以通过移除,然后动态下载的方式进行优化。那么App这个文件能否采用同样的方式呢?下面我们进行逐步分析。

3.3.1 App是什么?

App是dart代码编译出来的可执行文件,App体积还是比较大的,我们先对App内容进行分析,下面是使用nm命令显示的App内容

代码语言:javascript
复制
nm App
...
000000000038c440 t Precompiled_int_init__int64OverflowLimits_0150898_11508
000000000038a2b0 t Precompiled_int_parse_11499
000000000038a178 t Precompiled_int_tryParse_11498
0000000000392ae8 t Precompiled_num_parse_11570
0000000000392a74 t Precompiled_num_tryParse_11569
0000000000018c54 t Precompiled_pragma_get_name_131
000000000065c008 b _kDartIsolateSnapshotBss
00000000003a1270 S _kDartIsolateSnapshotData
0000000000009000 T _kDartIsolateSnapshotInstructions
000000000065c000 b _kDartVmSnapshotBss
0000000000399400 S _kDartVmSnapshotData
0000000000004000 T _kDartVmSnapshotInstructions
                 U dyld_stub_binder

注意这里我省略了相当多的符号,如果只想看Dart VM Data相关产物,可以使用xcrun strip命令进行过滤。自己研究的过程中发现Flutter1.9的版本没有这些Precompiled符号内容,原因是在打包脚本$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh中做了如下处理,而高版本的Flutter去掉了这些处理,保留了符号信息。

代码语言:javascript
复制
# 生成 dSYM 文件
RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App"

StreamOutput " ├─Stripping debug symbols..."
# 剥离调试符号表
RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App"
StreamOutput "done"

过滤掉符号信息后,App中只剩下下面4个东西

  • kDartIsolateSnapshotData
  • kDarVmSnapshotData
  • kDartIsolateSnapshotInstructions
  • kDartVmSnapshotInstructions

这4个东西是什么?探究过程我就省略不细说了,这里直接说结论。kDartIsolateSnapshotData、kDarVmSnapshotData为可执行文件App的数据段,kDartIsolateSnapshotInstructions、kDartVmSnapshotInstructions为代码段,由于iOS系统的限制,整个App可执行文件不可以动态下发。但是kDartIsolateSnapshotData、kDartVmSnapshotData为数据段,它们在加载时不存在限制,可以动态加载。因此得出结论kDartIsolateSnapshotData和kDarVmSnapshotData可以挪到App外部,进行动态加载。

3.3.2 App内的kDartIsolateSnapshotData、kDarVmSnapshotData是如何生成的?

前面我们讲到kDartIsolateSnapshotData、kDarVmSnapshotData可以从App中移除,改为动态加载。那么它们是怎么被生成到App中的?我们又如何把它们从App中分离呢?

  • 首先介绍一下Flutter虚拟机的运行模式。以iOS为例,Debug模式下Flutter的Dart虚拟机是JIT运行模式,JIT直接运行源码或者app.dill ,这也是Flutter热重载的原理。而在Release下,Flutter的Dart虚拟机是AOT运行模式,直接运行编译期编译好的机器码App。我们只能对Release模式下的App做文章,因为Debug模式下App包含很少的东西,里面没有可运行代码(这也是Debug的App.framework/App非常小的原因,使用nm查看App,发现里面什么都没有)。
  • AOT产物是如何生成的?查阅了Flutter源码和相关资料,我们发现Dart代码会使用gen_snapshot工具来编译成.S文件,然后通过xcrun工具来进行汇编和链接最终生成App。整个编译过程关键分为两步,一个是gen_sanpshot 编译,另外一个是xcrun编译。其中gen_snapshot由Flutter engine提供,属于DartVm代码部分,我们完全可以定制gen_sanpshot来改变App组成,进而达到减包的目的。

因此想要移除kDartIsolateSnapshotData、kDarVmSnapshotData,我们必须从gen_sanpshot入手

3.3.3 Dart源码的编译与加载

数据段编译过程:这里借用网上Dart源码编译成App.framework的流程图

数据段运行时加载过程:App运行前,Dart 虚拟机需要加载保存在App中的数据和代码,为了得到可供虚拟机运行的DartVMData,引擎初始化时按照下面步骤依次调用相关代码来完成DartVMData的创建。下面我从引擎源码上追踪了数据段的完整加载过程。

  • 第一步,DartVMData::Create  根据Setting中的配置,调用VMSnapshotFromSettings、IsolateSnapshotFromSettings std::shared_ptr<const DartVMData> DartVMData::Create( Settings settings, fml::RefPtr<DartSnapshot> vm_snapshot, fml::RefPtr<DartSnapshot> isolate_snapshot) { if (!vm_snapshot || !vm_snapshot->IsValid()) { // Caller did not provide a valid VM snapshot. Attempt to infer one // from the settings. vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings);//创建VM Snapshot if (!vm_snapshot) { FML_LOG(ERROR) << "VM snapshot invalid and could not be inferred from settings."; return {}; } } if (!isolate_snapshot || !isolate_snapshot->IsValid()) { // Caller did not provide a valid isolate snapshot. Attempt to infer one // from the settings. isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings);//创建Isolate Snapshot if (!isolate_snapshot) { FML_LOG(ERROR) << "Isolate snapshot invalid and could not be inferred " "from settings."; return {}; } } return std::shared_ptr<const DartVMData>(new DartVMData( std::move(settings), // std::move(vm_snapshot), // std::move(isolate_snapshot) // ));//生成DartVMData}
  • 第二步,VMSnapshotFromSettings、IsolateSnapshotFromSettings 这里的作用是完成数据段和代码段的重建 fml::RefPtr<DartSnapshot> DartSnapshot::VMSnapshotFromSettings( const Settings& settings) { TRACE_EVENT0("flutter", "DartSnapshot::VMSnapshotFromSettings"); auto snapshot = fml::MakeRefCounted<DartSnapshot>(ResolveVMData(settings), //数据段重建 ResolveVMInstructions(settings) //代码段重建 ); if (snapshot->IsValid()) { return snapshot; } return nullptr; }
  • 第三步,ResolveVMData 调用SearchMapping创建符号Mappping static std::shared_ptr<const fml::Mapping> ResolveVMData( const Settings& settings) {#if DART_SNAPSHOT_STATIC_LINK return std::make_unique<fml::NonOwnedMapping>(kDartVmSnapshotData, 0);#else // DART_SNAPSHOT_STATIC_LINK return SearchMapping( settings.vm_snapshot_data, // embedder_mapping_callback settings.vm_snapshot_data_path, // file_path settings.application_library_path, // native_library_path DartSnapshot::kVMDataSymbol, // native_library_symbol_name false // is_executable );#endif // DART_SNAPSHOT_STATIC_LINK}
  • 第四步,SearchMapping 调用fml::NativeLibrary::Create读取文件 static std::shared_ptr<const fml::Mapping> SearchMapping( MappingCallback embedder_mapping_callback, const std::string& file_path, const std::vector<std::string>& native_library_path, const char* native_library_symbol_name, bool is_executable) { // Ask the embedder. There is no fallback as we expect the embedders (via // their embedding APIs) to just specify the mappings directly. if (embedder_mapping_callback) { return embedder_mapping_callback(); } // Attempt to open file at path specified. if (file_path.size() > 0) { if (auto file_mapping = GetFileMapping(file_path, is_executable)) { return file_mapping; } } // Look in application specified native library if specified. for (const std::string& path : native_library_path) { auto native_library = fml::NativeLibrary::Create(path.c_str());//通过NativeLibrary加载文件 auto symbol_mapping = std::make_unique<const fml::SymbolMapping>( native_library, native_library_symbol_name); if (symbol_mapping->GetMapping() != nullptr) { return symbol_mapping; } } // Look inside the currently loaded process. { auto loaded_process = fml::NativeLibrary::CreateForCurrentProcess(); auto symbol_mapping = std::make_unique<const fml::SymbolMapping>( loaded_process, native_library_symbol_name); if (symbol_mapping->GetMapping() != nullptr) { return symbol_mapping; } } return nullptr; }
  • 第五步,NativeLibrary  最终调用dlopen去获取符号内容 NativeLibrary::NativeLibrary(const char* path) { ::dlerror(); handle_ = ::dlopen(path, RTLD_NOW);//实际上调用dlopen if (handle_ == nullptr) { FML_DLOG(ERROR) << "Could not open library '" << path << "' due to error '" << ::dlerror() << "'."; } }

最终结论:我们可以移除kDartIsolateSnapshotData、kDarVmSnapshotData放到App外部,然后通过修改ResolveVMData,在引擎初始化时,从外部文件中重建Dart虚拟机所需要的数据结构,达到缩减App体积的目的。

3.4 FLutter.framework/Flutter

Flutter.framework/Flutter为引擎产物,其大小是固定的,但是初始占比比较大。这部分能优化的空间很小,主要是通过裁剪引擎不需要的功能,减少体积。编译引擎时可以选择性编译skia和boringssl,收益大概只有几百K。

除此之外可以对Flutter的符号进行分离。

代码语言:javascript
复制
#将flutter的符号,存为.dSYM文件
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM  $frameworkpath/Release/Flutter.framework/flutter
#然后strip掉符号
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter

4、实践

前面都是在分析如何去iOS产物进行优化,接下来我们真正开始动手减包之路。

4.1 引擎编译配置

这部分涉及到的引擎编译部分知识,可自行参考相关资料,这里不再进行细节描述。以下列出相关编译脚本,和需要注意的事项。

在引擎源码目录下创建以下脚本

  • debug模式
代码语言:javascript
复制
  #!/bin/bash
  #ios_debug.sh
  #需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”
  #引擎编译很耗内存,电脑性能吃紧的把10调低一点

  #构建iOS debug 引擎
  Echo “当前目录在:”
  pwd

  # 构建模拟器
  Echo "构建iOS debug 模拟器"
  ./flutter/tools/gn --unoptimized --runtime-mode debug --simulator
  ./flutter/tools/gn --unoptimized --ios --runtime-mode debug --simulator
  ninja -C out/host_debug_sim_unopt -j 10
  ninja -C out/ios_debug_sim_unopt -j 10
  Echo  ""

  # 构建armv7
  Echo "构建iOS debug arm"
  ./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm
  ./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
  ninja -C out/host_debug_unopt_arm -j 10
  ninja -C out/ios_debug_unopt_arm -j 10
  Echo  ""


  # 构建arm64
  Echo "构建iOS debug arm64"
  ./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm64
  ./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64
  ninja -C out/host_debug_unopt -j 10
  ninja -C out/ios_debug_unopt -j 10
  Echo  ""


  #归档
  Echo  "开始归档"
  rm -rf tmp/*
  flutter_lipo=./arch_file
  buidmode=ios


  cp -rf out/ios_debug_unopt/Flutter.framework tmp/

  lipo -create -output tmp/Flutter.framework/Flutter \
  out/ios_debug_sim_unopt/Flutter.framework/Flutter \
  out/ios_debug_unopt/Flutter.framework/Flutter \
  out/ios_debug_unopt_arm/Flutter.framework/Flutter

  #cd tmp
  #zip -r Flutter.framework.zip Flutter.framework
  #cd ..
  #mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
  #cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  #copy gen_snapshot
  cp -f out/ios_debug_unopt/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
  cp -f out/ios_debug_unopt_arm/clang_x64/gen_snapshot  "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7

  Echo  "归档完毕"
  • Profile模式
代码语言:javascript
复制
  #!/bin/bash
  #ios_profile.sh
  #需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”


  #构建iOS debug 引擎

  Echo “当前目录在:”
  pwd


  # 构建arm
  Echo "构建iOS profile arm"
  #./flutter/tools/gn --runtime-mode profile --ios-cpu arm
  #ninja -C out/host_profile_arm -j 6

  ./flutter/tools/gn --ios --runtime-mode profile --ios-cpu arm
  ninja -C out/ios_profile_arm -j 6
  echo ""

  # 构建arm64
  Echo "构建iOS profile arm64"
  #./flutter/tools/gn --runtime-mode profile --ios-cpu arm64
  #ninja -C out/host_profile -j 6

  ./flutter/tools/gn --ios --runtime-mode profile --ios-cpu arm64
  ninja -C out/ios_profile -j 6
  echo ""

  Echo  "开始归档"
  rm -rf tmp/*
  flutter_lipo=./arch_file
  buidmode=ios-profile

  cp -rf out/ios_profile/Flutter.framework tmp/
  lipo -create -output tmp/Flutter.framework/Flutter \
  out/ios_profile/Flutter.framework/Flutter \
  out/ios_profile_arm/Flutter.framework/Flutter \
  "${flutter_lipo}"/flutter-profile-x86_64

  #cd tmp
  #zip -r Flutter.framework.zip Flutter.framework
  #cd ..
  #mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
  #cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  #copy gen_snapshot
  cp -f out/ios_profile/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
  cp -f out/ios_profile_arm/clang_x64/gen_snapshot  "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7
  • Release模式
代码语言:javascript
复制
  #!/bin/bash
  #ios_release.sh
  #需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”
  #构建iOS Release 引擎
  Echo “当前目录在:”
  pwd

  # 构建arm
  Echo "构建iOS release armv7"
  #./flutter/tools/gn --runtime-mode release --ios-cpu arm
  #ninja -C out/host_release_arm -j 6
  ./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm
  ninja -C out/ios_release_arm -j 6
  echo ""

  # 构建arm64
  Echo "构建iOS release arm64"
  #./flutter/tools/gn --runtime-mode release --ios-cpu arm64
  #ninja -C out/host_release -j 6
  ./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm64
  ninja -C out/ios_release -j 6
  echo ""

  Echo "合并归档"
  rm -rf tmp/*
  flutter_lipo=./arch_file
  buidmode=ios-release

  cp -rf out/ios_release/Flutter.framework tmp/

  lipo -create -output tmp/Flutter.framework/Flutter \
  out/ios_release/Flutter.framework/Flutter \
  out/ios_release_arm/Flutter.framework/Flutter \
  "${flutter_lipo}"/flutter-release-x86_64

  #cd tmp
  #zip -r Flutter.framework.zip Flutter.framework
  #cd ..
  #mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
  #cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/

  #copy gen_snapshot
  cp -f out/ios_release/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
  cp -f out/ios_release_arm/clang_x64/gen_snapshot  "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7
  Echo  "归档完毕"

其他详细编译参数,使用./flutter/tools/gn —help命令查看。

另外flutter引擎默认不支持bitcode,如果需要支持,需要在编译脚本后添加—bitcode

代码语言:javascript
复制
#例如,添加--bitcode,最终的引擎产物将包含bitcode
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm --bitcode

4.2 xcode配置引擎调试环境

  • 以Debug模式为例,找到引擎源码目录src/out/ios_debug_unopt下的products.xcodeproj。该工程为引擎源码对应的iOS工程
  • 打开flutter工程下的./iOS/Runner.xcworkspace。该目录由Flutter SDK 中的flutter_tools自动生成,保存了运行flutter所需要的iOS宿主模板Runner工程
  • 把products.xcodeproj拖入到Runner项目中,并在Generated.xcconfig中添加相关环境变量
代码语言:javascript
复制
#以使用ios_debug_unopt引擎为例,添加如下代码,即可调试引擎
FLUTTER_FRAMEWORK_DIR=/Users/wangtengyu/allenwork/engine/src/out/ios_debug_unopt
LOCAL_ENGINE=ios_debug_unopt
FLUTTER_ENGINE=/Users/wangtengyu/allenwork/engine/src

4.3 减包引擎源码修改

4.3.1 添加自定义配置,Settings

Settings是一个重要的配置结构体,位于src/flutter/common/settings.h中,这个公共头文件被大量使用。我们可以在settings.h添加我们自己的代码,来实现想要的功能。

为了自定义DartVMData加载路径,我们在settings结构体中添加了2个string成员用来保存vm和isolate数据文件路径。

代码语言:javascript
复制
struct Settings {
  Settings();

  Settings(const Settings& other);

  ~Settings();  //add by allentywang.  dart data path.
  std::string ios_vm_snapshot_data_path;  // vm data path
  std::string ios_isolate_snapshot_data_path; // isolate data path
  //end
  ...
  ...
}
4.3.2 修改Dart编译工具,gen_snapshot

Dart业务代码使用gen_snapshot工具编译到App中,在程序运行时通过引擎内部的虚拟机加载App中的Dart编译代码。下面从写入和读取这两个方面介绍,如何分离Dart编译产物的数据段。

重定向App数据段的写入
代码语言:javascript
复制
//文件:image_snapshot.cc//添加头文件#include "bin/file.h"#include <iostream>//添加写入函数//add by allen. 把stream写到本地文件void WriteTextToLocalFile(WriteStream* clustered_stream, bool vm){#if defined(TARGET_OS_MACOS_IOS)  //add by allentywang
    auto OpenFile = [](const char* filename){
        Syslog::Print("open file : %s\n", filename);
        bin::File* file = bin::File::Open(NULL, filename, bin::File::kWriteTruncate);        if (file == NULL) {
          Syslog::PrintErr("Error: Unable to write file: %s\n", filename);
          Dart_ExitScope();
          Dart_ShutdownIsolate();          exit(255);
        }        return file;
    };    auto StreamingWriteCallback = [](void* callback_data,                                     const uint8_t* buffer,
                                     intptr_t size) {
        bin::File* file = reinterpret_cast < bin::File* >(callback_data);        if (!file->WriteFully(buffer, size)) {
          Syslog::PrintErr("Error: Unable to write snapshot file\n");
          Dart_ExitScope();
          Dart_ShutdownIsolate();          exit(255);
    }

    };#if defined(TARGET_ARCH_ARM64)
    printf("this is arm64\n");
    bin::File *file = OpenFile(vm ? "./SnapshotData/arm64/VmSnapshotData.S" : "./SnapshotData/arm64/IsolateSnapshotData.S");#else//#if defined(TARGET_ARCH_ARM)
    printf("this is armv7\n");
    bin::File *file = OpenFile(vm ? "./SnapshotData/armv7/VmSnapshotData.S" : "./SnapshotData/armv7/IsolateSnapshotData.S");#endif //end of TARGET_ARCH_ARM64
  bin::RefCntReleaseScope rs(file);
  StreamingWriteStream stream = StreamingWriteStream(512 * KB, StreamingWriteCallback, file);

  uword buffer = reinterpret_cast<uword>(clustered_stream->buffer());
  intptr_t length = clustered_stream->bytes_written();
  uword start = buffer;
  uword end = buffer + length;  auto const end_of_words =
      Utils::RoundDown(end, sizeof(compiler::target::uword));  for (auto cursor = reinterpret_cast< compiler::target::uword* >(start);
       cursor < reinterpret_cast< compiler::target::uword* >(end_of_words);
       cursor++) {    #if defined(TARGET_ARCH_IS_64_BIT)
    stream.Print(".quad 0x%0.16" Px "\n", *cursor);    #else
    stream.Print(".long 0x%0.8" Px "\n", *cursor);    #endif
  }  if (end != end_of_words) {    auto start_of_rest = reinterpret_cast< const uint8_t* >(end_of_words);
    stream.Print(".byte ");    for (auto cursor = start_of_rest;
         cursor < reinterpret_cast< const uint8_t* >(end); cursor++) {      if (cursor != start_of_rest) stream.Print(", ");
      stream.Print("0x%0.2" Px "", *cursor);
    }
    stream.Print("\n");
  }#endif //end of TARGET_OS_MACOS_IOS}

...//修改数据段写入
 void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
  ...#if defined(TARGET_OS_MACOS_IOS)
  WriteTextToLocalFile(clustered_stream, vm);//ios下写到外部文件中,WriteTextToLocalFile为上面我们自己的写入函数#else
  const char* data_symbol =
      vm ? "_kDartVmSnapshotData" : "_kDartIsolateSnapshotData";
  assembly_stream_.Print(".globl %s\n", data_symbol);
  Align(kMaxObjectAlignment);
  assembly_stream_.Print("%s:\n", data_symbol);
  uword buffer = reinterpret_cast<uword>(clustered_stream->buffer());
  intptr_t length = clustered_stream->bytes_written();
  WriteByteSequence(buffer, buffer + length);
  Syslog::Print("write file : %s\n", data_symbol);#endif //end of TARGET_OS_MACOS_IOS}

注意:要根据不同架构,分别保存VmSnapshotData.S和IsolateSnapshotData.S。架构区分宏定义为TARGET_ARCH_ARM

上面我们分离出了VmSnapshotData.S和IsolateSnapshotData.S,接下来需要将其编译成机器代码IsolateData.dat和VMData.dat

代码语言:javascript
复制
#以armv7的VmSnapshotData.S和IsolateSnapshotData.S为例,arm64类同
armv7=./SnapshotData/armv7
echo "编译数据段"
xcrun cc -arch armv7  -c $armv7/IsolateSnapshotData.S -o $armv7/HeadIsolateData.dat
xcrun cc -arch armv7  -c $armv7/VmSnapshotData.S -o $armv7/HeadVMData.dat
# 去掉多余头部
tail -c +313 $armv7/HeadIsolateData.dat > $armv7/IsolateData.dat
tail -c +313 $armv7/HeadVMData.dat > $armv7/VMData.dat

IsolateData.dat和VMData.dat为我们最终想要分离的数据段

自定义App数据段的重建

根据前面3.3.3节的分析,重建数据段,我们可以从ResolveVMData入手,下面是修改的代码。

代码语言:javascript
复制
//文件:dart_snapshot.cc//新增自定义重建函数//add by allentywang. create data Mappingstd::shared_ptr<const fml::Mapping> SetupMapping(const std::string &path) {  // Check if the path exists and it readable directly.

  auto fd = fml::OpenFile(path.c_str(), false, fml::FilePermission::kRead);  // Check the path relative to the current executable.
  if (!fd.is_valid()) {    auto directory = fml::paths::GetExecutableDirectoryPath();    if (!directory.first) {      return nullptr;
    }

    std::string path_relative_to_executable = fml::paths::JoinPaths({directory.second, path});

    fd = fml::OpenFile(path_relative_to_executable.c_str(),                       false,
                       fml::FilePermission::kRead);
  }  if (!fd.is_valid()) {    return nullptr;
  }

  std::initializer_list<fml::FileMapping::Protection> protection = {fml::FileMapping::Protection::kRead};  auto file_mapping = std::make_unique<fml::FileMapping>(fd, std::move(protection));  if (file_mapping->GetSize() != 0) {    return file_mapping;
  }  return nullptr;
}//end//修改原来的ResolveVMData和ResolveIsolateData函数。static std::shared_ptr<const fml::Mapping> ResolveVMData(    const Settings& settings) {#if DART_SNAPSHOT_STATIC_LINK
  return std::make_unique<fml::NonOwnedMapping>(kDartVmSnapshotData, 0);#else   // DART_SNAPSHOT_STATIC_LINK#if OS_IOS //add by allentywang
    if (settings.ios_vm_snapshot_data_path.empty()) {//ios
        printf("ResolveVMData from local\n");          return SearchMapping(
              settings.vm_snapshot_data,          // embedder_mapping_callback
              settings.vm_snapshot_data_path,     // file_path
              settings.application_library_path,  // native_library_path
              DartSnapshot::kVMDataSymbol,        // native_library_symbol_name
              false                               // is_executable
              );
    } else {        printf("ResolveVMData from settings.ios_vm_snapshot_data_path\n");      return SetupMapping(settings.ios_vm_snapshot_data_path);
    }#else
    return SearchMapping(
        settings.vm_snapshot_data,          // embedder_mapping_callback
        settings.vm_snapshot_data_path,     // file_path
        settings.application_library_path,  // native_library_path
        DartSnapshot::kVMDataSymbol,        // native_library_symbol_name
        false                               // is_executable
    );#endif#endif  // DART_SNAPSHOT_STATIC_LINK}static std::shared_ptr<const fml::Mapping> ResolveIsolateData(    const Settings& settings) {#if DART_SNAPSHOT_STATIC_LINK
  return std::make_unique<fml::NonOwnedMapping>(kDartIsolateSnapshotData, 0);#else   // DART_SNAPSHOT_STATIC_LINK#if OS_IOSif (settings.ios_isolate_snapshot_data_path.empty()) {    printf("ResolveVMData from local\n");  return SearchMapping(
    settings.isolate_snapshot_data,       // embedder_mapping_callback
    settings.isolate_snapshot_data_path,  // file_path
    settings.application_library_path,    // native_library_path
    DartSnapshot::kIsolateDataSymbol,     // native_library_symbol_name
    false                                 // is_executable
  );
} else {    printf("ResolveVMData from settings.ios_isolate_snapshot_data_path\n");  return SetupMapping(settings.ios_isolate_snapshot_data_path);
}#elsereturn SearchMapping(
    settings.isolate_snapshot_data,       // embedder_mapping_callback
    settings.isolate_snapshot_data_path,  // file_path
    settings.application_library_path,    // native_library_path
    DartSnapshot::kIsolateDataSymbol,     // native_library_symbol_name
    false                                 // is_executable);#endif#endif  // DART_SNAPSHOT_STATIC_LINK}

要使代码生效,我们还需要指定正确的settings.ios_isolate_snapshot_data_path路径,这个路径变量就是之前在settings结构体中新增的配置。接下来路径指定需要在DefaultSettingsForProcess函数中完成。

4.3.4 修改引擎配置的加载,DefaultSettingsForProcess

引擎初始化时,会调用DefaultSettingsForProcess函数,来初始化Settings配置。由于我们删除了framework内部的flutter_assetsicudtl.dat,分离了App数据段IsolateData.datVMData.dat。接下来我们需要对这4个产物路径进行重新设置,让引擎能够顺利初始化。代码如下

代码语言:javascript
复制
// 文件:FlutterDartProject.mm//新增函数initSettingvoid initSettings(flutter::Settings &settings){    printf("%s %s:%d\n", __FUNCTION__, __FILE__, __LINE__);    // 检查资源目录,如果找到对应资源,则保存路径到Settings#if (FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG)//debug不做任何处理,只针对profile和release
    printf("settings.icu_data_path = %s\n", settings.icu_data_path.c_str());    // flutter_assets
    printf("settings.assets_path = %s\n", settings.assets_path.c_str());#if 1 //defined(TARGET_ARCH_ARM64) 架构区分在中有相关宏定义
    // VMData.dat
    NSString *vmDataPath = [[NSBundle mainBundle] pathForResource:@"VMData" ofType:@"dat"];    if (vmDataPath == nil)
    {
        NSLog(@"can not found VMData.dat from resource, download from server?");
    }else{
        NSLog(@"check vmDataPath from resource: %@", vmDataPath);
        settings.ios_vm_snapshot_data_path = vmDataPath.UTF8String;
    }    // IsolateData.dat
    NSString *isolateDataPath = [[NSBundle mainBundle] pathForResource:@"IsolateData" ofType:@"dat"];    if (vmDataPath == nil)
    {
        NSLog(@"can not found IsolateData.dat from resource, download from server?");
    }else{
        NSLog(@"check isolateDataPath from resource: %@", isolateDataPath);
        settings.ios_isolate_snapshot_data_path = isolateDataPath.UTF8String;
    }#else
    printf("arch is armv7\n");#endif

    // icudtl.dat
    NSString *icudtlPath = [[NSBundle mainBundle] pathForResource:@"icudtl" ofType:@"dat"];    if (icudtlPath == nil)
    {
        NSLog(@"can not found icudtl.dat from resource, download from server?");
    }else{
        NSLog(@"check icudtl from resource: %@", icudtlPath);
        settings.icu_data_path = icudtlPath.UTF8String;
    }    // flutter_assets//    settings.assets_path = assetsPath.UTF8String;


    // end#endif}

... static flutter::Settings DefaultSettingsForProcess(NSBundle* bundle = nil) {  auto command_line = flutter::CommandLineFromNSProcessInfo();  // Precedence:
  // 1. Settings from the specified NSBundle.
  // 2. Settings passed explicitly via command-line arguments.
  // 3. Settings from the NSBundle with the default bundle ID.
  // 4. Settings from the main NSBundle and default values.

  NSBundle* mainBundle = [NSBundle mainBundle];
  NSBundle* engineBundle = [NSBundle bundleForClass:[FlutterViewController class]];  bool hasExplicitBundle = bundle != nil;  if (bundle == nil) {
    bundle = [NSBundle bundleWithIdentifier:[FlutterDartProject defaultBundleIdentifier]];
  }  if (bundle == nil) {
    bundle = mainBundle;
  }  auto settings = flutter::SettingsFromCommandLine(command_line);
  initSettings(settings);//获取到settings后,调用initSettings函数对setting进行修改,将4个路径改到实际对应的位置

  ...
}

完成后,重新编译引擎,替换引擎即可。

4.4 Flutter侧产物打包脚本

注意脚本中使用CloseBitcode函数关闭了bitcode,让flutter build ios-framework命令生成的framework不带bitcode。因为xcode工程默认是开启bitcode的,而前面我们的引擎产物没有加--bitcode参数,不关闭xcode的bitcode选项是无法编译成功的。

脚本过程说明就省略了,这里直接贴上代码

代码语言:javascript
复制
#ios_build_reduce.sh
#检查路径是否存在,不存在就退出脚本
AssertExists() {
  if [[ ! -e "$1" ]]; then
    if [[ -h "$1" ]]; then
      echo "The path $1 is a symlink to a path that does not exist"
    else
      echo "The path $1 does not exist"
    fi
    exit -1
  fi
  return 0
}

#要关闭bitcode功能,必须修改.ios下的pods配置
CloseBitcode () {
  str1="#this is generate from ios_build_reduce.sh to set 'ENABLE_BITCODE' = 'NO' for all targets of pods build setting \n\
  post_install do |installer|\n\
    installer.pods_project.targets.each do |target|\n\
      target.build_configurations.each do |config|\n\
        config.build_settings['ENABLE_BITCODE'] = 'NO'\n\
      end\n\
    end\n\
  end"
  echo "$str1" >> $1
}

#运行脚本,开启VERBOSE_SCRIPT_LOGGING时提供命令记录
RunCommand() {
  if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
    echo "♦ $*" #show script
  fi
  "$@"  #run
  return $? #return last result
}

#VERBOSE_SCRIPT_LOGGING="开启脚本记录"
echo "iOS Flutter 产物集成 start!!!!!!"
cd ..

#bitcode需要手动配置.ios下的Pods工程,不能clean
flutter clean
echo "flutter clean done !!!"
flutter pub get
echo "flutter pub get done !!!"

#初始化目录
armv7=./SnapshotData/armv7
arm64=./SnapshotData/arm64
mkdir -p $armv7
mkdir -p $arm64
frameworkpath='build/ios/framework'
#rm       $frameworkpath/FlutterPackage.zip
#rm -rf   $frameworkpath/flutter_reduce
mkdir -p $frameworkpath/flutter_reduce/arm64
mkdir -p $frameworkpath/flutter_reduce/armv7

#修改podfile
podfile='./.ios/Podfile'
AssertExists "$podfile"
CloseBitcode "$podfile"

#编译framework
flutter build ios-framework
echo "flutter-framework 打包成功 !!!"

debugpath='build/ios/framework/Debug'
releasepath='build/ios/framework/Release'
libpath='../../ios/LocalLib/Flutter'

rm -rf "$libpath/Debug"
rm -rf "$libpath/Release"

echo "编译数据段"
xcrun cc -arch armv7  -c $armv7/IsolateSnapshotData.S -o $armv7/HeadIsolateData.dat
xcrun cc -arch armv7  -c $armv7/VmSnapshotData.S -o $armv7/HeadVMData.dat
# 去掉多余头部
tail -c +313 $armv7/HeadIsolateData.dat > $armv7/IsolateData.dat
tail -c +313 $armv7/HeadVMData.dat > $armv7/VMData.dat

xcrun cc -arch arm64  -c $arm64/IsolateSnapshotData.S -o $arm64/HeadIsolateData.dat
xcrun cc -arch arm64  -c $arm64/VmSnapshotData.S -o $arm64/HeadVMData.dat
# 去掉多余头部
tail -c +313 $arm64/HeadIsolateData.dat > $arm64/IsolateData.dat
tail -c +313 $arm64/HeadVMData.dat > $arm64/VMData.dat

cp -rf $armv7/IsolateData.dat  $frameworkpath/flutter_reduce/armv7
cp -rf $armv7/VMData.dat  $frameworkpath/flutter_reduce/armv7

cp -rf $arm64/IsolateData.dat  $frameworkpath/flutter_reduce/arm64
cp -rf $arm64/VMData.dat  $frameworkpath/flutter_reduce/arm64

echo "移除flutter_asserts"
mv $releasepath/App.framework/flutter_assets $frameworkpath/flutter_reduce/
echo "移除icudtl.dat"
mv $releasepath/Flutter.framework/icudtl.dat $frameworkpath/flutter_reduce/

echo "压缩分离产物FlutterPackage.zip"
zip -q -r $frameworkpath/FlutterPackage.zip $frameworkpath/flutter_reduce

echo  "framework优化体积:"
printf "未压缩总量:%s----%s\n" `du  -sh $frameworkpath/flutter_reduce`
printf "         \t├─%s----%s\n" `du  -sh $frameworkpath/flutter_reduce/armv7`
printf "         \t├─%s----%s\n" `du  -sh $frameworkpath/flutter_reduce/arm64`
printf "         \t├─%s----%s\n" `du  -sh $frameworkpath/flutter_reduce/icudtl.dat`
printf "         \t├─%s----%s\n" `du  -sh $frameworkpath/flutter_reduce/flutter_assets`
RunCommand printf "压缩后总量:%s----%s\n" `du  -sh $frameworkpath/FlutterPackage.zip`
printf "\n"
printf "flutter(armv7、arm64) strip之前:%s----%s\n" `du  -sh $frameworkpath/Release/Flutter.framework/flutter`
AssertExists "${frameworkpath}"
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM  $frameworkpath/Release/Flutter.framework/flutter
echo "分离符号:${frameworkpath}/Flutter.framework.dSYM"
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter
printf "flutter(armv7、arm64) strip之后:%s----%s\n" `du  -sh $frameworkpath/Release/Flutter.framework/flutter`

4.5 iOS侧Pod集成

iOS宿主工程Podfile添加如下代码。使用FlutterDebug.podspec和FlutterRelease.podspec来引入flutter的framework产物

代码语言:javascript
复制
# 是否源码集成, 0:否 / 1:是
IsFlutterSourceCode = 0

# 集成 FlutterModule
def FlutterModuleIntegration
    if IsFlutterSourceCode == 1
        # 源码集成(官方方案)
        flutter_application_path = '../native_modules/mlive/'
        load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
        install_all_flutter_pods(flutter_application_path)
    else
        # 区分debug/release场景使用不同的调试包
        pod 'FlutterDebug' ,:configurations => ['Debug'] ,:path => 'LocalLib/Flutter'
        pod 'FlutterRelease' ,:configurations => ['Release', 'AppStore', 'iAP'] ,:path => 'LocalLib/Flutter'
    end
end

4.6 最终效果

名称

优化方式

减包收益

icudtl.dat

下载

884k

flutter_assets

下载

2.1M

App数据段

下载

单架构平均2.8M

flutter

strip调试符号

单架构平均6M

framework总计收益

—-

11.7M

size Report最终收益

—-

—-

总结:我们通过删除不必要的文件、移走部分文件改为下发、去掉Flutter的符号文件、引擎大小优化等措施,使iOS接入Flutter的体积成本降到10M。

5、参考文章

  • 字节跳动- 【如何缩减接近 50% 的 Flutter 包体积】:https://juejin.im/post/5de8a32c51882512664affa4

QQ音乐招聘Android/ios客户端开发,点击左下方“查看原文”投递简历~

也可将简历发送至邮箱:tmezp@tencent.com

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、背景
    • 1.1 Flutter混合开发模式
      • 1.2 Flutter瘦身需求
        • 1.3 本文内容涉及的开发环境
        • 2、iOS framework产物分析
          • 2.1 framework结构
            • 2.2 Framework体积分析(Release模式下)
            • 3、iOS减包思路
              • 3.1 App.framework/flutter_assets
                • 3.2 Flutter.framework/icudtl.dat
                  • 3.3 App.Framework/App
                    • 3.4 FLutter.framework/Flutter
                    • 4、实践
                      • 4.1 引擎编译配置
                        • 4.2 xcode配置引擎调试环境
                          • 4.3 减包引擎源码修改
                            • 4.3.1 添加自定义配置,Settings
                            • 4.3.2 修改Dart编译工具,gen_snapshot
                            • 4.3.4 修改引擎配置的加载,DefaultSettingsForProcess
                          • 4.4 Flutter侧产物打包脚本
                            • 4.5 iOS侧Pod集成
                              • 4.6 最终效果
                              • 5、参考文章
                              相关产品与服务
                              云直播
                              云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档