Android Service 之三(Bind Service,使用 AIDL)

http://www.race604.com/communicate-with-remote-service-1/

首先,大概来总结一下与Service的通信方式有很多种:

  1. 通过BroadCastReceiver:这种方式是最简单的,只能用来交换简单的数据;
  2. 通过Messager:这种方式是通过一个传递一个Messager给对方,通过这个它来发送Message对象。这种方式只能单向传递数据。可以是ServiceActivity,也可以是从Activity发送数据给Service。一个Messeger不能同时双向发送;
  3. 通过Binder来实现远程调用(IPC):这种方式是Android的最大特色之一,让你调用远程Service的接口,就像调用本地对象一样,实现非常灵活,写起来也相对复杂。

本文最重点谈一下怎么使用AIDL实现Service端和Client端的双向通信(或者叫”调用”)。

首先定义一个AIDL接口如下:

1
// IRemoteService.aidl
package com.race604.servicelib;

interface IRemoteService {
    int someOperate(int a, int b);
}

这里只定义了一个简单的接口someOperate(),输入参数ab,返回一个int值。

Service的实现如下:

1
// RemoteService.java
package com.race604.remoteservice;

import ...

public class RemoteService extends Service {
    private static final String TAG = RemoteService.class.getSimpleName();
    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        @Override
        public int someOperate(int a, int b) throws RemoteException {
            Log.d(TAG, "called RemoteService someOperate()");
            return a + b;
        }
    };

	@Override
    public IBinder onBind(Intent intent) {
        return mBinder; // 注意这里返回binder
    }
}

这里,在RemoteService里面实现一个IRemoteService.Stub接口的Binder,并且在onBind()中返回此Binder对象。
AndroidManifest.xmlRemoteService的申明如下:

1
<service
  android:name=".RemoteService"
  android:enabled="true"
  android:exported="true" >

  <intent-filter>
  	<action android:name="com.race604.servicelib.IRemoteService" />
  </intent-filter>
</service>

这里android:exported="true"表示可以让其他进程绑定,这里还有一个<action android:name="com.race604.servicelib.IRemoteService" />,这里是为了让后面的Client通过此Action来绑定。

Client的调用方法如下:

1
package com.race604.client;

import ...

public class MainActivity extends ActionBarActivity implements View.OnClickListener {

    private static final String TAG = MainActivity.class.getSimpleName();
    private IRemoteService mService;

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Toast.makeText(MainActivity.this, "Service connected", Toast.LENGTH_SHORT).show();
            mService = IRemoteService.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Toast.makeText(MainActivity.this, "Service disconnected", Toast.LENGTH_SHORT).show();
            mService = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.bind).setOnClickListener(this);
        findViewById(R.id.unbind).setOnClickListener(this);
        findViewById(R.id.call).setOnClickListener(this);
    }

    private void callRemote() {

        if (mService != null) {
            try {
                int result = mService.someOperate(1, 2);
                Toast.makeText(this, "Remote call return: " + result, Toast.LENGTH_SHORT).show();
            } catch (RemoteException e) {
                e.printStackTrace();
                Toast.makeText(this, "Remote call error!", Toast.LENGTH_SHORT).show();
            }
        } else {
            Toast.makeText(this, "Service is not available yet!", Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.bind:
                Intent intent = new Intent(IRemoteService.class.getName());
                bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
                break;
            case R.id.unbind:
                unbindService(mServiceConnection);
                break;
            case R.id.call:
                callRemote();
                break;
        }
    }
}

在客户端,使用Context.bindService()函数,绑定到远程的Service。注意到这里的Intent intent = new Intent(IRemoteService.class.getName());,和上面的Service申明的Action一致。BIND_AUTO_CREATE这个Flag从名字就能看出,表示如果Bind的时候,如果还没有Service的实例,就自动创建。

这里有一个地方需要注意,在Android 5.0以后,就不允许使用非特定的Intent来绑定Service了,需要使用如下方法:

1
Intent intent = new Intent(IRemoteService.class.getName());
intent.setClassName("com.race604.remoteservice", "com.race604.remoteservice.RemoteService");
// 或者setPackage()
// intent.setPackage("com.race604.remoteservice");
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);

到这里就基本实现了一个完整的Client调用远程Service的实例了。


http://www.race604.com/communicate-with-remote-service-2/

这是系列文章《与Sevice实现双向通信》的第二篇。有了上一篇文章作为基础,本文实现一个稍微复杂一点的场景:

Service端实现一个控制中心(例如一个多人游戏),客户端可以随时加入,或者退出,每个客户端都可以获取当前参与进来的成员列表。

根据需求,在上一篇文章的代码的基础上,我们可以很容易申明如下接口:

1
// IRemoteService.aidl
package com.race604.servicelib;

interface IRemoteService {
    ...
    
    void join(String userName);
    void leave(String userName);
    List<String> getParticipators();
}

Service的实现也很简单,大致如下:

1
// RemoteService.java
package com.race604.remoteservice;

import ...

public class RemoteService extends Service {
  private List<String> mClients = new ArrayList<>();

  private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    @Override
    public void join(String name) throws RemoteException {
        mClients.add(name);
    }

    @Override
    public void leave(String name) throws RemoteException {
    	mClients.remove(name);
    }

    @Override
    public List<String> getParticipators() throws RemoteException {
    	return mClients;
    }
  };
...

这里的实现非常简单,看起来也没有问题。

客户端的实现我这里就不写了。我们期望Client调用join()leave()成对出现。在离开的时候,注意调用leave()

但是,考虑一个情况:客户端意外退出。例如客户端因为错误应用Crash,或者被Kill掉了。没有机会调用到leave()。这样Service中的mClients中的还保持这个客户端的信息,得不到释放。这里还是一个简单的例子,但是如果Service中如果为Client申请了一些资源,客户端意外退出以后,Service中资源得不到释放,会造成资源浪费。

幸运的是,Binder有可以让对端的进程得到意外退出通知的机制:Link-To-Death)。我这里以我们这里Service被通知Client意外退出的情况为例,实现的方法如下:

  1. Client传递一个Binder对象给Service,此Binder对象与Client的进程关联;
  2. 在Sevice中接受到这个Binder对象,并且使用binder.linkToDeath(),注册一个DeathRecipient回调;
  3. 实现DeathRecipient。当Client意外退出的时候,DeathRecipient.binderDied()将被回调,我们可以在这里释放相关的资源。

具体实现如下:
修改AIDL的定义如下:

1
// IRemoteService.aidl
package com.race604.servicelib;

interface IRemoteService {
    ...
    
    void join(IBinder token, String name);
    void leave(IBinder token);
    List<String> getParticipators();
}

注意到这里接口中传入了一个IBinder对象token,此就是客户端的唯一标示。

接下来重点看一下Service的实现。我们首先定义个类来保存Client的信息,如下:

1
private final class Client implements IBinder.DeathRecipient {
    public final IBinder mToken;
    public final String mName;

    public Client(IBinder token, String name) {
        mToken = token;
        mName = name;
    }

    @Override
    public void binderDied() {
        // 客户端死掉,执行此回调
        int index = mClients.indexOf(this);
        if (index < 0) {
            return;
        }

        Log.d(TAG, "client died: " + mName);
        mClients.remove(this);
    }
}

这里为了方便,因为每个IBinder都需要注册一个IBinder.DeathRecipient回调,我们就直接让Client实现此接口。

Service中保存客户端的信息也做如下修改:

1
private List<Client> mClients = new ArrayList<>();

// 通过IBinder查找Client
private int findClient(IBinder token) {
    for (int i = 0; i < mClients.size(); i++) {
        if (mClients.get(i).mToken == token) {
            return i;
        }
    }
    return -1;
}

然后修改join()的实现如下:

1
@Override
public void join(IBinder token, String name) throws RemoteException {
    int idx = findClient(token);
    if (idx >= 0) {
        Log.d(TAG, "already joined");
        return;
    }

    Client client = new Client(token, name);
    // 注册客户端死掉的通知
    token.linkToDeath(client, 0);
    mClients.add(client);
}

注意到这里的token.linkToDeath(client, 0);,表示的含义就是与token(IBinder对象)关联的客户端,如果意外退出,就会回调client.binderDied()方法。

同理leave()的实现如下:

1
@Override
public void leave(IBinder token) throws RemoteException {
    int idx = findClient(token);
    if (idx < 0) {
        Log.d(TAG, "already left");
        return;
    }

    Client client = mClients.get(idx);
    mClients.remove(client);

    // 取消注册
    client.mToken.unlinkToDeath(client, 0);
}

当调用leave的时候,释放相关资源,取消IBinder.DeathRecipient回调,即client.mToken.unlinkToDeath(client, 0);

客户端调用就比较简单了,主要代码如下:

1
package com.race604.client;

import ...

public class MainActivity extends ActionBarActivity {
    ...
    private IBinder mToken = new Binder();
    private boolean mIsJoin = false;
    
    private void toggleJoin() {
        if (!isServiceReady()) {
            return;
        }

        try {
            if (!mIsJoin) {
                String name = "Client:" + mRand.nextInt(10);
                mService.join(mToken, name);
                mJoinBtn.setText(R.string.leave);
                mIsJoin = true;
            } else {
                mService.leave(mToken);
                mJoinBtn.setText(R.string.join);
                mIsJoin = false;
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

至此,核心代码都实现了。完整的代码请参考这个Commit

我们做如下测试,在客户端join()后,然后在最近任务列表中,删除client应用,我们看到service端打印信息:

1
02-04 14:01:23.627: D/RemoteService(29969): client died: Client:6

可见,我们Kill掉客户端,回调到了这里:

1
private final class Client implements IBinder.DeathRecipient {
	...
    @Override
    public void binderDied() {
        // 客户端死掉,执行此回调
        ...
        Log.d(TAG, "client died: " + mName);
        ...
    }
}

参考文章:

  1. Binders & Death Recipients

这是系列文章《与Sevice实现双向通信》的第三篇,也是完结篇。通过前两篇介绍,我们基本已经实现了Client到Service的调用了。但是Service怎么反过来调用到我们的Client端呢?

我们接着上篇的例子,我们希望能够实现,当有用户加入(join())或者离开(leave())的时候,能够通知客户端。

实现方法是,客户端注册一个回调到Service中,当Service有用户加入或者离开的时候,就调用此回调。因为普通的interface对象不能通过AIDL注册到Service中,我们需要定义一个AIDL接口,如下:

1
// IParticipateCallback.aidl
package com.race604.servicelib;

interface IParticipateCallback {
    // 用户加入或者离开的回调
    void onParticipate(String name, boolean joinOrLeave);
}

同时,在IRemoteService.aidl中添加两个方法如下:

1
// IRemoteService.aidl
package com.race604.servicelib;

// 注意这里需要import
import com.race604.servicelib.IParticipateCallback;

interface IRemoteService {
    ...
    
    void registerParticipateCallback(IParticipateCallback cb);
    void unregisterParticipateCallback(IParticipateCallback cb);
}

这里需要注意的是,需要import com.race604.servicelib.IParticipateCallback;

我们可以先设想一下,需要在Service的实现中,用一个List来保存注册进来的IParticipateCallback实例。你肯定很快就想到上一篇中说的问题,如果客户端意外退出的话,需要从List列表中删掉对应的实例。否则不仅浪费资源,而且在回调的时候,会出现DeadObjectException。当然,我们可以使用上一篇文章中的方法,传入一个IBinder对象,使用Link-To-Death回调。

幸运的是,这是一个典型的应用场景,Android SDK提供一个封装好的对象:RemoteCallbackList,帮我自动处理了Link-To-Death的问题,这就帮我们剩下了很多代码了。

下面来看我们的RemoteService的实现:

1
package com.race604.remoteservice;

import ...

public class RemoteService extends Service {
    ...
    private RemoteCallbackList<IParticipateCallback> mCallbacks = new RemoteCallbackList<>();

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        ...
        @Override
        public void registerParticipateCallback(IParticipateCallback cb) throws RemoteException {
            mCallbacks.register(cb);
        }

        @Override
        public void unregisterParticipateCallback(IParticipateCallback cb) throws RemoteException {
            mCallbacks.unregister(cb);
        }
        
        @Override
        public void join(IBinder token, String name) throws RemoteException {
            ...
            // 通知client加入
            notifyParticipate(client.mName, true);
        }

        @Override
        public void leave(IBinder token) throws RemoteException {
        	...
            // 通知client离开
            notifyParticipate(client.mName, false);
        }
    };

}

我们在Service中用mCallbacks来保存回调列表,在注册和反注册IParticipateCallback回调的时候,只要调用mCallbacks.register(cb);mCallbacks.unregister(cb);即可。是不是非常容易?

然后我们来看怎么在用户加入或者退出的时候,怎么通知回调。上面的代码中,在join()leave()函数中分别都调用了notifyParticipate()函数,我们来看它的实现:

1
private void notifyParticipate(String name, boolean joinOrLeave) {
    final int len = mCallbacks.beginBroadcast();
    for (int i = 0; i < len; i++) {
        try {
        	// 通知回调
            mCallbacks.getBroadcastItem(i).onParticipate(name, joinOrLeave);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    mCallbacks.finishBroadcast();
}

这里我们也是使用一个循环,获取每个callback,然后调用onParticipate()。循环开始前,使用mCallbacks.beginBroadcast();,来准备开始通知Callbacks,此函数返回值是mCallbacks中回调对象个数。循环结束的时候,调用mCallbacks.finishBroadcast();来宣告完成。

另外,在Service销毁的时候,需要清除掉mCallbacks中的所有的对象,如下:

1
@Override
public void onDestroy() {
    super.onDestroy();
    // 取消掉所有的回调
    mCallbacks.kill();
}

客户端使用IParticipateCallback的方法,只要实现IParticipateCallback.Stub即可,如下:

1
package com.race604.client;

import ...
public class MainActivity extends ActionBarActivity {
	...
	private ArrayAdapter<String> mAdapter;
    private IParticipateCallback mParticipateCallback = new IParticipateCallback.Stub() {

        @Override
        public void onParticipate(String name, boolean joinOrLeave) throws RemoteException {
            if (joinOrLeave) {
                mAdapter.add(name);
            } else {
                mAdapter.remove(name);
            }
        }
    };
}

此时,如果有用户加入或者离开,客户端就能自动受到回调了。完整的代码请参考这个Commit

本系列文章的完整源代码托管在Github上,有需要的可以参考。

到这里,我们就实现了真正意义上的与Sevice实现双向通信。虽然写了三篇文章,但是其实也不是很复杂。本文只是涉及到Service的Binder应用。其实Binder机制是Android的精华之一,为了深入理解,可以学习一下其实现原理,可以参考大神罗升阳的《老罗的Android之旅》的系列博客,关于Binder,可以从这里开始看。

总结:

参考:

  1. Android Develops | RemoteMessengerServiceSample

















http://rainbow702.iteye.com/blog/1149790

前面写了 Bind Service 的两种实现方式,接下来转一篇贴子来简述一下最后一种实现方式
第三种:使用 AIDL

前面讲的使用 Messenger 技术,其实是基于 AIDL架构的,但是 Messenger 使用一个队列来处理所有的请求,这样一来,就无法进行多线程的并发了。所以,如果你想同时接受并处理多个 client 的请求,那么请使用 AIDL 来实现,但这样的话,你需要小心地进行同步处理了哦~
默认情况下,一个应用不管有多少个 Activity、Service 或其他组件,它们都是运行在一个进程上,但是我们可以安排 Service 运行一个新的进程上,但是不同进程之间应该如何通信呢?当需要在不同的进程之间传递对象时,应该怎么做呢?AIDL(Android Interface Definition Language) 便是解决这一问题的钥匙。
使用 AIDL 并不是难事,但是比较繁琐,并且一不小心容易出错。好在 Android Dev Guide 的 "AIDL" 章节(在 "Dev Guide " 左侧列表的最下面)对这个问题讲解非常详细,再结合 Android APIDemo 中的 Remote Service Binding 给出了的示例,这都给了开发者提供了非常到位的帮助。以下内容就是我结合二者,整理出来的笔记,力求真理,但能力有限,差错难免,请读者坚持自己的判断力,本文只供参考,且不可尽信。

一、使用 AIDL 实现 IPC
面对问题,应统揽全局,归纳阶段,划定步骤,共得出四大步,分列如下:
① 创建.aidl 文件
    在这个文件里定义 method 和 field
② 把 .aidl 文件加入到 makefile 中去
    如果使用 Eclipse 则 ADT 会帮你管理
③ 实现接口方法
    AIDL 编译器会根据接口生成一个用 Java 语言编写的interface,这个 interface 有一个抽象的内部类,名字为 Stub,你必须创建一个类,继承于它,并且实现 .adil 文件中所声明的方法
④ 公开接口给客户端
    如果创建的是 service,则应该继承自 Service,并且重载 Service.onBind() 返回实现接口的类的实例

这四个步骤在 Remote Service Binding 中均有所呈现,以下分开阐述。Remote Service Binding 共包含有两个 .java 文件,三个 .aidl 文件,物理结构比较简单,但是逻辑结构就不那么简单,以下用 Class Diagram 来展示其中的关系。



 

1、创建.aidl 文件
AIDL 语法简单,用来声明接口,其中的方法接收参数和返回值,但是参数和返回值的类型是有约束的,且有些类型是需要 import,另外一些则无需这样做。
AIDL 支持的数据类型划分为四类,第一类是 Java 编程语言中的基本类型,第二类包括 String、List、Map 和 CharSequence,第三类是其他 AIDL 生成的 interface,第四类是实现了 Parcelable 接口的自定义类。
其中,除了第一类外,其他三类在使用时均需要特别小心。
使用第二类时,首先需要明白这些类不需要 import,是内嵌的。其次注意在使用 List 和 Map 此二者容器类时,需注意其元素必须得是 AIDL 支持的数据类型,List 可支持泛型,但是 Map 不支持,同时另外一端负责接收的具体的类里则必须是 ArrayList 和 HashMap。
使用第三、四类时,需要留意它们都是需要 import 的,但是前者传递时,传递的是 reference,而后者则是 value。

在创建 .aidl 文件的过程中,应该注意一旦 method 有参数,则需注意在前面加上 in, out 或 inout,它们被称为 directional tag,但是对于基本类型的参数,默认就是 in,并且不能为其他值。

Remote Service Binding 共包括了三个 .aidl 文件,分别是IRemoteService.aidl、IRemoteServiceCallback.aidl、ISecondary.aidl,通过它们可以看到如何使用第一类和第三类的数据类型,稀罕的是,看不到第二类、第四类数据类型的使用,也没有看到 directional tag。

 

2、实现 Interface
AIDL 为你生成一个 interface 文件,文件名称和 .aidl 文件相同。如果使用 Eclipse 插件,则 AIDL 会在构建过程中自动运行,如果不使用插件,则需要先使用 AIDL。
生成的 interface 会包含一个抽象内部类 Stub,它声明了在 .aidl 文件里的所有方法。要想实现你在 .aidl 文件里定义的接口,就必须实现这个Stub类,如下:

Java代码   收藏代码
  1. /** 
  2.  * This implementation is used to receive callbacks from the remote 
  3.  * service. 
  4.  */  
  5. private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {  
  6.     /** 
  7.      * This is called by the remote service regularly to tell us about 
  8.      * new values.  Note that IPC calls are dispatched through a thread 
  9.      * pool running in each process, so the code executing here will 
  10.      * NOT be running in our main thread like most other things -- so, 
  11.      * to update the UI, we need to use a Handler to hop over there. 
  12.      */  
  13.     public void valueChanged(int value) {  
  14.         mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));  
  15.     }  
  16. };  

 

Stub 也定义了一些帮助方法,比较常用的有 asInterface(),其接收一个 IBinder 作为参数,并且返回一个 interface 的实例用来调用IPC方法。

Java代码   收藏代码
  1. private static INotificationManager sService;  
  2.    
  3. static private INotificationManager getService()  
  4. {  
  5.     if (sService != null) {  
  6.         return sService;  
  7.     }  
  8.     sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));  
  9.     return sService;  
  10. }  

 

 要实现 interface,需要继承 Stub,实现其方法,这在 RemoteService 和 RemoteServiceBinding 都可以找到相关代码。

这个环节是重中之重,需要特别小心的有两点,其一是抛出的所有异常均不会发给调用者;其二是IPC调用是同步的,这意味IPC服务一旦花费较长时间完成的话,就会引起ANR,应该将这样的操作放在单独的线程里。

 

3、向客户端公开 Interface
独乐乐不如众乐乐,需要将服务公开出去,要达成这个目的,须得创建一个 Service 的子类,并且实现 Service.onBind(Intent),通过这个方法将实现了接口的类的实例返回回来。通过查看 RemoteService 一目了然。

 

Java代码   收藏代码
  1. @Override  
  2. public IBinder onBind(Intent intent) {  
  3.     // Select the interface to return.  If your service only implements  
  4.     // a single interface, you can just return it here without checking  
  5.     // the Intent.  
  6.     if (IRemoteService.class.getName().equals(intent.getAction())) {  
  7.         return mBinder;  
  8.     }  
  9.     if (ISecondary.class.getName().equals(intent.getAction())) {  
  10.         return mSecondaryBinder;  
  11.     }  
  12.     return null;  
  13. }  

 

其中的 mBinder 和 mSecondaryBinder 分别是实现了 IRemoteService 和 ISecondary 接口的类的实例。

4、使用Parcelables传值
前文中提到 Remote Servcie Binding 没有使用第四类数据类型作为参数,这是示例的不足,要想让一个类变成第四类,需要遵照以下步骤:

① 引入 Parcelable 接口
② 实现 writeToParcel(Parcel out)
③ 增加一个静态的field,其实现 Parcelable.Creator 接口
④ 创建一个 .aidl 文件,用以声明你的 parcelables 类
在 "AIDL" 中,类 Rect 是一个不错的示例,弥补了 Remote Service Binding 的不足。

 

二、调用 IPC 方法
万事俱备,只欠东风,IPC 备妥,只待调用。在 Remote Service Binding 中,RemoteServiceBinding 正是 IPC 的调用者,既然要使用接口,那就先声明 interface 类型的变量

Java代码   收藏代码
  1. // The primary interface we will be calling on the service.   
  2. IRemoteService mService = null;   
  3. // Another interface we use on the service.   
  4. ISecondary mSecondaryService = null;   

 

实现 ServiceConnection,在 onServiceConnected(ComponentName className, IBinder service) 中完成对 mService 和 mSecondaryService 的赋值。

 

Java代码   收藏代码
  1. private ServiceConnection mConnection = new ServiceConnection() {  
  2.     public void onServiceConnected(ComponentName className,  
  3.             IBinder service) {  
  4.         mService = IRemoteService.Stub.asInterface(service);  
  5.         // ... 以下省略  
  6.     }  
  7. }  

 

接着别忘了调用 Context.bindService(),完成任务以后,调用 Context.unbindService()。如果在 connection 中断的情况下,调用 IPC 方法,你会遇到 DeadObjectException,这是 remote method 能抛出的唯一异常。

 

原文地址: http://www.poemcode.net/2010/05/aidl-ipc/


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值