Android8开机动画,1:bootanimation的启动流程

(学习课程为千里马framework的教程,整理成笔记自用)

代码路径介绍:
bootanimation   frameworks/base/cmds/bootanimation/
surfaceflinger  frameworks/native/services/surfaceflinger/
init            system/core/init/

bootanimation是绘制图像的,bootanimation是依赖 surfaceflinger的,
所以 bootanimation 需要等 surfaceflinger启动以后才能进行绘制

整体流程大致为:

        1:init进程启动:解析init.rc,启动 surfaceflinger
        2:surfaceFlinger初始化:设置属性触发bootanim服务启动
        3:init响应属性:通过属性服务启动bootanim进程
        4:BootAnimation:通过SurfaceFlinger渲染动画,监听退出信号

首先要先启动 surfaceflinger

1: 系统启动时,init进程会读取 init.rc文件 , init.rc文件中定义了 Surfaceflinger服务。

service surfaceflinger /system/bin/surfaceflinger
        ...
   init进程会根据 init.rc中的配置,在系统启动时触发Surfaceflinger的启动。
   (SurfaceFlinger的主要任务:
    1:初始化图形硬件
    2:创建显示缓冲区
    3:管理显示内容的合成和输出
     )

  这里init.rc文件应该也会触发bootanim.rc文件,但是bootanim.rc文件中设置了
    service bootanim  /system/bin/bootanim
        ...
        disable  (在开机时不会启动)
        ...

 bootanim的主要任务:
       1:通过SurfaceFlinger创建显示Surface;
       2:加载并显示开机动画资源
       3:在系统启动完成后退出

        执行到这里,表示surfacefligner 服务已经启动了,接下来进入下一步,进入surfaceflinger主函数部分, main_surfaceflinger.cpp是SurfaceFlinger进程的入口

2. SurfaceFlianger的启动与初始化

在 main_surfaceflinger.cpp 中找到主函数

ramework/native/services/surfaceflinger/main_surfaceflinger.cpp

    int mian (int , char**) {
	...

	sp<SurfaceFlinger> flinger = new SurfaceFlinger();	//创建实例
	//main()函数创建SuerfaceFlinger,实例并调用 init() 和 run()

	//初始化硬件
	flinger -> init();	//初始化HWC、启动属性线程

	//注册到ServiceManger
	sp<IServiceManager> sm(defaultServiceManager());
	//注册服务
	sm->addService(String16(SurfaceFlinger::getServiceName()),flinger,false);

	//进入主循环
	flinger -> run();	//处理VSync和合成任务	

	..
}

 这样做的目的:提供图形合成和核心能力,管理屏幕显示内容

关键方法:
    1: new SurfaceFlinger()
        作用:创建SurfaceFlinger对象,负责管理图形显示合成(Compositor)的核心服务
        为什么要创建SurfaceFlinger()方法: SurfaceFlinger是Android图形系统的画像,
            负责将应用层的Surface合成到屏幕
    2: init():
        作用:初始化硬件图形(HWC)、显示配置,并启动属性设置线程
    3:   run():
        作用:进入主事件循环,处理VSync(垂直同步)信号和图形合成任务
        为什么需要: SurfaceFlinger需要持续监听显示事件,确保帧按屏幕刷新合成

下一步进入SurfaceFlinger.cpp文件中,因为从上述代码中得知:
        flinger -> init();
        flinger -> run();
        进入SurfaceFlinger,找到init()方法,开始执行init()方法

(这里放入全部init()方法中全部代码的原因,是我不理解为什么只有

if (getHwComposer().hasCapability(
            HWC2::Capability::PresentFenceIsNotReliable)) {
        mStartPropertySetThread = new StartPropertySetThread(false);
    } else {
        mStartPropertySetThread = new StartPropertySetThread(true);
    }

    if (mStartPropertySetThread->Start() != NO_ERROR) {
        ALOGE("Run StartPropertySetThread failed!");
    }
是关键方法,其他方法又起到了什么作用

        这一段代码是触发开机动画的关键代码,其他部分开机动画的启动起到了什么作用,所以下面附上init()部分的全部代码,对其整体进行简单分析:

SurfaceFlinger.cpp中init()方法的调用

 默认显示初始化EGL
        //作用:初始化EGL ,创建OpenGL ES的显示连接和上下文
        //与动画关系:EGL 是 OpenGL ES的渲染基础,Bootanimation通过SurfaceFlinger获取 Surface提交
        //帧数据时,依赖EGL上下文进行图形绘制
                                                                                                (代码如下:下同)

mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(mEGLDisplay, NULL, NULL);

 start the EventThread 开始线程
        //创建 Vsync事件线程
        //作用:创建两个VSync事件线程:
        //    mEventThread:处理应用层的 VSync信号(如Bootanimation的帧提交)
        //    mSREventThread:处理SurfaceFlinger自身的合成任务
        //与动画的关系:
        //    Bootanimation按屏幕刷新率提交帧,依赖VSync信号同步渲染,避免屏幕撕裂

sp<VSyncSource> vsyncSrc = new DispSyncSource(&mPrimaryDispSync,
    vsyncPhaseOffsetNs, true, "app");
mEventThread = new EventThread(vsyncSrc, *this, false);
sp<VSyncSource> sfVsyncSrc = new DispSyncSource(&mPrimaryDispSync,
	    sfVsyncPhaseOffsetNs, true, "sf");
mSFEventThread = new EventThread(sfVsyncSrc, *this, true);
mEventQueue.setEventThread(mSFEventThread);

// set EventThread and SFEventThread to SCHED_FIFO to minimize jitter
        //设置实时调度策略
        //作用:将VSync线程的调度策略设为 SCHED_FIFO(实时优先级),确保VSync信号处理的递延迟
        //与动画的关系:保证动画帧率稳定,避免因为线程调度延迟导致动画卡顿

struct sched_param param = {0};
param.sched_priority = 2;
if (sched_setscheduler(mSFEventThread->getTid(), SCHED_FIFO, &param) != 0) {
	ALOGE("Couldn't set SCHED_FIFO for SFEventThread");
}
 if (sched_setscheduler(mEventThread->getTid(), SCHED_FIFO, &param) != 0) {
        ALOGE("Couldn't set SCHED_FIFO for EventThread");
}

//初始化硬件合成器(HWComposer)
        //作用:初始化硬件合成器(HWComposer),与显示硬件和驱动交互,管理显示缓冲区
        //与动画关系:HWC负责将BootAnimation的帧数据最终提交到屏幕,硬件加速合成可降低CPU/GPU负载

mHwc.reset(new HWComposer(false));
mHwc->registerCallback(this, mComposerSequenceId);

// set initial conditions (e.g. unblank default device)
        //初始化显示设备
        //初始化默认显示设备(如主屏幕),设置分辨率和显示模式
        //与动画关系:确保动画按屏幕物理分辨率渲染,避免拉伸或黑边

initializeDisplays();

// Inform native graphics APIs whether the present timestamp is supported:
        //启动属性设置线程(StartPropertySetThread)
        //作用:根据硬件合成器的能力(是否支持可靠的PresentFence),设置属性
        //service.sf.present_timestamp , 并触发bootanim服务启动
        //关键点在于:
        //    PresentFence:用于同步帧提交与屏幕刷新,若不可靠,需禁用时间戳相关优化
        //    service.bootanim.exit = 0: 允许动画启动
        //    ctl.start = bootanim : 通过init()线程启动 bootanim服务

if (getHwComposer().hasCapability(HWC2::Capability::PresentFenceIsNotReliable)) {
	mStartPropertySetThread = new StartPropertySetThread(false);
} else {
	mStartPropertySetThread = new StartPropertySetThread(true);
}

if (mStartPropertySetThread->Start() != NO_ERROR) {
	ALOGE("Run StartPropertySetThread failed!");
}

ALOGV("Done initializing");

(这段代码是触发bootanim的关键点, mStartPropertySetThread -> Start() 启动属性设置线程)
        初始化硬件(HWC2),根据硬件composer的能力来决定是否支持可靠的呈现时间戳。
        调用getHwComposer()函数,获取硬件composer实例
        使用hasCapability()函数检查composer是否具有 HWC2::Capability::PresentFencelsNotReliable能力。

        不可靠,给mStartPropertySetThread传入一个false
        可靠,给mStartPropertySetThread传入一个true

        Start函数的返回值被与NO_ERROR进行比较,以检查线程启动是否成功。如果Start函数返回的值不等于NO_ERROR
        则说明线程启动失败,程序将执行if语句块中的来处理这个错误

针对SurfaceFlinger.cpp中init()方法关于开机动画调用情况的总结:

一:除了关键bootanim的触发点以外,其他部分的代码也是必要的:

    1:EGL和RenderEngine:
        --提供OpenGL ES渲染上下文,Bootanimation需要将图片帧渲染到Surface,依赖EGL和
          RenderEngine的GPU加速能力
    2:VSync事件线程
        --确保动画按屏幕刷新率提交帧,避免丢帧或卡顿
    3:HWComposer:
        --管理显示缓冲区,硬件加速合成动画帧,降低CPU/GPU负载。
    4:显示设备初始化:
        --设置正确的分辨率和显示模式,保证动画全屏显示

二:开机动画的完整依赖链
 SurfaceFlinger 初始化
    ├─ EGL/RenderEngine → 提供 GPU 渲染能力
    ├─ HWComposer → 硬件加速合成
    ├─ VSync 线程 → 同步帧率
    ├─ 显示设备初始化 → 设置分辨率
    └─ StartPropertySetThread → 触发动画启动

----直接触发:StartPropertySetThread是动画开启的“开关”;
----间接依赖:其他代码为动画提供运行环境(e.g. 渲染、同步、显示)

三:总结
1- StartPropertySetThread的作用 

        仅负责通过属性服务触发 bootanim 服务器动,是动画流程的“点火器”

2- 其他代码的作用:

         构建SurfaceFlinger的图形处理基础设施,为动画渲染、同步、合成提供底层支持。

3- 全部代码的作用

         开机动画是SurfaceFlinger功能的一个用例,其正常运行依赖完整的图形子系统初始化。

        mStartPropertyThread -> Start() 触发了动画启动的开关,进入 StartPropertyThead.cpp文件中,调用Start方法。现在进入StartPropertyThread.cpp文件中查看具体情况

进入StartPropertyThread.cpp文件

         这个类属于Android命名空间,这个类继承自Thread,构造函数接受了一个布尔参数“timestampProeprtyValue",用来初始化成员变量, 接下来是Start()方法的重载,以及threadLoop()方法的实现。

        首先,构造函数StartProeprtyThread::StartPropertySetThread(bool timestampPropertyValue()).这里调用基类Thread的构造函数(Thead(false)),参数是false,可能表示线程不是可加入的(joinable)。 然后初始化成员变量mTimetampPropertyValue,这个变量用于后续设置属性。

        然后Start()方法,这是线程的主循环,在Andorid的线程模型中,thread()返回true会继续循环,返回false则线程结束。这里返回false,表示线程只会执行一次就退出。

        第一个属性kTimetampProperty根据mTimetampPropertyValue设置为 1 或 0 , 这个属性的作用是告诉系统是否支持呈现时间戳。
        第二个属性 service.bootanim.exit 设置为 0 ,表示允许开机动画运行
        第三个属性 ctl.start 设置为 bootanim,触发init进程启动bootanim服务

1:继承自Thread类, Thread(false)调用基类构造函数,参数false表示线程是不可加入(non-joinable)的。
        --不可加入线程:线程结束后自动释放资源,无需调用join()等待其结束。
2:初始化成员变量
        --mTimestampPropertyValue是一个布尔值,用于后续设置service.sf.present_timestamp属性
在开机动画中的作用:
        --根据硬件合成器(HWC)的能力(是否支持可靠的PresentFence),决定是否启动时间戳属性。

StartPropertySetThread::StartPropertySetThread(bool timestampPropertyValue):
    Thread(false), mTimestampPropertyValue(timestampPropertyValue) {}

此参数在SurfaceFlinger::init()中通过 getHwComposer().hasCapability(...)判断后传入。

/Start()方法:
//1:启动线程
        --run()是Thread类的方法,启动新线程并执行threadLoop()
 //2:参数说明
        --线程优先级:SurfaceFlinger::StartPropertySetThread , 用于调用调试和日志标识
        --优先级:PRIORITY_NORMAL表示普通优先级,确保线程不会过度占用CPU
 //3:在开机动画中的作用
        --启动线程后,立即执行threadLoop()方法设置关键属性,触发开机动画

status_t StartPropertySetThread::Start() {
    return run("SurfaceFlinger::StartPropertySetThread", PRIORITY_NORMAL);
}

线程主循环

bool StartPropertySetThread::threadLoop() {
// Set property service.sf.present_timestamp, consumer need check its readiness
//1:设置service.sf.present_timestamp:
//    --属性名:kTimestamProperty(实际值为"service.sf.present_timestamp")
//    --属性值:根据mTimestampPropertyValue 这是为 1 或 0
//    --作用:
//        1 表示系统支持可靠的呈现时间戳(Present Timestamp)
//        0 表示不支持
    property_set(kTimestampProperty, mTimestampPropertyValue ? "1" : "0");




// Clear BootAnimation exit flag
//2:设置service.bootanim.exit = 0 
//    --作用:通知开机动画进程(bootanim)可以运行
//    --动画进程逻辑:
//    bootanim会周期性检查此属性,如果值为 1 ,则退出动画
    property_set("service.bootanim.exit", "0");	//允许动画运行



// Start BootAnimation if not started
//3:设置 ctl.start = bootanim
//    --作用:通过init进程启动bootanim服务
//    --init进程响应机制:
//    init监听 ctl.start属性,解析到bootanim后,从服务列表中找到对应服务并启动。
	    property_set("ctl.start", "bootanim");	//触发bootanim服务启动



//4:返回false
//    --线程行为:threadLoop() 返回 false表示线程仅执行一次后退出
	    return false;	//线程仅执行一次
}

在开机动画中的作用:
        1:配置时间戳支持状态
        2:允许动画运行

我们再梳理一遍处理流程

第一步:property_set()的实现:
        --底层逻辑:
            property_set()通过Unix Domain Socket 和 init 进程通信,写入属性值
            init进程的property_service模块监听Socket,处理属性请求。
    第二步:init进程堆 ctl.start的处理
        --接受属性
            init和handle_property_set_fd()监听到ctl.start = bootanim
        --解析命令:
            调用 init.cpp 中的 handle_control_message()方法
            handle_control_message()
                ...
            };
        --启动服务
            从服务列表中找到bootanim,调用Service::Start()派生子进程执行
    第三步:bootanim对service.bootanim.exit的响应
        动画进程逻辑
        //Bootanimation.cpp文件中
        void BootAnimation::checkExit(){
            ...
            property_get(EXIT_PROP_NAME,value,"0");
            ...
        }

     动画循环中,周期性检查此属性,实现退出控制。

        那么问题来了,为什么这里property_set一设置,bootanim就会启动,上面说到,两个property_set设置处,init进程会对两个设置进行监听。
        第一个property_set中设置了 service_bootanim.exit = 0 , 表示运行开机动画启动;
        第二个property_set中设置了 ctl.start = bootanim, init的handle_contorl_message()获取到了这个属性变化;
        那么下一步,先针对property_set()关键代码的调用继续梳理,再回到init.cpp 中继续跟踪。

ctl.start = bootanim的处理流程

第一步:属性设置请求接收
        1:其他进程调用property_set("ctl.start","bootanim");
        e.g. SurfaceFlinger的StartPropertySetThread设置此属性
        2:写入到Socket中
        通过Unix Domain Socket /dev/socket/property_service发送属性名和值

 第二步:init进程的时间循环响应
        1:epoll_wait监听到Socket事件:
            -当ctl.start = bootanim写入到Scoket时,epoll_wait返回,触发回调函数
            handle_property_set_fd.
        2:调用 handle_property_set_fd():
            //读取属性名和值

 第三部:处理控制命令 ctl.start
        1:handle_property_set()解析控制命令(代码在propert_service.cpp文件中):
        static void handle_property_set(...) {
                if (name.starts_with("ctl.")) {
                handle_control_message(name.substr(4), value); // 提取 "start" 和 "bootanim"
                }
        }
          
        2:handle_control_message()启动服务(代码在init.cpp文件中):
        void handle_control_message(const string& msg, const string& name) {
                Service* svc = ServiceManager::GetInstance().FindServiceByName(name);
                if (msg == "start") svc->Start(); // 调用 Service::Start()
        }

其关键调用链为:

1. main()第二阶段初始化
	-> property_int()
	->start_property_service() -> 创建 Socket,注册epoll监听
2. 其他进程设置属性:
	-> property_set("ctl.start","bootanim");
3. init进程响应
	-> epoll_wait 监听到 Socket事件
	-> handle_property_set_fd() -> 解析属性名和值
	-> handle_property_set() -> 处理控制命令
	-> handle_control_message("start","bootanim"); ->提取接收到的信息
4. 服务启动
	-> ServiceManager::FindServiceByName("bootanim");
	-> Service::Start() -> fork() + execve("/system/bin/bootanimation");

        这里为了衔接后续 init.cpp 文件里相关方法的调用以及 property_service方法的调用。有必要说明整个调用链是什么。        

        下一步我们再次回到 init.cpp 文件中,查看其在main方法中的调用流程
        说明:这里的分析只针对init 对ctl.start的处理

init.cpp文件中,ctl.start的处理流程

第一步:初始化属性

//初始化属性系统
property_init();


//启动服务属性(创建 Socket 并注册 epoll 监听)
//这里调用了property_service.cpp文件中的start_property_service()方法
//---> 去property_service.cpp中查看start_property_service()的调用链
start_property_service();	//启动属性服务

通过epoll监听该Socket的事件,注册回调函数 handle_property_set_fd.

第二步:处理属性设置请求

进入property_serivce.cpp文件中,执行start_property_service()
代码位于 /system/core/init/property_service.cpp中

void start_property_service() {
//设置属性服务的版本号 2,有助于系统跟踪兼容性或在调试时识别服务状态
    property_set("ro.property_service.version", "2");


//参数解析:
//	PROP_SERVICE_NAME:Socket路径为/dev/socket/property_service
//	SOCK_STREAM:面向连接的流失Socket,确保可靠传输
//	SOCK_CLOEXEC:进程执行exec时关闭Socket,避免泄漏
//	SOCK_NONBLOCK:非阻塞冒失,防止进程在等待连接时被挂起
//	0666:权限设置为所有用户可读写
//作用:
//	创建一个用于跨进程通信的Socket,其他进程通过此Socket与init进程交互以设置或读取属性
    //获取了一个property_set_fd

    property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
		                           false, 0666, 0, 0, nullptr, sehandle);
    if (property_set_fd == -1) {
        PLOG(ERROR) << "start_property_service socket creation failed";
        exit(1);
    }

    //一直在监听property_set_fd
    //listen(property_set_fd, 8); 
    //开始监听Socket,允许最多8个未决连接,
    // --- 这使得其他进程可以通过此Socket与属性服务通信



    //将Socket的文件描述符注册到init进程的epoll实例中,并绑定回调函数
    //handle_property_set_fd:
    //当Socket有新的连接数据到达时,epoll_wait会触发handle_property_set_fd处理请求
    //handle_property_set_fd:负责解析属性请求,验证权限后调用property_set修改属性
    //(register)注册
    //这里register_epoll_handler会一直监听handler_property_set_fd有没有变化,如果有变化
    //就会回调handler_property_set_fd方法
    register_epoll_handler(property_set_fd, handle_property_set_fd);
}

第三步:回调handle_property_set_fd方法,那么我们进入到property_service.cpp的hander_property_set_fd方法中继续跟踪

        property_service.cpp文件位置,/systm/core/init/property_service.cpp

static void handler_property_set_fd(){

    //模式超时时间为2000毫秒,防止客户端长时间不发送数据导致阻塞
    static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms */

	
    //接受连接并分析命令
    int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
    struct ucred cr;
    getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size);
    SocketConnection socket(s, cr);
    uint32_t cmd;
    socket.RecvUint32(&cmd, &timeout_ms);

	
	...

	
    //解析属性名和值
	
    case PROP_MSG_SETPROP: {
    char prop_name[PROP_NAME_MAX];
    char prop_value[PROP_VALUE_MAX];

    if (!socket.RecvChars(prop_name, PROP_NAME_MAX, &timeout_ms) ||	//name = "ctr.start"
    !socket.RecvChars(prop_value, PROP_VALUE_MAX, &timeout_ms)) {	//value = "bootanim"
    PLOG(ERROR) << "sys_prop(PROP_MSG_SETPROP): error while reading name/value from the socket";
    return;
}

    prop_name[PROP_NAME_MAX-1] = 0;
    prop_value[PROP_VALUE_MAX-1] = 0;

    handle_property_set(socket, prop_value, prop_value, true);
    break;
    }

	...

}

        到这里 handle_property_set方法通过soket跨进程通信获取到了 prop_name = ctr.start  和 prop_value = bootanim
        下一步,调用property_servcie.cpp中的handle_property_set方法。

//权限验证
//权限检查
//	--check_control_mac_perms():验证客户端(SurfaceFlinger)是否有权执行
//	--SELinux策略:确保SurfaceFlinger的上下文允许发送
//		ctl.*属性。
static void handle_property_set(SocketConnection& socket,
                                const std::string& name,
                                const std::string& value,
                                bool legacy_protocol) {

		..........
		
	char* source_ctx = nullptr;
 	getpeercon(socket.socket(), &source_ctx);

	if (android::base::StartsWith(name, "ctl.")) {
	if (check_control_mac_perms(value.c_str(), source_ctx, &cr)) {
	handle_control_message(name.c_str() + 4, value.c_str());
	if (!legacy_protocol) {
	socket.SendUint32(PROP_SUCCESS);
	}
}

         通过:handle_control_message(name.c_str() + 4, value.c_str());的上下文得知,在这个判断语句中, 检查了一下控制,然后调用了 handle_control_message()方法,并将通过Socket获取到的 name 和 value 变化的值传入其中。

        那么我们下面,再去handle_control_message()方法中看一下做了那些操作
handle_control_message()方法在 /system/core/init/init.cpp文件中,那么我们再回到init.cpp文件中看一下

//处理控制命令
//这里会传入的参数 msg.name=ctl.star 
void handle_control_message(const std::string& msg, const std::string& name) {

    //该函数首先调用FindServiceByName,从service_list中查询
    Service* svc = ServiceManager::GetInstance().FindServiceByName(name);
    if (svc == nullptr) {
        LOG(ERROR) << "no such service '" << name << "'";
        return;
    }

    if (msg == "start") {
    // msg 获取到 start , 开始启动服务
            svc->Start();
    } else if (msg == "stop") {
            svc->Stop();
    } else if (msg == "restart") {
            svc->Restart();
    } else {
	LOG(ERROR) << "unknown control msg '" << msg << "'";
    }
}
//查询服务:从init.rc定义的bootanim服务,获取其Service对象。
//启动服务:调用Service::Start(),派生子进程 /system/bin/bootanimation 

//查询服务:从init.rc定义的bootanim服务,获取其Service对象。
//启动服务:调用Service::Start(),派生子进程 /system/bin/bootanimation

        这里就相当于手动启动 bootanimation , 前面bootanim.rc文件中设置类disable , 就是开机时不会自行启动bootanimation,所以需要这里通过SurfaceFlinger调用bootanimation手动启动,才能进入bootanimation绘制.

 总结:

        init启动 ---> SurfaceFlinger初始化 ---> 属性触发 ---> bootanim启动

下面就到了bootanimation的实现

  

frameworks/base/cmds/bootanimation/bootanimation_main.cpp

    int main(int argc, char** argv)
    {
     
     
    sp<ProcessState> proc(ProcessState::self());
    ProcessState::self()->startThreadPool();
     
    // create the boot animation object
    sp<BootAnimation> boot = new BootAnimation();//创建BootAnimation实例
    
    //加入Binder通信
    IPCThreadState::self()->joinThreadPool();//binder线程池,与surfaceflinger通信用的。
     
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值