Flutter系列二:探究Flutter App在Android宿主App中的整合

前面一篇文章我们探讨了一下Flutter App如何被整合到iOS App中的,本文我们接着来讨论下Flutter App如何被整合到Android App中的。

Gradle

我们简单看一下Android项目的代码结构:

Android项目的代码结构

作为Android项目的自动化构建工具,我们先来看看GradleFlutter APP的构建过程中大概做了哪些工作。

settings.gradle

settings.gradle中主要是用来配置Android Project中所有需要依赖的module,即进行工程树的配置。

settings.gradle
// 1
include ':app'

// 2
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()

assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
// 3
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
  1. 首先引入app module
  2. 读取local.properties这个文件中的flutter.sdk属性的值,赋值给flutterSdkPath这个变量
  • local.properties中除了Android SDK路径,还定义了Flutter相关的一些值。如Flutter SDK路径Flutter构建模式Flutter版本编号等
local.properties
sdk.dir=/Users/*/Library/Android/sdk
flutter.sdk=/Users/*/Documents/flutter
flutter.buildMode=debug
flutter.versionName=1.0.0
flutter.versionCode=1
  1. 引入"$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"中的脚本

我们接下来来看看app_plugin_loader.gradle脚本的代码:

app_plugin_loader.gradle
import groovy.json.JsonSlurper

def flutterProjectRoot = rootProject.projectDir.parentFile

// 1 找到配置文件
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins-dependencies')
if (!pluginsFile.exists()) {
  return
}

// 2 读取配置文件
def object = new JsonSlurper().parseText(pluginsFile.text)
assert object instanceof Map
assert object.plugins instanceof Map
assert object.plugins.android instanceof List
object.plugins.android.each { androidPlugin ->
  assert androidPlugin.name instanceof String
  assert androidPlugin.path instanceof String
  def pluginDirectory = new File(androidPlugin.path, 'android')
  assert pluginDirectory.exists()
  include ":${androidPlugin.name}"
  project(":${androidPlugin.name}").projectDir = pluginDirectory
}

  • 读取Android文件同级目录下的.flutter-plugins-dependencies文件
  • 读取该文件下的plugins字段下的android数组,对数组的每个元素配置依赖。

提示: 是不是很熟悉? 没错iOSPod脚本读的就是这个文件plugins字段下的的ios字段的值。

{
    "plugins":{
        "android":[
            ...
            {
                "name":"sqflite",
                "path":"/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/",
                "dependencies":[

                ]
            }
            ...
        ],
        "ios":[...],
    }
}

总结一下,最后settings.gradle大致生成的内容如下所示:

include ':app'

include ':fijkplayer'
project(":fijkplayer").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/fijkplayer-0.8.7/android'

include ':shared_preferences'
project(":shared_preferences").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.12+4/android'

include ':sqflite'
project(":sqflite").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/android'

include ':url_launcher'
project(":url_launcher").projectDir = '/Users/*/Documents/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-5.7.10/android'

总结:settings.gradle中完成了所有依赖的module的配置。

Project / build.gradle

我们来看看Project / build.gradle中的一些设置:

// 1.
rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
// 2.
subprojects {
    project.evaluationDependsOn(':app')
}

  1. 设置project子project的输出路径,路径为和android同级的build文件夹下。

build directory

进去瞅瞅就可以验证:

build files

你估计发现了,iOS的编译结果也是放在这个目录里面的。

  1. 通过evaluationDependsOn定义了所有其他的moudule的配置都依赖于app这个moudule。即其他的所有moudule配置必须得等app这个moudule的配置完成后再进行配置。

总结:Project / build.gradle中配置了各个moudule的编译输出路径和moudule间的依赖关系。

app / build.gradle

接下来我们来看下app / build.gradle中的内容:

// 1. 
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}

def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}

// 2.
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

// 3.
flutter {
    source '../..'
}

代码解释:

  1. 第一大段的作用是从local.properties文件中读取版本编号和版本名称,设置为Android App的版本编号和版本名称;
  2. 根据从local.properties文件中读取到的$flutterRoot路径导入$flutterRoot/packages/flutter_tools/gradle/flutter.gradle的脚本运行;
  3. flutter扩展的source属性配置为.. / ..
flutter.gradle

flutter.gradle的目的是在Android 宿主App的编译构建流程中执行一些Flutter相关的任务。

flutter.gradle中有两个重要的类,一个是FlutterPlugin,一个是FlutterTask

flutter.gradle代码结构

FlutterPlugin

FlutterPlugin作为Gradle Plugin实现了Plugin接口,所以它的入口方法是apply()方法:

FlutterPlugin/apply
void apply(Project project) {
    ...
    // 1. 
    project.extensions.create("flutter", FlutterExtension)
    // 2
    project.afterEvaluate this.&addFlutterTasks
    // 3
    if (shouldSplitPerAbi()) {
        project.android {
            splits {
                abi {
                    enable true
                    reset()
                    universalApk false
                }
            }
        }
    }
    getTargetPlatforms().each { targetArch ->
        String abiValue = PLATFORM_ARCH_MAP[targetArch]
        project.android {
            if (shouldSplitPerAbi()) {
                splits {
                    abi {
                        include abiValue
                    }
                }
            }
        }
    }
    
    // 4
    String flutterRootPath = resolveProperty("flutter.sdk", System.env.FLUTTER_ROOT)
    if (flutterRootPath == null) {
        throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file or with a FLUTTER_ROOT environment variable.")
    }
    flutterRoot = project.file(flutterRootPath)
    if (!flutterRoot.isDirectory()) {
        throw new GradleException("flutter.sdk must point to the Flutter SDK directory")
    }

    engineVersion = useLocalEngine()
        ? "+" // Match any version since there's only one.
        : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "internal", "engine.version").toFile().text.trim()

    String flutterExecutableName = Os.isFamily(Os.FAMILY_WINDOWS) ? "flutter.bat" : "flutter"
    flutterExecutable = Paths.get(flutterRoot.absolutePath, "bin", flutterExecutableName).toFile();

    // 5
    project.android.buildTypes {
        profile {
            initWith debug
            if (it.hasProperty("matchingFallbacks")) {
                matchingFallbacks = ["debug", "release"]
            }
        }
    }
    // 6
    if (shouldShrinkResources(project)) {
        String flutterProguardRules = Paths.get(flutterRoot.absolutePath, "packages", "flutter_tools",
                    "gradle", "flutter_proguard_rules.pro")
        project.android.buildTypes {
            release {
                shrinkResources isBuiltAsApp(project)
                proguardFiles project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro"
            }
        }
    }
    // 7
    if (useLocalEngine()) {
        String engineOutPath = project.property('local-engine-out')
        File engineOut = project.file(engineOutPath)
        if (!engineOut.isDirectory()) {
            throw new GradleException('local-engine-out must point to a local engine build')
        }
        localEngine = engineOut.name
        localEngineSrcPath = engineOut.parentFile.parent
    }
    // 8
    project.android.buildTypes.each this.&addFlutterDependencies
    project.android.buildTypes.whenObjectAdded this.&addFlutterDependencies
}

代码解释:

  1. 创建一个FlutterExtension扩展,这个扩展有两个属性,source-Flutter APP工程的路径,target- Flutter APP的执行入口,不设置就默认lib/main.dart
  2. app module的其他Task完成后执行addFlutterTasks方法;
  3. 决定是否开启abi分包
  4. 获取一些系统环境变量;
    • flutterRootPath — /Users/*/Documents/flutter
    • flutterRoot — /Users/*/Documents/flutter
    • engineVersion — 1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e
    • flutterExecutable — flutter(mac), flutter.bat(windows)
  5. 默认有debugrelease两个模式,这里参照debug模式又添加了一个profile构建模式,所以现在变成了debug,releaseprofile三个构建模式;
  6. 是否启动shrinkResources资源缩减;
  7. 是否设置本地maven仓库;
  8. 给每个构建模式添加Flutter依赖addFlutterDependencies的调用。

接下来我们看看addFlutterDependencies中的实现:

FlutterPlugin/addFlutterDependencies
void addFlutterDependencies(buildType) {
    ...
    // 1. 
    String hostedRepository = System.env.FLUTTER_STORAGE_BASE_URL ?: DEFAULT_MAVEN_HOST
    String repository = useLocalEngine()
        ? project.property('local-engine-repo')
        : "$hostedRepository/download.flutter.io"
    project.rootProject.allprojects {
        repositories {
            maven {
                url repository
            }
        }
    }
    // 2
    addApiDependencies(project, buildType.name,
            "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion")
            print("io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion\n");

    // 3
    List<String> platforms = getTargetPlatforms().collect()
    if (flutterBuildMode == "debug" && !useLocalEngine()) {
        platforms.add("android-x86")
        platforms.add("android-x64")
    }
    platforms.each { platform ->
        String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_")
        addApiDependencies(project, buildType.name,
                "io.flutter:${arch}_$flutterBuildMode:$engineVersion")
                            print("io.flutter:${arch}_$flutterBuildMode:$engineVersion\n");
    }
}

代码解释:

  1. 设置maven仓库的url地址,默认是https://storage.googleapis.com/download.flutter.io, 如果网速不太理想也可以配置FLUTTER_STORAGE_BASE_URL环境变量,让其指向国内的镜像地址https://storage.flutter-io.cn/download.flutter.io
  2. 添加嵌入式的依赖io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion, 这个依赖与构建模式Flutter Engine版本有关系。例子-io.flutter:flutter_embedding_debug:1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e
  3. 添加libflutter.so依赖,这个依赖和架构Flutter Engine版本有关系。例子-io.flutter:armeabi_v7a_debug:1.0.0-2c956a31c0a3d350827aee6c56bb63337c5b4e6e

提示:

  1. flutter_embedding的作用是赋予Flutter嵌入native的能力;
  2. libflutter.so就是Flutter Engine;
  3. 这两个依赖最后调用的是project.dependencies.add(configuration, dependency, config)这个方法,所以是给project加的依赖。因为sqflite等其他的module都需要这两个依赖。

至此FlutterPlugin/apply的流程已经分析完了,接下来我们就来分析第2步遗留的addFlutterTasks方法。

FlutterPlugin/addFlutterTasks

这个方法的代码量比较大,我们来概括总结一下。

private void addFlutterTasks(Project project) {
    // 1
    String target = project.flutter.target
    if (target == null) {
        target = 'lib/main.dart'
    }
    // 2
    def addFlutterDeps = { variant -> 
        2.1 
        FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) { ... }
        2.2
        Task packFlutterAppAotTask = project.tasks.create(name: "packLibs${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}", type: Jar) { ... }
        2.3
        addApiDependencies(project, variant.name, project.files {
            packFlutterAppAotTask
        })
        2.4 
        Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}",
                type: Copy,
            ) { ... }
    }
    // 3
    project.android.applicationVariants.all { variant -> ...}
    // 4
    configurePlugins()
}
  1. gradle.properties获取各种参数,上面给出的一个例子—如果没有target配置,就默认设置为lib/main.dart
  2. 定义一个addFlutterDeps的函数.
  • 根据构建模式和第一步从gradle.properties获取各种参数一起创建对应的FlutterTaskFlutterTask的功能则是编译Flutter APP的代码。
  • packFlutterAppAotTask这个task是将FlutterTask的编译结果打包成libs.jar文件。
  • project上libs.jar的文件依赖。
  • copyFlutterAssetsTask是进行Flutter App相关的asset进行拷贝。由于Flutter可能作为插件编译或子项目编译,如果是插件编译产物编译结果打包为AAR,子项目编译则子项目编译时,编译结果被打包成APK,所以两种情况下有区别处理。
  1. 为所有applicationVariantslibraryVariants添加Flutter依赖,执行addFlutterDeps函数然后把APK拷贝到目标路径。
  2. 这个方法是给project添加Plugin的依赖.编译方式不同依赖的处理方式也不一样。
FlutterTask

FlutterTaskbuild()调用的是父类的buildBundle()方法:

void buildBundle() {
    if (!sourceDir.isDirectory()) {
        throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
    }

    intermediateDir.mkdirs()

    // Compute the rule name for flutter assemble. To speed up builds that contain
    // multiple ABIs, the target name is used to communicate which ones are required
    // rather than the TargetPlatform. This allows multiple builds to share the same
    // cache.
    String[] ruleNames;
    if (buildMode == "debug") {
        if (fastStart) {
            ruleNames = ["faststart_android_application"]
        } else {
            ruleNames = ["debug_android_application"]
        }
    } else {
        ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" }
    }
    project.exec {
        logging.captureStandardError LogLevel.ERROR
        executable flutterExecutable.absolutePath
        workingDir sourceDir
        if (localEngine != null) {
            args "--local-engine", localEngine
            args "--local-engine-src-path", localEngineSrcPath
        }
        if (verbose) {
            args "--verbose"
        } else {
            args "--quiet"
            }
        args "assemble"
        args "--depfile", "${intermediateDir}/flutter_build.d"
        args "--output", "${intermediateDir}"
        if (performanceMeasurementFile != null) {
            args "--performance-measurement-file=${performanceMeasurementFile}"
        }
        if (!fastStart || buildMode != "debug") {
            args "-dTargetFile=${targetPath}"
        } else {
            args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}"
        }
        args "-dTargetPlatform=android"
        args "-dBuildMode=${buildMode}"
        if (trackWidgetCreation != null) {
            args "-dTrackWidgetCreation=${trackWidgetCreation}"
        }
        if (splitDebugInfo != null) {
            args "-dSplitDebugInfo=${splitDebugInfo}"
        }
        if (treeShakeIcons == true) {
            args "-dTreeShakeIcons=true"
        }
        if (dartObfuscation == true) {
            args "-dDartObfuscation=true"
        }
        if (dartDefines != null) {
            args "--DartDefines=${dartDefines}"
        }
        if (bundleSkSLPath != null) {
            args "-iBundleSkSLPath=${bundleSkSLPath}"
        }
        if (codeSizeDirectory != null) {
            args "-dCodeSizeDirectory=${codeSizeDirectory}"
        }
        if (extraGenSnapshotOptions != null) {
            args "--ExtraGenSnapshotOptions=${extraGenSnapshotOptions}"
        }
        if (extraFrontEndOptions != null) {
            args "--ExtraFrontEndOptions=${extraFrontEndOptions}"
        }
        args ruleNames
    }
}

这个方法其实是执行带一些参数的flutter build方法。

APK

最后,我们来看看APK的结构。

  1. lib文件中默认包含了4种架构的支持,里面包括了Flutter Enginelibflutter.soFlutter App代码libapp.soFlutter插件依赖的so文件libijkffmpeg.so,libijkplayer.so,libijksdl.so

你可能会遇到couldn't find "libflutter.so",因为x86里面没有libflutter.so和libapp.so,这是flutter的一个已知的问题。

  1. Flutter App中的资源文件都打包到了assets/flutter_assets中。

APK

AndroidManifest.xml

我们来看看AndroidManifest.xml里面都配置了些什么东东:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.jj_movie">
    // 1.
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="jj_movie"
        android:icon="@mipmap/ic_launcher">
        <activity
            ...>
            // 2
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            // 3
            <meta-data
              android:name="io.flutter.embedding.android.SplashScreenDrawable"
              android:resource="@drawable/launch_background"
              />
            
        </activity>
        // 4
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>
  1. 项目的Application的类为io.flutter.app.FlutterApplication;
  2. 默认定义了一个白色的主题,在Flutter APP加载显示前用户可见,在Flutter APP加载显示后作为Window的背景;
  3. 可以修改启动图;
  4. 使用Flutter Android Embedding V2 版本。FlutterActivity,FlutterActivity,FlutterActivityFlutterActivity等类都是在V2版本中引入的。

FlutterApplication

FlutterApplication中的代码很简单,就主要执行了FlutterInjector.instance().flutterLoader().startInitialization(this)这行代码。

public class FlutterApplication extends Application {
  @Override
  @CallSuper
  public void onCreate() {
    super.onCreate();
    FlutterInjector.instance().flutterLoader().startInitialization(this);
  }
}

FlutterLoader主要的作用就是加载FLutter Engine 和加载Flutter APP的资源等。我们看看FlutterLoaderstartInitialization方法中的主要代码:

public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
    // 1.
    if (Looper.myLooper() != Looper.getMainLooper()) {
      throw new IllegalStateException("startInitialization must be called on the main thread");
    }

    // 2.
    VsyncWaiter.getInstance((WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE))
        .init();

    // 3.
    Callable<InitResult> initTask =
        new Callable<InitResult>() {
          @Override
          public InitResult call() {
            ...
          }
        };
  }
  1. 确保本方法是在主线程执行;
  2. VsyncWaiter进行初始化,它的主要作用是在Android系统注册并等待VSync信号。

VsyncWaiterFlutter渲染的中继者,当收到信号后,会通知Flutter app发起渲染调用,然后执行一些列的layout和paint,最后提交给GPU线程合成上屏。

  1. 开启一个异步线程,加载一些asset资源。

总结:FlutterLoader执行startInitialization是为Flutter app的 加载和渲染做好准备工作。

FlutterActivity

我们来看看FlutterActivity的重要代码:

public class FlutterActivity extends Activity {
    // 1
    protected FlutterActivityAndFragmentDelegate delegate;
    // 2
    private LifecycleRegistry lifecycle;

    public FlutterActivity() {
        lifecycle = new LifecycleRegistry(this);
    }
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // 3.1
        switchLaunchThemeForNormalTheme();
    
        super.onCreate(savedInstanceState);

        // 3.2
        delegate = new FlutterActivityAndFragmentDelegate(this);
        delegate.onAttach(this);
        delegate.onActivityCreated(savedInstanceState);
        
        // 3.3
        configureWindowForTransparency();
        // 3.4
        setContentView(createFlutterView());
        // 3.5
        configureStatusBarForFullscreenFlutterExperience();
    }
    
    private View createFlutterView() {
    return delegate.onCreateView(null , null, null);
  }
}
  1. FlutterActivityAndFragmentDelegate属性delegate可以说是负责处理FLutter app相关的绝大部分功能的对象;
  2. LifecycleRegistry是一个LifeCycle,处理FlutterActivity生命周期的事情,在构造函数中初始化;
  3. onCreate方法中主要的工作是:
    • 先切换到启动图的主题显示启动图片
    • 初始化FlutterActivityAndFragmentDelegate对象delegate
    • 然后将window的背景设置透明
    • FlutterActivityView添加由delegate对象创建的一个FlutterView,作为FLutter app的渲染ViewFlutterViewSurfaceView的子类。
    • Android 5.0以上设置为沉浸式状态栏

到此为止,一切都已经准备就绪了,就等FlutterActivityAndFragmentDelegate将内容加载进来了。

FlutterActivityAndFragmentDelegate

  1. 构造函数传入FlutterActivity作为Host, 主要就是为了获取context
FlutterActivityAndFragmentDelegate(@NonNull Host host) {
    this.host = host;
  }
  1. onAttach主要是初始化了Flutter Engine和注册了Flutter插件;
void onAttach(@NonNull Context context) {
    ensureAlive();

    if (flutterEngine == null) {
      setupFlutterEngine();
    }
    
    host.configureFlutterEngine(flutterEngine);
  }
// FlutterActivity
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    GeneratedPluginRegister.registerGeneratedPlugins(flutterEngine);
 }

GeneratedPluginRegistrant这个注册插件的逻辑和iOS的类似。

  1. 加载Flutter App的内容

FlutterActivity执行onStart时会调用FlutterActivityAndFragmentDelegateonStart方法,然后就从lib/main.dart入口文件开始执行了。

void onStart() {
    doInitialFlutterViewRun();
}

private void doInitialFlutterViewRun() {
    
    if (flutterEngine.getDartExecutor().isExecutingDart()) {
      return;
    }

    if (host.getInitialRoute() != null) {
      flutterEngine.getNavigationChannel().setInitialRoute(host.getInitialRoute());
    }

    String appBundlePathOverride = host.getAppBundlePath();
    if (appBundlePathOverride == null || appBundlePathOverride.isEmpty()) {
      appBundlePathOverride = FlutterInjector.instance().flutterLoader().findAppBundlePath();
    }

    DartExecutor.DartEntrypoint entrypoint =
        new DartExecutor.DartEntrypoint(
            appBundlePathOverride, host.getDartEntrypointFunctionName());
    flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint);
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值