如何应对Android面试官->进程通信,启动与获取SM服务

前言


image.png

本章主要围绕 ServiceManager 进行讲解,通过注册、启动等流程,玩转 ServiceManager;

ServiceManager 是什么?


客户端想要调用系统的 AMS 或者 PMS,但是客户端直接调用不到,只能获取到 AMS-IBinder 对象,AMS 在启动的时候就会把 AMS-IBinder 对象注册到 ServiceManager 中,客户端只要找到 ServiceManager 之后,就可以把 AMS-IBinder 对象返回给客户端,客户端就可以通过 AMS-IBinder 同 AMS 进行通信;

ServiceManager 它也是一个服务,但是它的句柄是确认的,也就是 handle = 0,而我们客户端在调用系统服务的时候,只需要调用这个已知道的 handle = 0 的服务就可以获取到 ServiceManager 的代理对象,然后就可以 同其他系统服务通信了;

ServiceManager 服务是如何注册的?


那么 ServiceManager 是怎么注册的呢?我们进入源码看一下,它的注册就在 init.rc 中:

// 打开 ServiceManager 这个程序,就会 调用 ServiceManager 的 main 方法
service servicemanager /system/bin/servicemanager
    class core
    user system
    group system
    critical
    onrestart restart healthd
    onrestart restart zygote
    onrestart restart media
    onrestart restart surfaceflinger
    onrestart restart drm

打开 ServiceManager 这个程序,就会 调用 servicemanager.c 的 main 方法,我们进入 main 方法看下:

int main(int argc, char **argv)
{
    struct binder_state *bs;
    // 这个方法做了两件事情,一是 open 方法,打开 binder 驱动,二是 mmap 方法,建立映射关系
    bs = binder_open(128*1024);
    if (!bs) {
        ALOGE("failed to open binder driver\n");
        return -1;
    }
    // 把 ServiceManager 设置成服务大管家
    if (binder_become_context_manager(bs)) {
        ALOGE("cannot become context manager (%s)\n", strerror(errno));
        return -1;
    }

    selinux_enabled = is_selinux_enabled();
    sehandle = selinux_android_service_context_handle();
    selinux_status_open(true);

    if (selinux_enabled > 0) {
        if (sehandle == NULL) {
            ALOGE("SELinux: Failed to acquire sehandle. Aborting.\n");
            abort();
        }

        if (getcon(&service_manager_context) != 0) {
            ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
            abort();
        }
    }

    union selinux_callback cb;
    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);
    cb.func_log = selinux_log_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);
    // 循环监听有没有其他服务访问
    binder_loop(bs, svcmgr_handler);

    return 0;
}

binder_open 这个方法做了两件事情,一是 open(128*1024) 方法,打开 binder 驱动,二是 mmap 方法,建立映射关系;

binder_become_context_manager 把 ServiceManager 设置成服务大管家;

binder_loop 循环监听有没有其他服务访问;

我们进入 binder_become_context_manager 这个方法看下,它是如何成为大管家的

int binder_become_context_manager(struct binder_state *bs)
{
    return ioctl(bs->fd, BINDER_SET_CONTEXT_MGR, 0);
}

直接调用的 ioctl 方法,传入的参数是 BINDER_SET_CONTEXT_MGR,而 这个方法最终调用到 binder_ioctl 中,我们进入 BINDER_SET_CONTEXT_MGR 这个 case 看下:

case BINDER_SET_CONTEXT_MGR:
    ret = binder_ioctl_set_ctx_mgr(filp);
    if (ret)
	goto err;
    break;

这个 case 中调用的是 binder_ioctl_set_ctx_mgr,我们进入这个方法看下:

static int binder_ioctl_set_ctx_mgr(struct file *filp)
{
	int ret = 0;
	struct binder_proc *proc = filp->private_data;
	struct binder_context *context = proc->context;

	kuid_t curr_euid = current_euid();
   // 先判断是不是已经存在了,存在则直接返回
	if (context->binder_context_mgr_node) {
		pr_err("BINDER_SET_CONTEXT_MGR already set\n");
		ret = -EBUSY;
		goto out;
	}
	ret = security_binder_set_context_mgr(proc->tsk);
	if (ret < 0)
		goto out;
   // 判断 sm 的 uid 是否是有效的,第一次肯定是无效的,所以直接进入 else
	if (uid_valid(context->binder_context_mgr_uid)) {
		if (!uid_eq(context->binder_context_mgr_uid, curr_euid)) {
			pr_err("BINDER_SET_CONTEXT_MGR bad uid %d != %d\n",
			       from_kuid(&init_user_ns, curr_euid),
			       from_kuid(&init_user_ns,
					 context->binder_context_mgr_uid));
			ret = -EPERM;
			goto out;
		}
	} else {
            // 设置 uid
            context->binder_context_mgr_uid = curr_euid;
	}
        // 通过 binder_new_node 创建 SM 的 binder_context_mgr_node 结构体
	context->binder_context_mgr_node = binder_new_node(proc, 0, 0);
	if (!context->binder_context_mgr_node) {
		ret = -ENOMEM;
		goto out;
	}
	context->binder_context_mgr_node->local_weak_refs++;
	context->binder_context_mgr_node->local_strong_refs++;
	context->binder_context_mgr_node->has_strong_ref = 1;
	context->binder_context_mgr_node->has_weak_ref = 1;
out:
	return ret;
}

到这里,就把 ServiceManager 设置成了大管家;

我们接下来看下 binder_loop 这个方法:

void binder_loop(struct binder_state *bs, binder_handler func)
{
    int res;
    // 结构体,这个结构体中有读和写,以及buffer
    struct binder_write_read bwr;
    uint32_t readbuf[32];

    bwr.write_size = 0;
    bwr.write_consumed = 0;
    bwr.write_buffer = 0;
    // BC_ENTER_LOOPER 是一个命令,进入循环的命令
    readbuf[0] = BC_ENTER_LOOPER;
    // 执行 binder_write 把 buffer 放进去,内部执行 ioctl 方法,最终调用到 binder_ioctl_write_read 方法
    binder_write(bs, readbuf, sizeof(uint32_t));

    for (;;) {
        bwr.read_size = sizeof(readbuf);
        bwr.read_consumed = 0;
        bwr.read_buffer = (uintptr_t) readbuf;

        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);

        if (res < 0) {
            ALOGE("binder_loop: ioctl failed (%s)\n", strerror(errno));
            break;
        }

        res = binder_parse(bs, 0, (uintptr_t) readbuf, bwr.read_consumed, func);
        if (res == 0) {
            ALOGE("binder_loop: unexpected reply?!\n");
            break;
        }
        if (res < 0) {
            ALOGE("binder_loop: io error %d %s\n", res, strerror(errno));
            break;
        }
    }
}

// 执行 binder_write 把 buffer 放进去,内部执行 ioctl 方法,最终调用到 binder_ioctl_write_read 方法,我们进入这个方法看下:

static int binder_ioctl_write_read(struct file *filp,
				unsigned int cmd, unsigned long arg,
				struct binder_thread *thread)
{
	int ret = 0;
	struct binder_proc *proc = filp->private_data;
	unsigned int size = _IOC_SIZE(cmd);
	void __user *ubuf = (void __user *)arg;
	struct binder_write_read bwr;

	if (size != sizeof(struct binder_write_read)) {
		ret = -EINVAL;
		goto out;
	}
	if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
		ret = -EFAULT;
		goto out;
	}
	binder_debug(BINDER_DEBUG_READ_WRITE,
		     "%d:%d write %lld at %016llx, read %lld at %016llx\n",
		     proc->pid, thread->pid,
		     (u64)bwr.write_size, (u64)bwr.write_buffer,
		     (u64)bwr.read_size, (u64)bwr.read_buffer);
    // 两个 if 来判断是要执行 read 还是 write 方法
	if (bwr.write_size > 0) {
		ret = binder_thread_write(proc, thread,
					  bwr.write_buffer,
					  bwr.write_size,
					  &bwr.write_consumed);
		trace_binder_write_done(ret);
		if (ret < 0) {
			bwr.read_consumed = 0;
			if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
				ret = -EFAULT;
			goto out;
		}
	}
	if (bwr.read_size > 0) {
		ret = binder_thread_read(proc, thread, bwr.read_buffer,
					 bwr.read_size,
					 &bwr.read_consumed,
					 filp->f_flags & O_NONBLOCK);
		trace_binder_read_done(ret);
		if (!list_empty(&proc->todo))
			wake_up_interruptible(&proc->wait);
		if (ret < 0) {
			if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
				ret = -EFAULT;
			goto out;
		}
	}
	binder_debug(BINDER_DEBUG_READ_WRITE,
		     "%d:%d wrote %lld of %lld, read return %lld of %lld\n",
		     proc->pid, thread->pid,
		     (u64)bwr.write_consumed, (u64)bwr.write_size,
		     (u64)bwr.read_consumed, (u64)bwr.read_size);
	if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
		ret = -EFAULT;
		goto out;
	}
out:
	return ret;
}

bwr.write_size > 0 执行 write 或者 bwr.read_size > 0 执行 read 操作,我们进入 write 的操作看下,这个方法最终执行的命令是我们前面看到的 BC_ENTER_LOOPER 这个命令,我们直接进入这个 case 看下

case BC_ENTER_LOOPER:
    binder_debug(BINDER_DEBUG_THREADS,
				     "%d:%d BC_ENTER_LOOPER\n",
				     proc->pid, thread->pid);
    if (thread->looper & BINDER_LOOPER_STATE_REGISTERED) {
        thread->looper |= BINDER_LOOPER_STATE_INVALID;
	binder_user_error("%d:%d ERROR: BC_ENTER_LOOPER called after BC_REGISTER_LOOPER\n",
	proc->pid, thread->pid);
    }
    // 其实就做了一件事情,给当前 thread 的 looper 设置了一个状态
    thread->looper |= BINDER_LOOPER_STATE_ENTERED;
    break;

其实就做了一件事情,给当前 thread 的 looper 设置了一个状态;

我们接着回到 binder_loop 方法往下看,下面一个是 for(;😉 无限循环,这里依然调用的是 res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr); 这个方法,只是 case 传递了 BINDER_WRITE_READ,我们继续进入这个方法看下,最终进入的是 binder_thread_read 方法,这个方法 最终调用到 ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread)); 进入等待状态了

if (wait_for_proc_work) {
	if (!(thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
					BINDER_LOOPER_STATE_ENTERED))) {
			binder_user_error("%d:%d ERROR: Thread waiting for process work before calling BC_REGISTER_LOOPER or BC_ENTER_LOOPER (state %x)\n",
				proc->pid, thread->pid, thread->looper);
			wait_event_interruptible(binder_user_error_wait,
						 binder_stop_on_user_error < 2);
	}
	binder_set_nice(proc->default_priority);
	if (non_block) {
		if (!binder_has_proc_work(proc, thread))
                    ret = -EAGAIN;
	} else {
            // binder 是阻塞的,所以最终进入到这里
           ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread));
        }
}

SM 的注册还是比较简单的,一共就三步,第一步是 binder_open,第二步是 binder_become_context_manager(成为大管家,也叫设置为守护进程),第三步是 binder_loop

image.png

整体就是图上 1 、2 、3 三个步骤;

ServiceManager 服务是如何获取的


我们接下来看下 SM 是如何获取的;获取 SM 的情况一般是两种,一种是注册服务到 SM,一种是通过 SM 去获取服务,这两种都需要拿到 SM 才能实现;SM 的 native 层获取一般是在 IServiceManager.cpp 中,我们进入这个文件看下

sp<IServiceManager> defaultServiceManager()
{
    if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
    
    {
        AutoMutex _l(gDefaultServiceManagerLock);
        while (gDefaultServiceManager == NULL) {
            gDefaultServiceManager = interface_cast<IServiceManager>(
                ProcessState::self()->getContextObject(NULL));
            if (gDefaultServiceManager == NULL)
                sleep(1);
        }
    }
    
    return gDefaultServiceManager;
}

native 层的获取 SM 都是从这里执行的,首先是一个单例,然后一个 while 循环来获取 SM,如果获取不到会进行一个休眠等待,主要的获取逻辑在下面这行代码

gDefaultServiceManager = interface_cast<IServiceManager>(
                ProcessState::self()->getContextObject(NULL));

self()

我们来逐步分析这个方法都做了什么,首先我们来看下 ProcessState::self()->getContextObject(NULL) 这个参数都做了什么操作;

首先是 self 方法,我们进入这个 self 方法看下做了什么

sp<ProcessState> ProcessState::self()
{
    Mutex::Autolock _l(gProcessMutex);
    if (gProcess != NULL) {
        return gProcess;
    }
    gProcess = new ProcessState;
    return gProcess;
}

很简单,就是 new ProcessState 并返回,创建这个 ProcessState 的时候做了什么呢?我们进入构造函数看一下

ProcessState::ProcessState()
    : mDriverFD(open_driver())
    , mVMStart(MAP_FAILED)
    , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
    , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
    , mExecutingThreadsCount(0)
    , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
    , mManagesContexts(false)
    , mBinderContextCheckFunc(NULL)
    , mBinderContextUserData(NULL)
    , mThreadPoolStarted{
        if (mDriverFD >= 0) {
        // XXX Ideally, there should be a specific define for whether we
        // have mmap (or whether we could possibly have the kernel module
        // availabla).
#if !defined(HAVE_WIN32_IPC)
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            // *sigh*
            ALOGE("Using /dev/binder failed: unable to mmap transaction memory.\n");
            close(mDriverFD);
            mDriverFD = -1;
        }
#else
        mDriverFD = -1;
#endif
    }

    LOG_ALWAYS_FATAL_IF(mDriverFD < 0, "Binder driver could not be opened.  Terminating.");
    }

第一步,是 open_driver() 通过 open 方法打开 binder 设备(驱动),并通过 maxThreads 设置最大线程数;

static int open_driver()
{
    int fd = open("/dev/binder", O_RDWR);
    if (fd >= 0) {
        fcntl(fd, F_SETFD, FD_CLOEXEC);
        int vers = 0;
        status_t result = ioctl(fd, BINDER_VERSION, &vers);
        if (result == -1) {
            ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
            close(fd);
            fd = -1;
        }
        if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
            ALOGE("Binder driver protocol does not match user space protocol!");
            close(fd);
            fd = -1;
        }
        size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
        result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
        if (result == -1) {
            ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
        }
    } else {
        ALOGW("Opening '/dev/binder' failed: %s\n", strerror(errno));
    }
    return fd;
}

我们接着看创建的逻辑,

mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);

通过 mmap 设置共享内存, 1M-8K(普通服务的大小);可以看到有一个常量参数 BINDER_VM_SIZE 它的值就是 #define BINDER_VM_SIZE ((110241024) - (4096 *2));

所以这个 self() 方法做了三件事情,分别是:

  1. 打开驱动,open_driver() 方法;
  2. 设置最大线程数据,15;
  3. mmap,设置共享内存大小,1M-8K;

getContextObject(NULL)

我们接下来看下 getContextObject(NULL) 做了些什么?这个方法直接调用了

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/)
{
    return getStrongProxyForHandle(0);
}

这个方法直接调用了 getStrongProxyForHandle(0) 传入了 0;我们进入这个方法看下都发生了什么?

sp<IBinder> ProcessState::getStrongProxyForHandle(int32_t handle)
{
    sp<IBinder> result;

    AutoMutex _l(mLock);
    // 查找 handle 对应的资源线
    handle_entry* e = lookupHandleLocked(handle);

    if (e != NULL) {
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {
                Parcel data;
                // 测试 binder 是否已经准备就绪
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }
            // 创建 BpBinder 并返回;
            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

这个方法就做了一件事情,就是创建 BpBinder,我们可以看下,在创建这个 BpBinider 的时候做了什么?

BpBinder


BpBinder::BpBinder(int32_t handle)
    : mHandle(handle)
    , mAlive(1)
    , mObitsSent(0)
    , mObituaries(NULL)
{
    ALOGV("Creating BpBinder %p handle %d\n", this, mHandle);

    extendObjectLifetime(OBJECT_LIFETIME_WEAK);
    // 主要是调用 incWeakHandle 做一个弱引用的计数
    IPCThreadState::self()->incWeakHandle(handle);
}

所以 ProcessState::self()->getContextObject(NULL) 这个参数本质就是返回 BpBinder 对象;这个 BpBinder 对象就是指向服务端的代理对象,我们在这里就可以认为是 ServiceManager;

image.png

BpBinder 是 BBinder 的代理对象,这里可以认为它俩是一样的;

Java 层和 Native 层通信的时候,需要一个 BinderProxy 和 ServiceManagerProxy,ServiceManagerProxy 里面有一个 BinderProxy,这个 BinderProxy 指向 BpBinder,也就是说 Java 层需要拿到这个 BpBinder 才能通信,Java 通过操作 ServiceManagerProxy,而 ServiceManagerProxy 通过操作 Remote(BinderProxy),这个时候,这个 BinderProxy 它是 Java 层的,它要指向 Native 层的才能通信,而这个 Native 层的就是 BpBinder,这个 BpBinder 就是代理对象;

Native 层也需要获取 BpBinder,但是它与 Java 层的获取方式不同,它是通过操作 BpServiceManager ,而 BpServiceManager 可以直接操作 BpBinder,这就少了一步,Java 层和 Native 层都是 BpBinder,再由 BpBinder 进行 transac 进行通信

interface_cast

我们接下来看下这个 interface_cast 方法,这个方法传入的参数就是 BpBinder;

template<typename INTERFACE>
inline sp<INTERFACE> interface_cast(const sp<IBinder>& obj)
{
    return INTERFACE::asInterface(obj);
}

这个方法调用的是 INTERFACE::asInterface(obj),这里是一个泛型操作,所以最终调用的是 IServiceManager 的 asInterface(obj) 方法; 但是这个方法并不在 IServiceManager 这个文件中,它使用的模板放进行了一个宏定义,我们在找下这个宏定义

#define DECLARE_META_INTERFACE(INTERFACE)                               \
    static const android::String16 descriptor;                          \
    static android::sp<I##INTERFACE> asInterface(                       \
            const android::sp<android::IBinder>& obj);                  \
    virtual const android::String16& getInterfaceDescriptor() const;    \
    I##INTERFACE();                                                     \
    virtual ~I##INTERFACE();

这里通过宏定义之后会进行一个替换,替换之后的 C++ 代码实现如下:

static const android::String16 descriptor;

static android::sp< IServiceManager > asInterface(const

android::sp<android::IBinder>& obj)

virtual const android::String16& getInterfaceDescriptor() const;

IServiceManager ();

virtual ~IServiceManager();

该过程主要是声明asInterface(),getInterfaceDescriptor()方法,所以这里我们不用过多的关心;

我们接着看下面的宏定义以及替换之后的逻辑

#define IMPLEMENT_META_INTERFACE(INTERFACE, NAME)                       \
    const android::String16 I##INTERFACE::descriptor(NAME);             \
    const android::String16&                                            \
            I##INTERFACE::getInterfaceDescriptor() const {              \
        return I##INTERFACE::descriptor;                                \
    }                                                                   \
    android::sp<I##INTERFACE> I##INTERFACE::asInterface(                \
            const android::sp<android::IBinder>& obj)                   \
    {                                                                   \
        android::sp<I##INTERFACE> intr;                                 \
        if (obj != NULL) {                                              \
            intr = static_cast<I##INTERFACE*>(                          \
                obj->queryLocalInterface(                               \
                        I##INTERFACE::descriptor).get());               \
            if (intr == NULL) {                                         \
                intr = new Bp##INTERFACE(obj);                          \
            }                                                           \
        }                                                               \
        return intr;                                                    \
    }                                                                   \
    I##INTERFACE::I##INTERFACE() { }                                    \
    I##INTERFACE::~I##INTERFACE() { }                                   \

替换之后的逻辑如下:

const android::String16
IServiceManager::descriptor(“android.os.IServiceManager”);
const android::String16& IServiceManager::getInterfaceDescriptor() const
{
    return IServiceManager::descriptor;
}

android::sp<IServiceManager> IServiceManager::asInterface(const
android::sp<android::IBinder>& obj)
{
    android::sp<IServiceManager> intr;
    if(obj != NULL) {
        intr = static_cast<IServiceManager *>(
        obj->queryLocalInterface(IServiceManager::descriptor).get());
        if (intr == NULL) {
            // 等价于 new BpServiceManager(BpBinder)
            intr = new BpServiceManager(obj);
        }
    }
    return intr;
}

IServiceManager::IServiceManager () {}
IServiceManager::~ IServiceManager() {}

展开之后,我们主要关心的就是 asInterface 方法,这个方法具体做了什么,我们来看下:

这里就是创建了 BpServiceManager,构造方法中传入的 BpBinder,我们来看下它是怎么使用 BpBinder 的;

BpServiceManager


BpServiceManager(const sp<IBinder>& impl)
        : BpInterface<IServiceManager>(impl)
    {
    }

我们来看下 BpInterface

template<typename INTERFACE>
class BpInterface : public INTERFACE, public BpRefBase
{
public:
                                BpInterface(const sp<IBinder>& remote);

protected:
    virtual IBinder*            onAsBinder();
};

这里我们直接看 BpInterface 的继承 BpRefBase

BpRefBase::BpRefBase(const sp<IBinder>& o)
    : mRemote(o.get()), mRefs(NULL), mState(0)
{
    extendObjectLifetime(OBJECT_LIFETIME_WEAK);

    if (mRemote) {
        mRemote->incStrong(this);           // Removed on first IncStrong().
        mRefs = mRemote->createWeak(this);  // Held for our entire lifetime.
    }
}

到此,我们就找到了 mRemote 它来操作 BpBinder,也就是对应的 java 层调用 mRemote.transact() 的时候,就是调用的 BpBinder.transact() 来进行通信;

所以这个方法也是做了三件事情

  1. new BpServiceManager(new BpBinder)
  2. remote.transcat() 远程调用
  3. remote = BpBinder

到此,ServiceManager 的注册与获取就写完了;

下一章预告


进程通信注册与获取服务及线程池讲解

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值