Android Binder框架实现之Parcel详解之read/writeStrongBinder实现

Android Binder框架实现之Parcel read/writeStrongBinder实现


Android Binder框架实现目录:

Android Binder框架实现之Binder的设计思想
Android Binder框架实现之何为匿名/实名Binder
Android Binder框架实现之Binder中的数据结构
Android Binder框架实现之Binder相关的接口和类
Android Binder框架实现之Parcel详解之基本数据的读写
Android Binder框架实现之Parcel read/writeStrongBinder实现
Android Binder框架实现之servicemanager守护进程
Android Binder框架实现之defaultServiceManager()的实现
Android Binder框架实现之Native层addService详解之请求的发送
Android Binder框架实现之Native层addService详解之请求的处理
Android Binder框架实现之Native层addService详解之请求的反馈
Android Binder框架实现之Binder服务的消息循环
Android Binder框架实现之Native层getService详解之请求的发送
Android Binder框架实现之Native层getService详解之请求的处理
Android Binder框架实现之Native层getService详解之请求的反馈
Android Binder框架实现之Binder Native Service的Java调用流程
Android Binder框架实现之Java层Binder整体框架设计
Android Binder框架实现之Framework层Binder服务注册过程源码分析
Android Binder框架实现之Java层Binder服务跨进程调用源码分析
Android Binder框架实现之Java层获取Binder服务源码分析



引言

  依然继承上篇博客Android Binder框架实现之Parcel详解之基本数据的读写未完成之意愿,继续分析讲解Parcel的高级用法如read/writeStrongBinder等相关方法的实现,让我们回忆回忆上一篇博客Parcel讲述了那些知识,重点归纳总结起来有如下几点:

  • Parcle的整体概述,以及在Android Binder框架中扮演的角色
  • Parcel数据结构容器类的获取,回收及销毁
  • Parcel数据结构容器类对基本数据的打包过程分析

有了前面的铺垫,对于接下来Parcel的read/writeStrongBinder等方法的实现逻辑理解操作起来就应该会轻松很多了。

  • 注意:本篇的介绍是基于Android 7.xx平台为基础的,其中涉及的代码路径如下:
framework/base/core/java/android/os/
  ---IInterface.java
  ---IServiceManager.java
  ---ServiceManager.java
  ---ServiceManagerNative.java(内含ServiceManagerProxy类)

framework/base/core/java/android/os/
  ---IBinder.java
  ---Binder.java(内含BinderProxy类)
  ---Parcel.java

framework/base/core/java/com/android/internal/os/
  ---BinderInternal.java

framework/base/core/jni/
  ---AndroidRuntime.cpp
  ---android_os_Parcel.cpp
  ---android_util_Binder.cpp
  
frameworks/native/libs/binder/BpBinder.cpp
frameworks/native/include/binder/IBinder.h
frameworks/native/libs/binder/Binder.cpp
frameworks/native/include/binder/Parcel.h
frameworks/native/libs/binder/Parcel.cpp
frameworks/base/core/jni/core_jni_helpers.h
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

  • Parcel通过JNI串联起Java层C++三者之间关系图如下:
    在这里插入图片描述



一. Parcel复杂数据结构读写

  我们知道Android Binder框架的实现中进行IPC通信时的数据载体是Parcel对象,即先将数据打包到Parcel中,然后借助Binder驱动传递到目的端(这个目的端不一定是服务端,也可以是客户端)。在博客中Android Binder框架实现之Framework层Binder服务注册过程源码分析中我们重点讲述了怎么将注册服务涉及的数据打包到Parcel中然后通过Binder驱动传递到servicemanager进程中,而在Android Binder框架实现之Java层获取Binder服务源码分析中我们重点分析了怎么将要查询的服务数据写入Parcel中借助Binder驱动向servicemanager进程中查询已经注册的服务,然后servicemanger进程将查询的结果打包到Parcel中传递回来供进程发起者查询并调用方法readStrongBinder读取会一个IBinder对象。

  可以看到上面两个Android Binder框架实现的重要架构都离不开Parcel的身影,Binder通信的发起端在将IPC数据打包到Parcel对象前,会首先获取一个Parcel对象,类似我们去邮局寄信件,首先需要从邮局获取信封,然后将信件装入到信封中,填写上收件人地址等就可以将信件发送出去。在Android的IPC通信中,Parcel对象就相当于信封,需要注册的服务相当于要邮寄的信件,handle就相当于收件人地址。

  好了,说了这么多Parcel在Android Binder框架中的重要性,我们也应该步入主题了,看看Android Parcel是怎么读取复杂对象的(我们主要关心的是读取IBinder),并且外加一个writeInterfaceToken方法。

//ServiceManagerNative.java
class ServiceManagerProxy implements IServiceManager {
    public void addService(String name, IBinder service, boolean allowIsolated)
            throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        //static final String descriptor = "android.os.IServiceManager",所以写入的字符串也是如此
        data.writeInterfaceToken(IServiceManager.descriptor);//详见章节1.1
        data.writeString(name);
		//最终等价于writeStrongBinder(new JavaBBinder(env, obj))
        data.writeStrongBinder(service);//详见章节1.2
        data.writeInt(allowIsolated ? 1 : 0);
		//成员变量mRemote指向BinderProxy
        mRemote.transact(ADD_SERVICE_TRANSACTION, data, reply, 0);
        reply.recycle();
        data.recycle();
    }
    
    public IBinder getService(String name) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken(IServiceManager.descriptor);
        data.writeString(name);
        mRemote.transact(GET_SERVICE_TRANSACTION, data, reply, 0);
        IBinder binder = reply.readStrongBinder();//详见章节1.3
        reply.recycle();
        data.recycle();
        return binder;
    }
}

1.1 writeInterfaceToken

//Parcel.java
private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName);
public final void writeInterfaceToken(String interfaceName) {
	//mNativePtr保存了C++层的Parcel对象,interfaceName为需要写入的数据内容
    nativeWriteInterfaceToken(mNativePtr, interfaceName);//详见章节1.1.1
}

  老规矩在正式开始该流程分析前,我们先奉上nativeWriteInterfaceToken整体式时序图,既是为了对后续流程的一个概述,也是一个小结:
在这里插入图片描述
  依然是老套路,通过JNI调用C++层的Parcel实现writeInterfaceToken的实现,我们继续接着分析。

1.1.1 android_os_Parcel_writeInterfaceToken
//android_os_parcel.cpp
static void android_os_Parcel_writeInterfaceToken(JNIEnv* env, jclass clazz, jlong nativePtr,
                                                  jstring name)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);//将nativePtr存储的地制值强制转换成Native层的Parcel对象
    if (parcel != NULL) {
        const jchar* str = env->GetStringCritical(name, 0);//将JNI中的jstring转换成jchar *
        if (str != NULL) {
            parcel->writeInterfaceToken(String16(
                  reinterpret_cast<const char16_t*>(str),
                  env->GetStringLength(name)));//详见章节1.1.2
            env->ReleaseStringCritical(name, str);//释放str的引用
        }
    }
}

   这里通过前面JNI的动态注册我们找到了nativeWriteInterfaceToken的本地方法实现函数android_os_Parcel_writeInterfaceToken,可以看到其逻辑比较简单,主要如下:

  • 将传递过来的参数nativePtr中存储的地址值强制转换为C++层的Parcel对象实例
  • 获取传递过来的参数name的字符串
  • 调用C++层的Parcel实例的函数writeInterfaceToken进行下一步操作
  • 调用 env->ReleaseStringCritical释放前面获取的字符串
1.1.2 Parcel::writeInterfaceToken
//IPCThreadState.cpp
IPCThreadState::IPCThreadState()
    : mProcess(ProcessState::self()),
      mMyThreadId(gettid()),
      mStrictModePolicy(0),
      mLastTransactionBinderFlags(0)
{
    pthread_setspecific(gTLS, this);
    clearCaller();
    mIn.setDataCapacity(256);
    mOut.setDataCapacity(256);
}

//Parcel.cpp
#define STRICT_MODE_PENALTY_GATHER (0x40 << 16)
status_t Parcel::writeInterfaceToken(const String16& interface)
{
    //writeInt32在前面的博客Parcel读取基本数据中已经有介绍过了,getStrictModePolicy()函数将返回IPCThreadState成员变量mStrictModePolicy的值,在构造IPCThreadState实例对象时,mStrictModePolicy被赋值为0了,如上代码所示
    writeInt32(IPCThreadState::self()->getStrictModePolicy() |
               STRICT_MODE_PENALTY_GATHER);
    
    
    return writeString16(interface);
}

status_t Parcel::writeString16(const String16& str)
{
    return writeString16(str.string(), str.size());//详见章节1.1.4
}

   writeInt32在前面的博客Parcel读取基本数据中已经有介绍过了,getStrictModePolicy()函数将返回IPCThreadState成员变量mStrictModePolicy的值,在构造IPCThreadState实例对象时,mStrictModePolicy被赋值为0了,如上代码所示

1.1.3 Parcel::writeInt32
//Parcel.cpp
status_t Parcel::writeInt32(int32_t val)
{
    return writeAligned(val);
}
 
template<class T>
status_t Parcel::writeAligned(T val) {
    COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T));
    //判断Parcel容器是否已经写满
    if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
        //将数据长度写入到mData+mDataPos的位置
        *reinterpret_cast<T*>(mData+mDataPos) = val;
		//调整mDataPos的位置
        return finishWrite(sizeof(val));
    }
    //如果数据已经写满,则增大容器容量
    status_t err = growData(sizeof(val));
	//重新写入数据
    if (err == NO_ERROR) goto restart_write;
    return err;
}

   这里没有啥好说的了,记住经过如上操作以后,Parcel实例对象中各个变量的值为如下:

  • mData:它的第0~3个字节保存了int32_t类型的数据STRICT_MODE_PENALTY_GATHER。
  • mDataPos:值为4,即下一个写入mData中的数据从第4个字节开始。
  • mDataSize:值为4,即mData中数据的大小。
  • mDataCapacity:值为6,即mData的数据容量为6字节。

此时此时,mData的数据如下图所示:
在这里插入图片描述

1.1.4 Parcel::writeString16

   我们接着继续分析writeString16,可以看到它会将要写入的字符串和字符串长度作为参数传递过去

//Parcel.cpp
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
    if (str == NULL) return writeInt32(-1);
	//将字符串长度写入Parcel中
    status_t err = writeInt32(len);
    if (err == NO_ERROR) {
        len *= sizeof(char16_t);//重新计算len的长度
        //在将字符串写入之前,增加mData的容量
        uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
        if (data) {
        	//将字符串拷贝到mData中
            memcpy(data, str, len);
            //字符串结束符
            *reinterpret_cast<char16_t*>(data+len) = 0;
            return NO_ERROR;
        }
        err = mError;
    }
    return err;
}

   让我们回到writeInterfaceToken继续分析writeString16,此时会调用重载的writeString16函数,下面我们来逐步分析:

  • writeString16(str, len)中,str=“android.os.IServiceManager”;len是由str.size()得来,虽然这里的字符串是String16类型(即每个字符占2个字节),但是str.size()是获取str中有效数据的个数(不包含字符串结束符),因此,len=26。
  • 首先调用writeInt32(len)将字符串的长度写入到Parcel中,writeInt32()在前面已经介绍过了。当再次写入int32_t类型的数据时,数据容量不够,会再次增长为12,即mDataCapacity=12;而写入int32_t类型的数据之后,mDataPos和mDataSize都增长为8。 此时,mData的数据如下图所示:
    在这里插入图片描述

在调用writeInt32(len)写入数据长度之后,再重新计算len的值为52,接着通过writeInplace()写入数据,继续分析。

1.1.5 Parcel::writeInplace
//Parcel.cpp
#define PAD_SIZE_UNSAFE(s) (((s)+3)&~3)
static size_t pad_size(size_t s) {
    if (s > (SIZE_T_MAX - 3)) {
        abort();
    }
    return PAD_SIZE_UNSAFE(s);
}
void* Parcel::writeInplace(size_t len)
{
    if (len > INT32_MAX) {
        return NULL;
    }
	//4字节对齐
    const size_t padded = pad_size(len);
    if (mDataPos+padded < mDataPos) {
        return NULL;
    }

    if ((mDataPos+padded) <= mDataCapacity) {
restart_write:
        uint8_t* const data = mData+mDataPos;

        // 如果padded!=len,则根据大端法还是小端法进行地址对齐设置。
        if (padded != len) {
			...
        }

        finishWrite(padded);
        return data;
    }

    status_t err = growData(padded);
    if (err == NO_ERROR) goto restart_write;
    return NULL;
}

   重点备注一下此时的入参len为54而不是52这个地方不要弄错了,至于为什么大家可以从代码里面查找到原因。下面接着继续分析代码:

  • pad_size()是4字节对齐的宏,所以pad_size(54)计算后的结果是56
  • 函数的初始值为padded=56,mDataPos=8,mDataCapacity=12。因此,会先调用growData(padded)来增加数据容量。growData()在前面已经介绍过;此时,它会将容量mDataCapacity增加至96
  • 接着会跳转到restart_write标签处,然后调用finishWrite(padded)来更新mDataPos和mDataSize。
  • 至此,writeInplace()就分析完了,它的作用就是增加mData的容量,并返回即将写入数据的地址。接着,我们继续回到1.1.4章节writeString16章节中,执行mmap(data, str, len)将数据拷贝到mData中;拷贝完毕之后,设置字符串的结束符为0
//Parcel.cpp
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
    if (str == NULL) return writeInt32(-1);
	//将字符串长度写入Parcel中
    status_t err = writeInt32(len);
    if (err == NO_ERROR) {
        len *= sizeof(char16_t);//重新计算len的长度
        //在将字符串写入之前,增加mData的容量
        uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
        if (data) {
        	//将字符串拷贝到mData中
            memcpy(data, str, len);
            //字符串结束符
            *reinterpret_cast<char16_t*>(data+len) = 0;
            return NO_ERROR;
        }
        err = mError;
    }
    return err;
}

writeInterfaceToken(IServiceManager::getInterfaceDescriptor())就分析完了。此时让我们来捋一捋Parcel已经写入的数据,其中mData中数据如下图所示:
在这里插入图片描述

1.1.6 writeInterfaceToken小结

   好了至此writeInterfaceToken分析完毕了,它从整体上可以划分为writeInt32和writeString16两个小部分。我们假设Parcel在内存中是以类似数组的存储空间进行存储的(这个只是假设,实际并不是如此),那么此时各个C_Parcel的位置相关的变量值的逻辑如下所示(是不是觉得Parcel还是很简单吗(到这里还是很简单,待分析完成了IBinder的打包和传输你就不会这么说了!),这里需要重点说明的是mData的值肯定不为0,因为内存中的0地址是给系统预留的,但是mDataPos的值是从0开始增长起来的。

在这里插入图片描述


1.2 Parcel.writeStrongBinder写入Binder实体的实现

   梦里寻他千百度那人却在灯火阑珊处,终于到了激动人心的时刻,是时候来攻破writeStrongBinder这块硬骨头了。

//Parcel.java
private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
 public final void writeStrongBinder(IBinder val) {
     nativeWriteStrongBinder(mNativePtr, val);
 }

在正式开始该流程分析前,我们先奉上writeStrongBinder整体式时序图,既是为了对后续流程的一个概述,也是一个小结:

在这里插入图片描述

1.2.1 android_os_Parcel_writeStrongBinder
//android_os_Parcel.cpp
//注意此处的入参object是Framework层Binder服务实体
 static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);//强制将nativePtr转换成Parcel对象,这个见怪不怪了
    if (parcel != NULL) {
		//等价于parcel->writeStrongBinder(new JavaBBinder(env, obj));
		/**
		*我只能说此处代码短小精悍,力量无穷啊,此处的代码逻辑分为两步
		*第一:调用ibinderForJavaObject创建JavaBBinder
		*第二:调用C++层的Parcel对象函数writeStrongBinder写入JavaBBinder
		*/
        const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));
        if (err != NO_ERROR) {
            signalExceptionForError(env, clazz, err);
        }
    }
}

   谷歌的Binder是设计思想是如此的巧妙,可是为什么代码逻辑排版就不能好好的整理一下呢!android_os_Parcel_writeStrongBinder函数逻辑看着比较简单,也就干了两件事情(但是分析起来你懂的):

  • 调用ibinderForJavaObject创建JavaBBinder
  • 调用C++层的Parcel对象函数writeStrongBinder写入JavaBBinder
1.2.2 Binder服务实体端类的继承关系和初始化

   Binder实体从层级关系来看又可以分为Java层的和C++层的。这里我们先简单梳理一下这两者的类图关系,及其二者之间的联系。

C++层Binder服务实体端类的关系图可以用如下示意图表示:
在这里插入图片描述
Java层Binder服务实体端类的关系图可以用如下示意图表示:
在这里插入图片描述
这里我们以Framework层Binder服务AMS注册到servicemanager中为例来说明writeStrongBinder的实现,我们看看其继承和实现关系(满足上述图示的Java层的Binder继承关系),如下:

//ActivityManagerService.java 
public final class ActivityManagerService extends ActivityManagerNative
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
        ...
}

//ActivityManagerNative.java
//这里的父类Binder是重点
public abstract class ActivityManagerNative extends Binder implements IActivityManager
{
	...
}

平谈无奇,没有啥重点!那还能咋样呢,继续往下分析Binder.java的构造方法,逻辑如下:

//Binder.java
private native final void init();
public class Binder implements IBinder {
	    public Binder() {
        init();

		...
    }
}

又是老套路,调用到了JNI层的函数,如下所示:

//android_util_Binder.cpp
static void android_os_Binder_init(JNIEnv* env, jobject obj)
{
    JavaBBinderHolder* jbh = new JavaBBinderHolder();//构建一个JavaBBinderHolder对象
    if (jbh == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
        return;
    }
    ALOGV("Java Binder %p: acquiring first ref on holder %p", obj, jbh);
	//增加引用计数
    jbh->incStrong((void*)android_os_Binder_init);
	/*
	*将JavaBBinderHolder对象保存在gBinderOffsets.mObject中,此时的gBinderOffsets.mObject已经在Zygote启动中和Binder.java中的mObject绑定了
	*/
    env->SetLongField(obj, gBinderOffsets.mObject, (jlong)jbh);
}

这个函数还干了一点事情,捯饬捯饬来说可以分为两步:

  • 构建一个JavaBBinderHolder对象
  • 将JavaBBinderHolder对象保存在gBinderOffsets.mObject中,此时的gBinderOffsets.mObject已经在Zygote启动中和Binder.java中的mObject绑定了

此时的你我想会有一个疑问就是gBinderOffsets这些的初始化不都是在Zygote进程中吗,我注册AMS服务是在system_server进程中进行的,这个gBinderOffsets能用不。我只能说孩子不要忘了你来自那里,当然不是天上了,那怕你贵为system_server进程你不也是Zygote进程孵化出来的,我们的Zygote进程也是无私的你继承了其所有也包括上面初数化的相关变量空间。

最后我要说一句,真的,我不是闲的蛋疼才加上上面的描述的,更加不是为了凑字数的,再说也没有稿费可骗啊!真的你们要相信我,接着继续分析ibinderForJavaObject。

1.2.3 ibinderForJavaObject
//android_os_Parcel.cpp
//此时的入参obj为Binder.java实例对象
sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj)
{
    if (obj == NULL) return NULL;
	//这里的IsInstanceOf和Java中的instanceof方法有点类似
    if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {//检测是否是Java层Binder类或者子类
        JavaBBinderHolder* jbh = (JavaBBinderHolder*)
            env->GetLongField(obj, gBinderOffsets.mObject);//以Binder(Java层)为参数生成JavaBBinderHolder(C++层)
        return jbh != NULL ? jbh->get(env, obj) : NULL;
    }

    if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) {//检测是否是Java层BinderProxy类或者子类
        return (IBinder*)
            env->GetLongField(obj, gBinderProxyOffsets.mObject);
    }

    ALOGW("ibinderForJavaObject: %p is not a Binder object", obj);
    return NULL;
}

   函数首先判断需要转换的是Java层的Binder对象还是BinderProxy对象,如果是Binder对象,则取出在构造服务对象时创建的JavaBBinderHolder对象,如果该对象不为空,则调用JavaBBinderHolder对象的get函数来获取JavaBBinder对象,相反则返回空;如果是BinderProxy对象,则取出该BinderProxy对应的C++层的BpBinder对象。这里传进来的是一个服务对象AMS,属于Binder对象,且在在构造该服务时已经创建了JavaBBinderHolder对象,因此此时取出来的对象不为空,通过get函数获取JavaBBinder对象。这里的JNI提供的函数IsInstanceOf和Java层的instanceof方法类似即判断两个对象是否属于同一个类或者及其子类。

1.2.4 JavaBBinderHolder类初始化
//android_util_Binder.cpp,又是在这个文件里面,你干的活还真多
class JavaBBinderHolder : public RefBase
{
public:
	...

    sp<JavaBBinder> getExisting()
    {
        AutoMutex _l(mLock);
        return mBinder.promote();
    }

private:
    Mutex           mLock;
    wp<JavaBBinder> mBinder;//注意这里的指针是wp类型的,即C++中的弱引用,可能会被回收
};

   JavaBBinderHolder它是如此朴实无华,没有构造函数,既然你这么绝情也不要怪我无意,老子不分析你了。

1.2.5 JavaBBinderHolder类初始化
	//android_util_Binder.cpp
    sp<JavaBBinder> get(JNIEnv* env, jobject obj)
    {
        AutoMutex _l(mLock);
        sp<JavaBBinder> b = mBinder.promote();//判断是会被回收或者已经创建了
        if (b == NULL) {
            b = new JavaBBinder(env, obj);//创建JavaBBinder,注意参数obj为Binder.java实例对象,详见3.6.7
            mBinder = b;
            ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%" PRId32 "\n",
                 b.get(), b->getWeakRefs(), obj, b->getWeakRefs()->getWeakCount());
        }

        return b;
    }

   通过前面的章节我们知道JavaBBinderHolder有一个名为mBinder的JavaBBinder对象的弱指针(C++层),mBinder是否被重新赋值的关键有两点第一其是否是第一次进入,第二由于mBinder是一个wp类型弱引用所以可能被系统垃圾回收机制回收,所以每次使用它的时候必须先行判断一下。

1.2.6 JavaBBinder
//android_util_Binder.cpp
class JavaBBinder : public BBinder
{
public:
    JavaBBinder(JNIEnv* env, jobject object)
        : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object))
    {
        ALOGV("Creating JavaBBinder %p\n", this);
        android_atomic_inc(&gNumLocalRefs);
        incRefsCreated(env);
    }
}

   这里注意JavaBBinder的父类为BBinder,并且以传递过来的参数object构建一个全局的mObject引用(object为Java层的Binder.java)。

到这里我们的parcel->writeStrongBinder(ibinderForJavaObject(env, object))源码就等价于下面的转换了(等价交换),及最终写入的是C++层的JavaBBinder对象。

parcel->writeStrongBinder(ibinderForJavaObject(env, object)) = parcel->writeStrongBinder(new JavaBBinder(env, obj));

1.2.7 JavaBBinder JavaBBinderHolder Binder关系小结

   分析至此,各位小伙们是不是对于JavaBBinder (C++),JavaBBinderHolder(C++),Binder(Java)三者之间的关系有理不断剪还乱的感觉!真的不要怪Android的妈咪谷歌,它这么做也是为了世界和平!这里我们可以使用下述的示意图来表述三者之间的关系:
在这里插入图片描述
上述三者是怎么关勾搭上的呢(不,关联上的),这要回到3.6.4 ibinderForJavaObject说起了:

  • 在该函数中首先将Binder.java构造方法创建的JavaBBinderHolder(C++)捯饬出来,
  • 接着创建一个JavaBBinder对象,并将传递过来的Framework层的Binder服务实例对象作为参数创建Java层服务Binder对象的全局引用,并将其保存到JavaBBinder对象的mObject变量中
  • 返回创建的JavaBBinder对象实例

是不是还是有点一脸蒙蔽,但是既然现在三者之间的关系已经创建既成事实了,那么就着这三者之间的关系再来捋一捋:

  • Android Framework层的Binder服务必须继承于Binder类,而我们的Binder对象在构造服务时,会首先在C++层构造一个JavaBBinderHolder对象,并将该对象的指针保存到Java层的服务的mObject变量中,即建立起了Binder(Java)到JavaBBinderHolder(C++)之间的关系了

  • 而我们的C++层的JavaBBinderHolder对象通过成员变量mBinder指向一个C++层的JavaBBinder对象,JavaBBinder类继承于BBinder类,是服务在C++层的表现形式,此时建立起了JavaBBinderHolder(C++)和JavaBBinder(C++)之间的关系了

  • 而我们的C++层的JavaBBinder对象又通过成员变量mObject指向Java层的Binder服务对象,即建立起了Binder(Java)和JavaBBinder(C++)之间的关联了

对于这三者之间的关系,我只想说一句话造孽啊!


1.3 Parcel::writeStrongBinder写入Binder实体的实现

//Parcel.cpp
status_t Parcel::writeStrongBinder(const sp<IBinder>& val)//注意这里的入参val为JavaBBinder
{
    return flatten_binder(ProcessState::self(), val, this);//详见3.7.1
}

  该函数没有多说的直接调用flatten_binder写将入参参数JavaBBinder写入C++层Parcel容器中,我们看看怎么通过flatten_binder对JavaBBinder进行扁平化处理,还记得我们再前面博客Android Binder框架实现之Parcel详解之基本数据的读写中将复杂数据拍扁传输吗,说的就是这个阶段。

在这里插入图片描述

1.3.1 flatten_binder扁平化处理
//Parcel.cpp
status_t flatten_binder(const sp<ProcessState>& /*proc*/,
    const sp<IBinder>& binder, Parcel* out)
{
    flat_binder_object obj;

    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
    if (binder != NULL) {
        IBinder *local = binder->localBinder();
        if (!local) {
            BpBinder *proxy = binder->remoteBinder();
            if (proxy == NULL) {
                ALOGE("null proxy");
            }
            const int32_t handle = proxy ? proxy->handle() : 0;
            obj.type = BINDER_TYPE_HANDLE;//远程Binder
            obj.binder = 0; /* Don't pass uninitialized stack data to a remote process */
            obj.handle = handle;//记录Binder代理的句柄
            obj.cookie = 0;
        } else {
            obj.type = BINDER_TYPE_BINDER;//本地Binder进入该分支
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
            obj.cookie = reinterpret_cast<uintptr_t>(local);//记录Binder实体的指针
        }
    } else {
        obj.type = BINDER_TYPE_BINDER;
        obj.binder = 0;
        obj.cookie = 0;
    }

    return finish_flatten_binder(binder, obj, out);
}

  我们这里的入参binder为JavaBBinder对象实例,我们回忆回忆其继承关系,其关系如下

IBinder(C++) ----> BBinder(C++) ---> JavaBBinder(C++)

而我们的BBinder的localBinder函数实现如下:

//Binder.cpp
BBinder* BBinder::localBinder()
{
    return this;
}

所以精简过后的flatten_binder函数如下:

//Parcel.cpp
status_t flatten_binder(const sp<ProcessState>& /*proc*/,
    const sp<IBinder>& binder, Parcel* out)
{
    flat_binder_object obj;

    obj.flags = 0x7f | FLAT_BINDER_FLAG_ACCEPTS_FDS;
    if (binder != NULL) {
        IBinder *local = binder->localBinder();
        if (!local) {
			...
        } else {
            obj.type = BINDER_TYPE_BINDER;//本地Binder进入该分支
            obj.binder = reinterpret_cast<uintptr_t>(local->getWeakRefs());
            obj.cookie = reinterpret_cast<uintptr_t>(local);//记录Binder实体的指针
        }
    } else {
		...
    }

    return finish_flatten_binder(binder, obj, out);//详见章节3.7.2
}

此时重要大哥flat_binder_object 要上场了,我们可以将其理解为它是用来专门描述BBinder对象的,将其带入实际代码,会得到如下的逻辑:

  • flat_binder_object结构体变量obj其成员type被赋值为BINDER_TYPE_BINDER,即表示此时的obj是一个BIndere实体对象
  • flat_binder_object结构体变量obj其成员binder记录Binder弱引用指针地址
  • flat_binder_object结构体变量obj其成员cookie 记录Binder实体指针地址

此时此刻关于flatten_binder扁平化的分析已经分析完毕了,此时flat_binder_object结构体中数据的映射关系如下:

在这里插入图片描述

1.3.2 finish_flatten_binder写入扁平化BInder数据
//Parcel.cpp
inline static status_t finish_flatten_binder(
    const sp<IBinder>& /*binder*/, const flat_binder_object& flat, Parcel* out)
{
    return out->writeObject(flat, false);
}

  接着往下干,看看Parcel是怎么将扁平化后的BInde实体类对象写入内存存储空间的。

1.3.3 Parcel::writeObject
status_t Parcel::writeObject(const flat_binder_object& val, bool nullMetaData)
{
    const bool enoughData = (mDataPos+sizeof(val)) <= mDataCapacity;
    const bool enoughObjects = mObjectsSize < mObjectsCapacity;
    if (enoughData && enoughObjects) {
restart_write:
        *reinterpret_cast<flat_binder_object*>(mData+mDataPos) = val;
		...
		
        // val.binder非空
        if (nullMetaData || val.binder != 0) {
        	//将地址偏移位置保存到mObjects[0]中
            mObjects[mObjectsSize] = mDataPos;
            acquire_object(ProcessState::self(), val, this, &mOpenAshmemSize);
            // 增加mObjectsSize的值
            mObjectsSize++;
        }

        return finishWrite(sizeof(flat_binder_object));
    }

    if (!enoughData) {
        const status_t err = growData(sizeof(val));
        if (err != NO_ERROR) return err;
    }
    if (!enoughObjects) {
    	//增加容量
        size_t newSize = ((mObjectsSize+2)*3)/2;
        //分配内存
        if (newSize*sizeof(binder_size_t) < mObjectsSize) return NO_MEMORY;   
        //动态分配一个newSize*sizeof(binder_size_t)大小的内存空间,并把内存空间的首地址给mObjects,并把mObjects指向的内存空间调整为newSize*sizeof(binder_size_t)
        binder_size_t* objects = (binder_size_t*)realloc(mObjects, newSize*sizeof(binder_size_t));
        if (objects == NULL) return NO_MEMORY;
        //设置mObjects的内存起始地址
        mObjects = objects;
        //设置mObjects对象的容量
        mObjectsCapacity = newSize;
    }

    goto restart_write;
}

status_t Parcel::finishWrite(size_t len)
{
    if (len > INT32_MAX) {
        return BAD_VALUE;
    }

    mDataPos += len;
    ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
    if (mDataPos > mDataSize) {
        mDataSize = mDataPos;
        ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
    }
    return NO_ERROR;
}

  说实话脱离实际的案例带入,直接分析上面的代码有点尬,这里我们假设此时我们是通过Parcel写入Binder实体实例对象,之前没有写入任何的数据进入(实际使用中不会存在此种情况,这里只是为了方便博客的继续),那么此时Parcel里面各个变量的值,此时,mDataPos=0, sizeof(val)=32, mDataCapacity=0,让我们带着这些数据来分析这段代码:

  • 根据此时Parcel里面数据数值计算,因此,enoughData=false。mObjectsSize和mObjectsCapacity的初始值=0,因此,enoughObjects=false。
  • 首先,执行if(!enoughData)部分,通过growData()将数据的容量增加至48(1.5倍)。即mDataCapacity=48。
  • 接着,执行if(!enoughObjects)部分,该部分的目的是分配对象空间,并修改mObjects和mObjectsCapacity的值。增加之后的容量mObjectsCapacity=3。
  • 然后,跳转到restart_write标签处。 reinterpret_cast<flat_binder_object>(mData+mDataPos) = val是保存val对象到mDataPos+mDataPos所指的地址中。
  • mObjects[mObjectsSize]=mDataPos,此处的mObjectsSize=0;这里是将对象的地址偏移mDataPos保存到mObjects[0]中。随后执行mObjectsSize++增加mObjectsSize的值为1。
  • 最后,调用finishWrite()更新mDataPos和mDataSize的值。

至此,data.writeStrongBinder()就分析完了。下面我们来捋一捋在将writeStrongBinder写入data之后,Parcel的数据如下图所示:

在这里插入图片描述


1.3 writeStrongBinder小结

  好了writeStrongBinder分析完毕了,我们假设Parcel在内存中是以类似数组的存储空间进行存储的(这个只是假设,实际并不是如此),那么此时各个C_Parcel的位置相关的变量值的逻辑如下所示这里需要重点说明的是mData的值肯定不为0,因为内存中的0地址是给系统预留的,但是mDataPos的值是从0开始增长起来的。

在这里插入图片描述

  并且我们在writeStrongBinder的源码分析过程中涉及到了Binder(Java),JavaBBinder(C++),flat_binder_object(C++)之间的相互转换关系,可以用如下的图示来表示:

在这里插入图片描述


1.4 readStrongBinder读取Binder代理端的实现

	//Parcel.java
	private static native IBinder nativeReadStrongBinder(long nativePtr);
    public final IBinder readStrongBinder() {
        return nativeReadStrongBinder(mNativePtr);//详见章节1.4.1
    }

  在正式开始该流程分析前,我们先奉上readStrongBinder整体式时序图,既是为了对后续流程的一个概述,也是一个小结:

在这里插入图片描述

  依然是老配方和老套路,通过JNI调用到Native层进行相关的处理。这里的readStrongBinder可以认为是前面注册服务中writeStrongBinder的逆向过程。

1.4.1 android_os_Parcel_readStrongBinder
//android_os_Parcel.cpp
static jobject android_os_Parcel_readStrongBinder(JNIEnv* env, jclass clazz, jlong nativePtr)
{
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
    if (parcel != NULL) {
    	//这里的javaObjectForIBinder是不是很熟悉,在注册服务中我们有讲述到主要是将BpBinder(C++)转换成Java层的BinderProxy
    	//我们这里只重点关注parcel->readStrongBinder()
        return javaObjectForIBinder(env, parcel->readStrongBinder());//详见章节1.4.2
    }
    return NULL;
}

  此处android_os_Parcel_readStrongBinder函数的逻辑可以分为两层:

1.4.2 Parcel::readStrongBinder
status_t Parcel::readStrongBinder(sp<IBinder>* val) const
{
    return unflatten_binder(ProcessState::self(), *this, val);//详见1.4.3
}

sp<IBinder> Parcel::readStrongBinder() const
{
    sp<IBinder> val;
    readStrongBinder(&val);
    return val;
}

  前有注册服务时候的flatten_binder扁平化Binder对象,现有unflatten_binder反扁平化Binder对象,你们是全家桶是吗!前饼果子来一套,切克闹!

1.4.3 unflatten_binder
//Parcel.cpp
status_t unflatten_binder(const sp<ProcessState>& proc,
    const Parcel& in, sp<IBinder>* out)
{
    //flat_binder_object应该是熟悉面孔了吗,主要用来解析Parcel中的存储的Binder数据类型
    const flat_binder_object* flat = in.readObject(false);

    if (flat) {
        switch (flat->type) {
            case BINDER_TYPE_BINDER:
				...
            case BINDER_TYPE_HANDLE://会走该分支,因为我们是跨进程获取Binder服务的代理端
            	//该过程主要是通过获取远程服务端引用的句柄,创建BpBinder
                *out = proc->getStrongProxyForHandle(flat->handle);//详见章节1.4.4
                return finish_unflatten_binder(
                    static_cast<BpBinder*>(out->get()), *flat, in);
        }
    }
    return BAD_TYPE;
}

  这里的flat_binder_object结构体是我们的老熟人了,其被设计用来解析存储Parcel中的Bidner对象类型数据(即可以是Binder实体服务,也可以是Binder引用对象,男女通吃老少皆宜!)。然后我们知道此时从Binder驱动中读取回来的是远程Binder服务的Binder引用类型(前提是非同一进程中获取),所以会走BINDER_TYPE_HANDLE分支,该分支干的事情吗也不多,主要如下:

  • 参数in是Java层的Parcel对象reply在C++层对应的Parcel对象,这里使用in.readObject(false)从Parcel对象中读取出servicemanager进程写入的binder_object,并转换为flat_binder_object类型指针
  • 通过Binder驱动返回的远程BInder的引用句柄handle创建一个BpBinder(handle)
  • 接着调用finish_unflatten_binder返回
1.4.4 Parcel::readObject
//Parcel.cpp
const flat_binder_object* Parcel::readObject(bool nullMetaData) const
{
    const size_t DPOS = mDataPos;
    if ((DPOS+sizeof(flat_binder_object)) <= mDataSize) {
        const flat_binder_object* obj
                = reinterpret_cast<const flat_binder_object*>(mData+DPOS);
        mDataPos = DPOS + sizeof(flat_binder_object);
        if (!nullMetaData && (obj->cookie == 0 && obj->binder == 0)) {
            // When transferring a NULL object, we don't write it into
            // the object list, so we don't want to check for it when
            // reading.
            ALOGV("readObject Setting data pos of %p to %zu", this, mDataPos);
            return obj;
        }

        // Ensure that this object is valid...
        binder_size_t* const OBJS = mObjects;
        const size_t N = mObjectsSize;
        size_t opos = mNextObjectHint;

        if (N > 0) {
            ALOGV("Parcel %p looking for obj at %zu, hint=%zu",
                 this, DPOS, opos);

            // Start at the current hint position, looking for an object at
            // the current data position.
            if (opos < N) {
                while (opos < (N-1) && OBJS[opos] < DPOS) {
                    opos++;
                }
            } else {
                opos = N-1;
            }
            if (OBJS[opos] == DPOS) {
                // Found it!
                ALOGV("Parcel %p found obj %zu at index %zu with forward search",
                     this, DPOS, opos);
                mNextObjectHint = opos+1;
                ALOGV("readObject Setting data pos of %p to %zu", this, mDataPos);
                return obj;
            }

            // Look backwards for it...
            while (opos > 0 && OBJS[opos] > DPOS) {
                opos--;
            }
            if (OBJS[opos] == DPOS) {
                // Found it!
                ALOGV("Parcel %p found obj %zu at index %zu with backward search",
                     this, DPOS, opos);
                mNextObjectHint = opos+1;
                ALOGV("readObject Setting data pos of %p to %zu", this, mDataPos);
                return obj;
            }
        }
        ALOGW("Attempt to read object from Parcel %p at offset %zu that is not in the object list",
             this, DPOS);
    }
    return NULL;
}

  注意此时的Parcel对象已经借助Binder驱动读取了servicemanager返回的相关远程代理端BInder的数据,这里借助readObject将数据转换成flat_binder_object类型的,至于为什么采取上述的读取方式这个就得参考Binder对象在Parcel容器中的存储方式了,这个图我们在上面总结过一次,再放出来一次如下:

在这里插入图片描述

1.4.5 unflatten_binder
//ProcessState.h
 struct handle_entry {
     IBinder* binder;
     RefBase::weakref_type* refs;
 };
 Vector<handle_entry>mHandleToObject;


//ProcessState.cpp
ProcessState::handle_entry* ProcessState::lookupHandleLocked(int32_t handle)
{
    const size_t N=mHandleToObject.size();
    if (N <= (size_t)handle) {
        handle_entry e;
        e.binder = NULL;
        e.refs = NULL;
        status_t err = mHandleToObject.insertAt(e, N, handle+1-N);
        if (err < NO_ERROR) return NULL;
    }
    return &mHandleToObject.editItemAt(handle);
}


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

    AutoMutex _l(mLock);

    handle_entry* e = lookupHandleLocked(handle);//查询mHandleToObject列表中是否有该handle的记录,即该进程以前是否有获取过该服务

    if (e != NULL) {//找到了
        // We need to create a new BpBinder if there isn't currently one, OR we
        // are unable to acquire a weak reference on this current one.  See comment
        // in getWeakProxyForHandle() for more info about this.
        IBinder* b = e->binder;
        if (b == NULL || !e->refs->attemptIncWeak(this)) {
            if (handle == 0) {//这里的handle为0是专属席位,是给ServiceManagerProxy的,所以我们和此处无缘
                Parcel data;
                status_t status = IPCThreadState::self()->transact(
                        0, IBinder::PING_TRANSACTION, data, NULL, 0);
                if (status == DEAD_OBJECT)
                   return NULL;
            }
			//根据handle创建BpBinder
            b = new BpBinder(handle); 
            e->binder = b;
            if (b) e->refs = b->getWeakRefs();
            result = b;
        } else {
            // This little bit of nastyness is to allow us to add a primary
            // reference to the remote proxy when this team doesn't have one
            // but another team is sending the handle to us.
            result.force_set(b);
            e->refs->decWeak(this);
        }
    }

    return result;
}

  经过重重险阻,最终创建了指向AMS Binder服务端的BpBinder代理对象。而我们在章节1.4中说过javaObjectForIBinder将native层BpBinder对象转换为Java层BinderProxy对象。 这里我们通过ServiceManager.getService最终获取了指向目标Binder服务端的代理对象BinderProxy。但是我们知道BinderProxy是用来和Binder驱动进行通信的,那么怎么和服务端的业务逻辑牵涉起来呢,这个我们章节1.6再来分析。回到章节1.4.3还有最后一个小点finish_unflatten_binder看看它干了啥,其实没有干啥:

//Parcel.cpp
inline static status_t finish_unflatten_binder(
    BpBinder* /*proxy*/, const flat_binder_object& /*flat*/,
    const Parcel& /*in*/)
{
    return NO_ERROR;
}

1.5 readStrongBinder读取Binder代理端小结

  至此readStrongBinder的整个脉络分析就告一段落了,这其中的跨度也很大,涉及到了Java层,JNI,C++,驱动层等,没有一定的毅力和学习方法,想真的把相关知识点啃下来是件不容易见的事情。

我们假设Parcel在内存中是以类似数组的存储空间进行存储的(这个只是假设,实际并不是如此),那么此时各个C_Parcel的位置相关的变量值的逻辑如下所示这里需要重点说明的是mData的值肯定不为0,因为内存中的0地址是给系统预留的,但是mDataPos的值是从0开始增长起来的。

在这里插入图片描述

  并且我们在readStrongBinder的源码分析过程中涉及到了BpBinder(C++),BinderProxy(Java),IXXXProxy(Java)之间的相互转换关系,可以用如下的图示来表示:

在这里插入图片描述

  • 首先从servicemanager进程返回来的Parcel对象中取出flat_binder_object

  • 然后根据Binder引用对象的描述符句柄(即handle)创建BpBinder对象,在创建Java层负责通信的BinderProxy对象

  • 最后创建和业务相关的XXXProxy对象,这样就得到了所查询得到的服务的代理对象通过该代理对象就可以向该服务的本地对象发送RPC远程调用请求。




总结

   Android Binder框架实现之Parcel详解终极篇就到这里告一段落了,通过上述篇章我们将Parcel的百般武艺基本统统介绍完毕了,基本的数据读写自然不必说了,其中最最重要的就是对Android Binder类型的数据的相关操作我们也重点分析了一二,这个也是Parcel在Android Binder开发中占有一席之地的根本原因,但是这个也是它的难点,所以想学好它还是得花费一番功夫的。好了,先说再见了。如果对你有帮助,欢迎关注和点赞,当然拍砖也是OK的,各位江湖见!

  • 18
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
以下是一个使用 AIDL 技术实现的 Kotlin 服务端示例,可以接收来自多个不同的 APP 客户端的请求,同时区分来自哪一个客户端 APP 的什么类型请求,并将请求加入任务栈,使用一个单独的类 TaskStack 来管理,任务和注册的回调消息绑定,使用一个 Dispach 类来管理任务的分发,按照请求的优先级同步或者异步地将任务分发执行,并在执行完成后通过 Dispach 将消息回调给发起请求的那个客户端 APP。代码如下: TaskStack.kt: ```kotlin import java.util.concurrent.PriorityBlockingQueue class TaskStack private constructor() { private val tasks: PriorityBlockingQueue<Task> = PriorityBlockingQueue() companion object { private var instance: TaskStack? = null @Synchronized fun getInstance(): TaskStack { if (instance == null) { instance = TaskStack() } return instance as TaskStack } } fun addTask(task: Task) { tasks.offer(task) } fun getNextTask(): Task? { return tasks.poll() } } ``` Task.kt: ```kotlin import android.os.Parcel import android.os.Parcelable class Task(val appId: String, val type: String, val priority: Int, val data: String?, val callback: ITaskCallback?) : Parcelable, Comparable<Task> { override fun compareTo(other: Task): Int { return other.priority - priority } override fun describeContents(): Int { return 0 } override fun writeToParcel(dest: Parcel?, flags: Int) { dest?.writeString(appId) dest?.writeString(type) dest?.writeInt(priority) dest?.writeString(data) dest?.writeStrongBinder(callback?.asBinder()) } companion object { @JvmField val CREATOR = object : Parcelable.Creator<Task> { override fun createFromParcel(source: Parcel): Task { return Task( source.readString() ?: "", source.readString() ?: "", source.readInt(), source.readString(), ITaskCallback.Stub.asInterface(source.readStrongBinder()) ) } override fun newArray(size: Int): Array<Task?> { return arrayOfNulls(size) } } } } ``` ITaskCallback.aidl: ```aidl interface ITaskCallback { void onTaskResult(String result); } ``` Dispach.kt: ```kotlin import java.util.concurrent.ExecutorService import java.util.concurrent.Executors class Dispach private constructor() { private var executorService: ExecutorService = Executors.newCachedThreadPool() companion object { private var instance: Dispach? = null @Synchronized fun getInstance(): Dispach { if (instance == null) { instance = Dispach() } return instance as Dispach } } fun dispatch(task: Task) { executorService.submit { val result = doTask(task) task.callback?.onTaskResult(result) } } private fun doTask(task: Task): String { // 执行任务的逻辑 return "" } } ``` TaskService.aidl: ```aidl interface TaskService { boolean registerCallback(ITaskCallback callback); boolean unregisterCallback(ITaskCallback callback); void addTask(Task task); } ``` TaskServiceImpl.kt: ```kotlin import android.os.RemoteCallbackList import android.os.RemoteException class TaskServiceImpl : TaskService.Stub() { private val callbacks: RemoteCallbackList<ITaskCallback> = RemoteCallbackList() private val taskStack: TaskStack = TaskStack.getInstance() private val dispach: Dispach = Dispach.getInstance() override fun registerCallback(callback: ITaskCallback): Boolean { return callbacks.register(callback) } override fun unregisterCallback(callback: ITaskCallback): Boolean { return callbacks.unregister(callback) } override fun addTask(task: Task) { val result = taskStack.addTask(task) if (result) { while (true) { val nextTask = taskStack.getNextTask() ?: break dispach.dispatch(nextTask) } } } private fun notifyTaskResult(result: String, callback: ITaskCallback) { try { callback.onTaskResult(result) } catch (e: RemoteException) { callbacks.unregister(callback) } } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值