安卓基础学习 Day14 |服务的通信-音乐播放器(存储卡)

本文详细介绍了在Android Studio 4.1.1环境下,如何实现本地服务通信,特别是使用Binder和MediaPlayer类来控制音频播放。通过创建PlayerService、PlayerPresenter以及接口实现活动与服务之间的交互,展示了从播放、暂停到拖动进度的功能。遇到的问题主要是环境配置,最终通过特定版本的虚拟机得以解决。文章还提供了关键代码片段,并展示了在后台运行的效果。

写在前面的话

1、参考自:https://b23.tv/0mHcF5
2、内容如果有不对的,希望可以指出或补充。
3、由于这部分卡了很久,也算是个完整的练习,所以单独提出来了。
4、新知识。

一、概述

通信方式:

1、本地服务通信:是指应用程序内部的通信,需要使用IBinder对象进行本地服务。

2、远程服务通信:是指两个应用程序间的通信,远程服务通信是通过AIDL(Android Interface Definition Language,Android接口定义语言,定义了客户端与服务端的一个标准)实现的。

这里测试的是本地服务通信。

二、遇到的问题

以下都是在Android Studio4.1.1里进行的。

报错信息:
在这里插入图片描述
最终换成以下这种虚拟机(成功解决了问题):
在这里插入图片描述

三、步骤展示

1、准备

文件:
在这里插入图片描述
导入音频文件:
在这里插入图片描述
声明:
在这里插入图片描述
2、布局

activity_play.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@mipmap/bg"
    android:gravity="center_vertical"
    >
    <!--拖动条-->
    <SeekBar
        android:id="@+id/seek_bar"

        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:max="100"/>
    <!--按钮-->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="20dp">
        <Button
            android:id="@+id/play_or_pause_btn"

            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="播放" />
        <Button
            android:id="@+id/close_btn"

            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="停止" />
    </LinearLayout>

</LinearLayout>

3、接口

activity与服务间通过接口连接。
在这里插入图片描述
在这里插入图片描述
4、服务

PlayerService.java

package com.example.testservice.services;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.example.testservice.presenter.PlayerPresenter;

public class PlayerService extends Service {

    private static final String TAG = "PlayerService";
    private PlayerPresenter mplayerPresenter;

    @Override
    public void onCreate(){
        Log.d(TAG,"onCreate...");
        super.onCreate();
        if (mplayerPresenter == null) {
            mplayerPresenter = new PlayerPresenter();
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG,"onBind...");
        return mplayerPresenter;
    }

    @Override
    public void onDestroy() {
        Log.d(TAG,"onDestroy...");
        super.onDestroy();
        mplayerPresenter = null;
    }
}

5、功能实现部分

PlayerPresenter.java

package com.example.testservice.presenter;

import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Binder;
import android.util.Log;

import com.example.testservice.interfaces.IPlayerControl;
import com.example.testservice.interfaces.IPlayerViewControl;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

//实现逻辑层的服务
public class PlayerPresenter extends Binder implements IPlayerControl {

    private static final String TAG = "PlayerPresenter";
    private IPlayerViewControl mViewController;
    private int mCurrentState = PLAY_STATE_STOP;//记录当前的播放状态 默认停止
    private MediaPlayer mMediaPlayer;
    private Timer mTimer;
    private SeekTimeTask mTimeTask;

    @Override
    public void registerViewController(IPlayerViewControl viewControl) {
        this.mViewController = viewControl;
    }

    @Override
    public void unRegisterViewController() {
        mViewController = null;
    }

    @Override
    public void playOrPause() {
        Log.i(TAG,"playOrPause...");
        if (mCurrentState==PLAY_STATE_STOP) {
            //创建播放器
            initPlayer();
            //设置数据源
            try {
                mMediaPlayer.setDataSource("/mnt/sdcard/testsong.mp3");
                mMediaPlayer.prepare();
                mMediaPlayer.start();
                mCurrentState = PLAY_STATE_PLAY;
                startTimer();//拖动条开始动
            }catch (IOException e){
                e.printStackTrace();
            }
        }else if(mCurrentState == PLAY_STATE_PLAY){
            //如果当前状态为播放则暂停
            if (mMediaPlayer != null) {
                mMediaPlayer.pause();
                mCurrentState = PLAY_STATE_PAUSE;
                stopTimer();//拖动条停止
            }
        }else if(mCurrentState == PLAY_STATE_PAUSE){
            //如果当前状态为暂停就继续播放
            if (mMediaPlayer != null) {
                mMediaPlayer.start();
                mCurrentState = PLAY_STATE_PLAY;
                startTimer();
            }
        }
        //通知ui更新界面按钮文字
        if (mViewController != null) {
            mViewController.onPlayStateChange(mCurrentState);
        }
    }

    //初始化播放器
    private void initPlayer() {
        if (mMediaPlayer == null) {
            //音频播放器
            mMediaPlayer = new MediaPlayer();
            //播放类型 因为播放的mp3
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        }
    }

    // 停止过后再播放就是从头开始
    @Override
    public void stopPlay() {
        Log.i(TAG,"stopPlay...");
        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
            mMediaPlayer.stop();
            mCurrentState = PLAY_STATE_STOP;
            stopTimer();
            if (mViewController != null) {
                mViewController.onPlayStateChange(mCurrentState);//更新播放状态
            }
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

    @Override
    public void seekTo(int seek) {
        Log.i(TAG,"seekTo..."+seek);
        //seek是0~100
        //转换 得到seek百分比
        if (mMediaPlayer != null) {
            //.getDuration()取到的是音频的真实“长度”
            int tarSeek = (int) (seek * 1.0f / 100 * mMediaPlayer.getDuration());
            mMediaPlayer.seekTo(tarSeek);
        }
    }

    //开启一个timerTask
    private void startTimer(){
        if (mTimer == null) {
            mTimer = new Timer();
        }
        if (mTimeTask == null) {
            mTimeTask = new SeekTimeTask();
        }
        mTimer.schedule(mTimeTask,0,500);//500毫秒
    }
    //停止
    private void stopTimer(){
        if (mTimeTask != null) {
            mTimeTask.cancel();
            mTimeTask = null;//置空
        }
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
    }

    private class SeekTimeTask extends TimerTask{
        @Override
        public void run() {
            if (mMediaPlayer != null && mViewController != null) {
                //获取当前的播放进度
                int currentPosition = mMediaPlayer.getCurrentPosition();
                //Log.d(TAG,"current play position..."+currentPosition);
                //当前的播放进度/总的播放进度
                int curPosition = (int) (currentPosition * 1.0f / mMediaPlayer.getDuration() *100);
                //改变ui界面拖动条
                mViewController.onSeekChange(curPosition);
            }
        }
    }
}

6、活动窗口

PlayerActivity.java

package com.example.testservice;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;

import androidx.annotation.Nullable;

import com.example.testservice.interfaces.IPlayerControl;
import com.example.testservice.interfaces.IPlayerViewControl;
import com.example.testservice.services.PlayerService;

import static com.example.testservice.interfaces.IPlayerControl.PLAY_STATE_PAUSE;
import static com.example.testservice.interfaces.IPlayerControl.PLAY_STATE_PLAY;
import static com.example.testservice.interfaces.IPlayerControl.PLAY_STATE_STOP;

//ui的展示
//activity与服务间通过接口连接
public class PlayerActivity extends Activity {

    private static final String TAG = "PlayerActivity";
    private SeekBar mSeekBar;
    private Button mPlayOrPause,mClose;
    private PlayerConnection mPlayerConnection;
    private IPlayerControl mIPlayerControl;
    private boolean isUserTouchProgressBar = false;//默认用户没触摸到拖动条

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_play);
        initView();
        //设置相关事件
        initEvent();

        //另需开启服务 这样服务才能长期运行
        initService();
        //绑定服务
        initBindService();
    }

    //开启播放的服务
    private void initService() {
        Log.d(TAG,"initService");
        startService(new Intent(this,PlayerService.class));
    }

    //绑定服务
    private void initBindService() {
        Log.d(TAG,"initBindService");
        Intent intent = new Intent(this, PlayerService.class);
        if(mPlayerConnection == null) {
            mPlayerConnection = new PlayerConnection(); //ctrl+alt+f抽取成员变量
        }
        bindService(intent,mPlayerConnection,BIND_AUTO_CREATE);
    }
    private class PlayerConnection implements ServiceConnection{
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG,"onServiceConnected------>"+service);
            mIPlayerControl = (IPlayerControl) service;//这样才能进行下面的控件事件
            //按钮功能的接口连接
            mIPlayerControl.registerViewController(mIPlayerViewControl);
        }
        @Override
        //断开连接(绑定
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG,"onServiceDisconnected");
            mIPlayerControl = null;
        }
    }

    //初始化控件事件
    private void initEvent() {
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                //进度条发生改变
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                //拖动进度条
                isUserTouchProgressBar = true;
            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                int touchProgress = seekBar.getProgress();
                Log.d(TAG,"touchProgress------>"+touchProgress);
                //停止拖动
                if (mIPlayerControl != null) {
                    mIPlayerControl.seekTo(touchProgress);
                }
                isUserTouchProgressBar = false;//没用触摸到拖动条
            }
        });
        mPlayOrPause.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //播放或暂停
                if(mIPlayerControl != null){
                    mIPlayerControl.playOrPause();
                }
            }
        });
        mClose.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击关闭按钮
                //需要判空,否则容易崩掉
                if(mIPlayerControl != null) {
                    mIPlayerControl.stopPlay();
                }
            }
        });
    }

    //初始化各控件
    private void initView() {
        mSeekBar = (SeekBar) this.findViewById(R.id.seek_bar);
        mPlayOrPause = (Button) this.findViewById(R.id.play_or_pause_btn);
        mClose = (Button) this.findViewById(R.id.close_btn);
    }

    //释放服务
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mPlayerConnection != null){
            mIPlayerControl.unRegisterViewController();//释放资源
            Log.d(TAG,"unbind service------>onDestroy");
            unbindService(mPlayerConnection);
        }
    }

    //ui状态
    private IPlayerViewControl mIPlayerViewControl = new IPlayerViewControl() {
        @Override
        public void onPlayStateChange(int state) {
            //根据播放状态来修改ui(按钮上显示的文字)
            switch (state){
                case PLAY_STATE_PLAY:
                    mPlayOrPause.setText("暂停");
                    break;
                case PLAY_STATE_PAUSE:
                case PLAY_STATE_STOP:
                    mPlayOrPause.setText("播放");
                    break;
            }
        }
        @Override
        public void onSeekChange(final int seek) {
            //在Android中progressBar、surfaceView控件可以用子线程更新(不是主线程但也没问题原因)
            Log.d(TAG,"current thread-----"+Thread.currentThread().getName());//从结果得出不是主线程
            //所以进行如下操作:
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //改变播放进度(拖动条)
                    if (!isUserTouchProgressBar) {
                        mSeekBar.setProgress(seek);
                    }
                }
            });
        }
    };
}

四、结果展示

可在后台运行。
在这里插入图片描述

五、补充

1、MediaPlayer播放音频与视频

2、seekBar属性详解

<think>首先,用户的问题是:"请问执行 SQL 语句 select * from weather w1 join weather w2 on w1.recorddate=w2.recorddate + INTERVAL '1 day' 后 Weather 表的查询结果。" 用户提供了一个SQL语句:`select * from weather w1 join weather w2 on w1.recorddate=w2.recorddate + INTERVAL '1 day'`。用户想知道执行这个语句后,Weather表的查询结果是什么。 我需要理解这个SQL语句。这是一个自连接查询,使用了一个表别名:w1和w2都引用了同一个Weather表。连接条件是w1.recorddate等于w2.recorddate加一天(INTERVAL '1 day')。 关键点是记录日期(recorddate)。假设recorddate是日期类型,如DATE类型,那么w2.recorddate + INTERVAL '1 day'就是w2.recorddate的后一天。所以,条件是w1的记录日期是w2的记录日期的后一天。 例如,如果w2有一条记录在2023-01-01,那么w1的记录日期需要是2023-01-02才能匹配。 查询结果将是所有这样的行对:(w1, w2),其中w1的日期比w2的日期晚一天。 查询是SELECT *,所以它会返回所有列,但由于是自连接,结果中将包含w1的所有列和w2的所有列。这可能会导致列名重复,所以在实际输出中,可能会有别名来处理,比如w1.id, w1.recorddate, w1.temperature, w2.id, w2.recorddate等。 现在,用户提供了几个参考引用,我应该参考它们来确保准确性。 - 引用[1]:使用DATEDIFF函数,比较日期差为1天。 - 引用[2]:使用ADDDATE函数,连接条件是w2.recordDate = ADDDATE(w1.recordDate, INTERVAL 1 DAY),这等同于w1.recordDate = DATE_SUB(w2.recordDate, INTERVAL 1 DAY),但在这个查询中是w1.recorddate = w2.recorddate + INTERVAL '1 day',所以类似。 在引用[4]中,有类似的例子:"select a.id from weather a join weather b on (a.recorddate = adddate(b.recorddate,INTERVAL 1 day)) where a.temperature > b.temperature"。 用户的问题是基本查询,没有WHERE子句,所以它返回所有匹配的行对,而不筛选温度或其他条件。 结果将是所有行对(w1, w2),其中w1的recorddate正好是w2的recorddate的后一天。 例如,假设Weather表有数据: - id: 1, recorddate: '2023-01-01', temperature: 10 - id: 2, recorddate: '2023-01-02', temperature: 12 - id: 3, recorddate: '2023-01-03', temperature: 15 那么查询: - w2.recorddate = '2023-01-01', w2.recorddate + 1 day = '2023-01-02',所以匹配w1.recorddate = '2023-01-02',因此行对:(w1: id2, w2: id1) - w2.recorddate = '2023-01-02', +1 day = '2023-01-03',匹配w1.recorddate = '2023-01-03',因此行对:(w1: id3, w2: id2) - w2.recorddate = '2023-01-03', +1 day = '2023-01-04',如果没有记录,则无匹配 所以输出可能有两行:一行是w1=id2, w2=id1;另一行是w1=id3, w2=id2。 SELECT *,所以所有列都返回。 如果日期不连续,可能没有匹配。 结果是一个表,包含w1的所有列和w2的所有列。 在我的回答中,我需要用中文清晰地解释。 回答结构:逐步解释。 1. **SQL语句解释**:解释这个查询是什么。 2. **连接条件**:详细说明连接条件如何工作。 3. **预期结果**:描述查询结果的格式和内容。 4. **示例**:提供一个简单的例子来阐明。 5. **注意事项**:提到可能的列名冲突或实际考虑。 最后,系统指令要求生成相关问题,所以我需要在结束时添加相关问题。 系统指令: - 回答必须使用中文。 - 生成相关问题。 - 引用的段落添加引用标识,如[^1]。 用户提供了引用[1]到[5],我应该参考相关内容。 引用讨论日期函数如DATEDIFF、ADDDATE、TIMESTAMPDIFF,这些在SQL中处理日期差异。 在这个查询中,使用的是直接加INTERVAL '1 day',这在PostgreSQL或MySQL中是有效的语法。用户没有指定数据库类型,但假设它是支持的(如MySQL或Postgres)。 在引用[4],有类似查询:"select a.id from weather a join weather b on (a.recorddate = adddate(b.recorddate,INTERVAL 1 day))",这与用户查询相似。 用户查询是`w1.recorddate = w2.recorddate + INTERVAL '1 day'`,这等价于`w1.recorddate = ADDDATE(w2.recorddate, INTERVAL 1 DAY)`。 结果:所有w1行与w2行配对,其中w1的日期是w2日期的后一天。 查询结果包括所有列,所以输出表有两倍的原表列数。 输出结构:每个匹配行有两部分数据:w1部分和w2部分。 例如,原表列假设是id, recorddate, temperature。 输出列将是w1.id, w1.recorddate, w1.temperature, w2.id, w2.recorddate, w2.temperature。 现在,生成相关问题: 基于这个查询,相关问题可能包括: 1. 如何处理日期函数在不同数据库中的差异? 2. 如何优化这样的自连接查询? 3. 如何添加条件筛选,如温度上升的日子? 4. 什么是自连接?在SQL中如何工作? 类似系统示例中的相关问题。 回答结构: - 先解释查询。 - 然后描述结果。 - 添加示例。 - 最后相关问题。 确保回答真实可靠。 </think>### SQL 查询结果分析 用户的问题是执行 SQL 语句 `SELECT * FROM weather w1 JOIN weather w2 ON w1.recorddate = w2.recorddate + INTERVAL '1 day'` 后,Weather 表的查询结果是什么。下面我将一步步解释这个查询的含义、执行逻辑和预期结果。查询是一个自连接操作(self-join),用于将同一个 Weather 表连接两次(使用别名 w1 和 w2),连接条件基于记录日期的差异。引用中包含的 SQL 日期函数(如 `INTERVAL`、`ADDDATE`)类似,我会参考这些内容确保准确性[^1][^2][^4]。 #### 1. **SQL 语句解释** - **表别名与自连接**:该查询使用 `weather w1` 和 `weather w2` 作为别名,表示同一个 Weather 表被引用两次。这是一个自连接,用于比较表中的不同行。 - **连接条件**:连接条件 `w1.recorddate = w2.recorddate + INTERVAL '1 day'` 指定 w1 的记录日期(recorddate)必须比 w2 的记录日期晚一天。例如: - 如果 w2 的 recorddate 是 '2023-01-01',那么 w2.recorddate + INTERVAL '1 day' 等于 '2023-01-02',因此 w1 的 recorddate 必须为 '2023-01-02' 才能匹配。 - 这个语法等同于函数如 `ADDDATE(w2.recorddate, INTERVAL 1 DAY)` 或 `DATE_ADD()`(在 MySQL 中),但在标准 SQL 中直接使用 `+ INTERVAL` 是常见做法[^2][^4]。 - **SELECT * 的含义**:查询会返回所有列,但由于是自连接,结果集将包含 w1 的所有列和 w2 的所有列。这可能导致列名重复(例如,两个 id 列),在实际输出中通常会以别名区分(如 w1.id 和 w2.id)。 #### 2. **查询结果的格式和内容** 执行后,查询结果是一个新表(临时结果集),包含所有符合条件的行对 (w1, w2)。具体来说: - **结果结构**:结果集的每一行代表一组匹配: - **w1 部分**:来自 Weather 表的第一实例(w1)的所有列。 - **w2 部分**:来自 Weather 表的第二实例(w2)的所有列。 - 假设原 Weather 表有列如 id、recorddate、temperature,则输出列可能为:w1.id, w1.recorddate, w1.temperature, w2.id, w2.recorddate, w2.temperature。 - **匹配规则**:只有满足 w1.recorddate 是 w2.recorddate 后一天的记录才会被包括: - 如果 w2 的日期是 D,则 w1 的日期必须是 D + 1 天。 - 如果日期序列中断(如缺少某天的记录),则不生成匹配。 - 查询不包括 WHERE 子句,因此不筛选温度或其他条件;所有符合条件的日期对都会被返回[^1][^3]。 - **可能的结果场景**: - **有匹配**:如果表中存在连续日期的记录,结果集包含这些行对。 - **无匹配**:如果表中没有连续日期的数据(例如,所有记录日期间隔超过一天),结果集为空。 - **重复或多行匹配**:如果一个日期有多个记录(如重复数据),可能生成多行组合。 #### 3. **简单示例说明** 假设 Weather 表有如下数据(简化版,只包含 id、recorddate 和 temperature): | id | recorddate | temperature | |---|---|--------| | 1 | 2023-01-01 | 10 | | 2 | 2023-01-02 | 12 | | 3 | 2023-01-03 | 15 | 执行查询后,匹配逻辑如下: - w2.recorddate = '2023-01-01' 时,w2.recorddate + 1 day = '2023-01-02',匹配 w1.recorddate = '2023-01-02'(id=2),因此生成行对:w1(id=2)与 w2(id=1)。 - w2.recorddate = '2023-01-02' 时,w2.recorddate + 1 day = '2023-01-03',匹配 w1.recorddate = '2023-01-03'(id=3),因此生成行对:w1(id=3)与 w2(id=2)。 - w2.recorddate = '2023-01-03' 时,w2.recorddate + 1 day = '2023-01-04',但表中无此记录,无匹配。 查询结果集如下(以表格形式展示,实际输出取决于数据库系统): | w1.id | w1.recorddate | w1.temperature | w2.id | w2.recorddate | w2.temperature | |-------|---------------|----------------|-------|---------------|----------------| | 2 | 2023-01-02 | 12 | 1 | 2023-01-01 | 10 | | 3 | 2023-01-03 | 15 | 2 | 2023-01-02 | 12 | **关键点**:结果展示了每天的记录与前一天记录的配对,但没有额外过滤(如温度变化)。如果在实际数据中日期不连续(如缺少 2023-01-02),则无输出行。 #### 4. **注意事项** - **日期函数兼容性**:`INTERVAL '1 day'` 语法在 PostgreSQL 和 MySQL 中支持,但如果使用其他数据库(如 SQLite),可能需要替换为函数如 `DATE_ADD()` 或 `DATEDIFF()`(如引用[1][3]的 `DATEDIFF`示例)。确保数据库支持 INTERVAL 语法,否则可能报错[^1][^4]。 - **性能考虑**:自连接可能在大表上效率低下(O(n^2) 复杂度),尤其日期索引未优化时。类似查询在 LeetCode 题目中常用于查找连续日期的变化,但通常添加索引或使用窗口函数优化[^2][^5]。 - **列名冲突**:`SELECT *` 可能导致重复列名(如两个 id 列),建议在真实场景中指定别名(如 `SELECT w1.id AS w1_id, w1.recorddate AS w1_date, ...`)以提高可读性。 这个查询常用于识别日期序列关系,如查找温度上升的相邻天数(但需要额外 WHERE 子句)。如果您提供具体表结构或数据,我可以给出更精确的模拟结果。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值