android使用throttle(节流阀)技术

Throttle技术主要用于防止系统因处理过多流量而崩溃,常见于异步操作。在Android中,当ContentObserver频繁触发onChange导致游标过多,可能导致内存不足。Throttle通过设置最小和最大超时时间来控制回调频率,避免过度查询。本文通过一个Throttle类的实现,详细解释了其工作原理和使用方法。

throttle 技术其实并不提升性能,这个技术主要是防止系统被超过自己不能处理的流量给搞垮了,这其实是个保护机制。使用throttle技术一般来说是对于一些自己无法控制的系统,只要是异步,一般都会有throttle机制。

比如:
在android系统中大量是使用ContentObserver(ContentObserver用于监听数据变化),一般情况会在ContentObserver中的onChange方法作相关查询工作,也就说会生成游标。当某时瞬间游标生成过多(数据库查询极端频繁,但属于正常的插入,而且不能用批量插入),会导致内存不够而被系统自动终止该程序。这时我们就可以使用throttle技术。
代码如下:
public class Throttle {
    public static final boolean DEBUG = false; // Don't submit with true

    public static final int DEFAULT_MIN_TIMEOUT = 150;
    public static final int DEFAULT_MAX_TIMEOUT = 2500;
    /* package */ static final int TIMEOUT_EXTEND_INTERVAL = 500;

    private static Timer TIMER = new Timer();

    private final Clock mClock;
    private final Timer mTimer;

    /** Name of the instance.  Only for logging. */
    private final String mName;

    /** Handler for UI thread. */
    private final Handler mHandler;

    /** Callback to be called */
    private final Runnable mCallback;

    /** Minimum (default) timeout, in milliseconds.  */
    private final int mMinTimeout;

    /** Max timeout, in milliseconds.  */
    private final int mMaxTimeout;

    /** Current timeout, in milliseconds. */
    private int mTimeout;

    /** When {@link #onEvent()} was last called. */
    private long mLastEventTime;

    private MyTimerTask mRunningTimerTask;

    /** Constructor with default timeout */
    public Throttle(String name, Runnable callback, Handler handler) {
        this(name, callback, handler, DEFAULT_MIN_TIMEOUT, DEFAULT_MAX_TIMEOUT);
    }

    /** Constructor that takes custom timeout */
    public Throttle(String name, Runnable callback, Handler handler,int minTimeout,
            int maxTimeout) {
        this(name, callback, handler, minTimeout, maxTimeout, Clock.INSTANCE, TIMER);
    }

    /** Constructor for tests */
    /* package */ Throttle(String name, Runnable callback, Handler handler,int minTimeout,
            int maxTimeout, Clock clock, Timer timer) {
        if (maxTimeout < minTimeout) {
            throw new IllegalArgumentException();
        }
        mName = name;
        mCallback = callback;
        mClock = clock;
        mTimer = timer;
        mHandler = handler;
        mMinTimeout = minTimeout;
        mMaxTimeout = maxTimeout;
        mTimeout = mMinTimeout;
    }

    private void debugLog(String message) {
        Log.d(Logging.LOG_TAG, "Throttle: [" + mName + "] " + message);
    }

    private boolean isCallbackScheduled() {
        return mRunningTimerTask != null;
    }

    public void cancelScheduledCallback() {
        if (mRunningTimerTask != null) {
            if (DEBUG) debugLog("Canceling scheduled callback");
            mRunningTimerTask.cancel();
            mRunningTimerTask = null;
        }
    }

    /* package */ void updateTimeout() {
        final long now = mClock.getTime();
        if ((now - mLastEventTime) <= TIMEOUT_EXTEND_INTERVAL) {
            mTimeout *= 2;
            if (mTimeout >= mMaxTimeout) {
                mTimeout = mMaxTimeout;
            }
            if (DEBUG) debugLog("Timeout extended " + mTimeout);
        } else {
            mTimeout = mMinTimeout;
            if (DEBUG) debugLog("Timeout reset to " + mTimeout);
        }

        mLastEventTime = now;
    }

    public void onEvent() {
        if (DEBUG) debugLog("onEvent");

        updateTimeout();

        if (isCallbackScheduled()) {
            if (DEBUG) debugLog("    callback already scheduled");
        } else {
            if (DEBUG) debugLog("    scheduling callback");
            mRunningTimerTask = new MyTimerTask();
            mTimer.schedule(mRunningTimerTask, mTimeout);
        }
    }

    /**
     * Timer task called on timeout,
     */
    private class MyTimerTask extends TimerTask {
        private boolean mCanceled;

        @Override
        public void run() {
            mHandler.post(new HandlerRunnable());
        }

        @Override
        public boolean cancel() {
            mCanceled = true;
            return super.cancel();
        }

        private class HandlerRunnable implements Runnable {
            @Override
            public void run() {
                mRunningTimerTask = null;
                if (!mCanceled) { // This check has to be done on the UI thread.
                    if (DEBUG) debugLog("Kicking callback");
                    mCallback.run();
                }
            }
        }
    }

    /* package */ int getTimeoutForTest() {
        return mTimeout;
    }

    /* package */ long getLastEventTimeForTest() {
        return mLastEventTime;
    }
}
 
下面这个类,如何避免短时间内(200ms)多次请求同一个接口,package com.tplink.omada.sdncontrollerv6.repository.devicedetail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.tplink.omada.common.utils.requestThenPostValue import com.tplink.omada.libnetwork.controller.protocol.GeneralResponse import com.tplink.omada.libnetwork.sdncontroller.model.ApAfcConfig import com.tplink.omada.libnetwork.sdncontroller.model.ApChannelLimit import com.tplink.omada.libnetwork.sdncontroller.model.ApDhcpReservationConfig import com.tplink.omada.libnetwork.sdncontroller.model.ApGeneralConfig import com.tplink.omada.libnetwork.sdncontroller.model.ApPortInfo import com.tplink.omada.libnetwork.sdncontroller.model.Components import com.tplink.omada.libnetwork.sdncontroller.model.DeviceRememberMe import com.tplink.omada.libnetwork.sdncontroller.model.EnergySavingMode import com.tplink.omada.libnetwork.sdncontroller.model.EnumDeviceCompatibleType import com.tplink.omada.libnetwork.sdncontroller.model.FirmwareInfo import com.tplink.omada.libnetwork.sdncontroller.model.IntelliRecoverDeviceType import com.tplink.omada.libnetwork.sdncontroller.model.LanNetwork import com.tplink.omada.libnetwork.sdncontroller.model.LocateResponse import com.tplink.omada.libnetwork.sdncontroller.model.MonitorItem import com.tplink.omada.libnetwork.sdncontroller.model.SdnApConfig import com.tplink.omada.libnetwork.sdncontroller.model.SdnApOverrideSsid import com.tplink.omada.libnetwork.sdncontroller.model.SdnApSmartAntenna import com.tplink.omada.libnetwork.sdncontroller.model.SdnApWlanGroup import com.tplink.omada.libnetwork.sdncontroller.model.SdnBriefApDetail import com.tplink.omada.libnetwork.sdncontroller.model.SdnIpSetting import com.tplink.omada.libnetwork.sdncontroller.model.SdnLed import com.tplink.omada.libnetwork.sdncontroller.model.SdnLoadBalanceList import com.tplink.omada.libnetwork.sdncontroller.model.SdnMeshStastic import com.tplink.omada.libnetwork.sdncontroller.model.SdnRadioInformation import com.tplink.omada.libnetwork.sdncontroller.model.SiteSetting import com.tplink.omada.libnetwork.sdncontroller.model.UnbindMacList import com.tplink.omada.libnetwork.sdncontroller.model.VerifyRespond import com.tplink.omada.libnetwork.sdncontroller.model.devicedetail.sdnv6.ApStatusInfoResult import com.tplink.omada.libnetwork.sdncontroller.model.lansetting.LanNetworkSplit import com.tplink.omada.sdncontroller.repository.ApMonitorHomeRepository import com.tplink.omada.sdncontroller.repository.ApMonitorV5Repository import com.tplink.omada.sdncontroller.repository.ClientItemRepository import com.tplink.omada.sdncontroller.repository.DeviceRepository import com.tplink.omada.sdncontroller.repository.IntelliRecoverRepository import com.tplink.omada.sdncontroller.repository.LicenseRepo import com.tplink.omada.sdncontroller.repository.RememberDeviceRepository import com.tplink.omada.sdncontroller.repository.SdnV6EAPDeviceDetailOpenApiRepository import com.tplink.omada.sdncontroller.repository.SiteRepository import com.tplink.omada.sdncontroller.repository.WirelessRepository import com.tplink.omada.sdncontroller.repository.lansettings.LanProfileRepository import com.tplink.omada.sdncontroller.repository.mspv2.MspV2MspViewRepository import com.tplink.omada.sdncontroller.ui.monitor.SdnDeviceStatus import com.tplink.omada.sdncontroller.ui.monitor.SdnDeviceStatusStrategy import com.tplink.omada.sdncontroller.ui.monitor.mspv2.EnumDeviceLevel import com.tplink.omada.sdncontroller.viewmodel.SdnApComponentsViewModel import com.tplink.omada.sdncontrollerv6.repository.devicedetail.core.AbstractSdnV6DeviceDetailCacheRepository import com.tplink.omada.sdncontrollerv6.repository.devicedetail.core.SdnV6DeviceDetailContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class SdnV6EAPDeviceDetailCacheRepository(private val deviceDetailContext: SdnV6DeviceDetailContext) : AbstractSdnV6DeviceDetailCacheRepository(deviceDetailContext) { // AP设备的概要信息,这个信息是最重要的,如果没有获取到该数据后续的很多判断都要错误 private val _apInfoLiveData = MutableLiveData<SdnBriefApDetail>() val apInfoLiveData: LiveData<SdnBriefApDetail> = _apInfoLiveData private val _ledLiveData = MutableLiveData<SdnLed>() val ledLiveData: LiveData<SdnLed> = _ledLiveData private val _ipSettingLiveData = MutableLiveData<SdnIpSetting>() val ipSettingLiveData: LiveData<SdnIpSetting> = _ipSettingLiveData private val _dhcpReservationLiveData = MutableLiveData<ApDhcpReservationConfig>() val dhcpReservationLiveData: LiveData<ApDhcpReservationConfig> = _dhcpReservationLiveData private val _lanNetworkListLiveData = MutableLiveData<List<LanNetwork>>() val lanNetworkListLiveData: LiveData<List<LanNetwork>> = _lanNetworkListLiveData private val _lanProfileNetworkListLiveData = MutableLiveData<List<LanNetworkSplit>>() val lanProfileNetworkListLiveData: LiveData<List<LanNetworkSplit>> = _lanProfileNetworkListLiveData private val _loadBalanceLiveData = MutableLiveData<SdnLoadBalanceList>() val loadBalanceLiveData: LiveData<SdnLoadBalanceList> = _loadBalanceLiveData private val _verifyRespondLiveData = MutableLiveData<VerifyRespond>() val verifyRespondLiveData: LiveData<VerifyRespond> = _verifyRespondLiveData private val _meshStatisticsLiveData = MutableLiveData<SdnMeshStastic>() val meshStatisticsLiveData: LiveData<SdnMeshStastic> = _meshStatisticsLiveData private val _channelLimitLiveData = MutableLiveData<ApChannelLimit>() val channelLimitLiveData: LiveData<ApChannelLimit> = _channelLimitLiveData private val _energySavingModeLiveData = MutableLiveData<EnergySavingMode>() val energySavingModeLiveData: LiveData<EnergySavingMode> = _energySavingModeLiveData private val _afcConfigLiveData = MutableLiveData<ApAfcConfig>() val afcConfigLiveData: LiveData<ApAfcConfig> = _afcConfigLiveData private val _smartAntennaLiveData = MutableLiveData<SdnApSmartAntenna>() val smartAntennaLiveData: LiveData<SdnApSmartAntenna> = _smartAntennaLiveData private val _rememberMeLiveData = MutableLiveData<DeviceRememberMe>() val rememberMeLiveData: LiveData<DeviceRememberMe> = _rememberMeLiveData private val _apPortsLiveData = MutableLiveData<List<ApPortInfo>>() val apPortsLiveData: LiveData<List<ApPortInfo>> = _apPortsLiveData // AP设备的Radio信息 private val _radioInfoLiveData = MutableLiveData<SdnRadioInformation>() val radioInfoLiveData: LiveData<SdnRadioInformation> = _radioInfoLiveData // AP设备的状态信息 private val _apStatusInfoLiveData = MutableLiveData<ApStatusInfoResult>() val apStatusInfoLiveData: LiveData<ApStatusInfoResult> = _apStatusInfoLiveData // AP设备的WlanGroup信息 private val _wlanGroupLiveData = MutableLiveData<List<SdnApWlanGroup>>() val wlanGroupLiveData: LiveData<List<SdnApWlanGroup>> = _wlanGroupLiveData // AP设备的overrideSsid信息 private val _overrideSsidLiveData = MutableLiveData<SdnApOverrideSsid>() val overrideSsidLiveData: LiveData<SdnApOverrideSsid> = _overrideSsidLiveData // EAP设备的组件协商信息 var deviceComponentsVM = SdnApComponentsViewModel() private val manager = deviceDetailContext.manager private val eapMSPApi by lazy { manager.createSdnRepository(MspV2MspViewRepository::class.java) } private val eapApi by lazy { manager.createSdnRepository(ApMonitorHomeRepository::class.java) } private val clientApi by lazy { manager.createSdnRepository(ClientItemRepository::class.java) } private val v5Api by lazy { manager.createSdnRepository(ApMonitorV5Repository::class.java) } private val deviceApi by lazy { manager.createSdnRepository(DeviceRepository::class.java) } private val siteApi by lazy { manager.createSdnRepository(SiteRepository::class.java) } private val eapOpenApi by lazy { manager.createSdnRepository(SdnV6EAPDeviceDetailOpenApiRepository::class.java) } private val licenseApi by lazy { manager.createSdnRepository(LicenseRepo::class.java) } private val rememberDeviceApi by lazy { manager.createSdnRepository(RememberDeviceRepository::class.java) } private val intelliRecoverApi by lazy { manager.createSdnRepository(IntelliRecoverRepository::class.java) } private val wlanGroupOpenApi by lazy { manager.createSdnRepository(WirelessRepository::class.java) } private val lanProfileApi by lazy { manager.createSdnRepository(LanProfileRepository::class.java) } private fun isMSPLevel(): Boolean = deviceDetailContext.fromPage == EnumDeviceLevel.MSP_DEVICE suspend fun requestAPInfo(): GeneralResponse<SdnBriefApDetail> { return _apInfoLiveData.requestThenPostValue { if (isMSPLevel()) { eapMSPApi.getMspApInfo(deviceDetailContext.deviceMac) } else { eapApi.getApInfo(deviceDetailContext.deviceMac) } } } override suspend fun requestDeviceComponents(): GeneralResponse<Components> { return getOrRequestDeviceComponents(deviceComponentsVM) { deviceApi.getApComponente(it) } } suspend fun requestApRadioInfo(): GeneralResponse<SdnRadioInformation> { return _radioInfoLiveData.requestThenPostValue { if (isMSPLevel()) { eapMSPApi.getMspApRadio(deviceDetailContext.deviceMac) } else { eapApi.getApRadio(deviceDetailContext.deviceMac) } } } suspend fun requestApStatusInfo(): GeneralResponse<ApStatusInfoResult> { return _apStatusInfoLiveData.requestThenPostValue { eapOpenApi.getApStatusInfo(deviceDetailContext.deviceMac) } } suspend fun forgetDevice(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { if (isMSPLevel()) { eapMSPApi.forgetMspDevice(deviceDetailContext.deviceMac, deviceDetailContext.deviceMac) } else { eapApi.forgetDevice(deviceDetailContext.deviceMac, deviceDetailContext.deviceMac) } } } suspend fun requestMeshStatistic(): GeneralResponse<SdnMeshStastic> { return _meshStatisticsLiveData.requestThenPostValue { clientApi.getMeshStastic(deviceDetailContext.deviceMac) } } suspend fun requestApChannelLimitConfig(): GeneralResponse<ApChannelLimit> { return _channelLimitLiveData.requestThenPostValue { v5Api.getApChannelLimitConfig(deviceDetailContext.deviceMac) } } suspend fun requestApSmartAntenna(): GeneralResponse<SdnApSmartAntenna> { return _smartAntennaLiveData.requestThenPostValue { eapApi.getApSmartAntenna(deviceDetailContext.deviceMac) } } suspend fun requestApEnergySavingMode(): GeneralResponse<EnergySavingMode> { return _energySavingModeLiveData.requestThenPostValue { eapApi.getApEnergySavingMode(deviceDetailContext.deviceMac) } } suspend fun onlineUpgrade(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { eapApi.onlineUpgrade(deviceDetailContext.deviceMac, deviceDetailContext.deviceMac) } } suspend fun getFirmwareInfo(): GeneralResponse<FirmwareInfo> { return withContext(Dispatchers.IO) { eapApi.getFirmwareInfo(deviceDetailContext.deviceMac) } } suspend fun reboot(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { eapApi.reboot(deviceDetailContext.deviceMac) } } suspend fun locate(locate: Boolean): GeneralResponse<LocateResponse> { return withContext(Dispatchers.IO) { eapApi.locate(deviceDetailContext.deviceMac, locate) } } suspend fun unbind(): GeneralResponse<Unit> { val unbindMacList = UnbindMacList(arrayListOf(deviceDetailContext.deviceMac)) return withContext(Dispatchers.IO) { if (isMSPLevel()) eapMSPApi.unbindMspDevices(unbindMacList) else licenseApi.unbindDevices(unbindMacList) } } suspend fun requestDeviceRememberMeStatus(): GeneralResponse<DeviceRememberMe> { return _rememberMeLiveData.requestThenPostValue { rememberDeviceApi.getDeviceRememberMeStatus(deviceDetailContext.deviceMac) } } suspend fun requestLedStatus(): GeneralResponse<SdnLed> { return _ledLiveData.requestThenPostValue { eapApi.getApLedStatus(deviceDetailContext.deviceMac) } } suspend fun requestIpSetting(): GeneralResponse<SdnIpSetting> { return _ipSettingLiveData.requestThenPostValue { eapApi.getIpSetting(deviceDetailContext.deviceMac) } } suspend fun requestDhcpReservation(): GeneralResponse<ApDhcpReservationConfig> { return _dhcpReservationLiveData.requestThenPostValue { eapApi.getApDhcpReservationConfig(deviceDetailContext.deviceMac) } } suspend fun requestLanNetworkList(): GeneralResponse<List<LanNetwork>> { return _lanNetworkListLiveData.requestThenPostValue { eapApi.getLanNetworkList() } } suspend fun requestLoadBalance(): GeneralResponse<SdnLoadBalanceList> { return _loadBalanceLiveData.requestThenPostValue { eapApi.getLoadBalance(deviceDetailContext.deviceMac) } } suspend fun requestVerifyRespond(): GeneralResponse<VerifyRespond> { return _verifyRespondLiveData.requestThenPostValue { intelliRecoverApi.checkVerify(MonitorItem(mac = deviceDetailContext.deviceMac, deviceType = IntelliRecoverDeviceType.AP.value)) } } suspend fun forceProvisionApDevice(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { v5Api.forceProvisionApDevice(deviceDetailContext.deviceMac) } } suspend fun requestApAfcConfig(): GeneralResponse<ApAfcConfig> { return _afcConfigLiveData.requestThenPostValue { v5Api.getApAfcConfig(deviceDetailContext.deviceMac) } } suspend fun setApAfcConfig(isChecked: Boolean): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { v5Api.setApAfcConfig(deviceDetailContext.deviceMac, ApAfcConfig(null, isChecked)) } } suspend fun requestSiteSetting(): GeneralResponse<SiteSetting> { return withContext(Dispatchers.IO) { siteApi.getSiteSetting() } } suspend fun requestLanProfileNetworkList(): GeneralResponse<List<LanNetworkSplit>> { return _lanProfileNetworkListLiveData.requestThenPostValue { lanProfileApi.getLanNetworkList() } } suspend fun setApConfig(config: SdnApConfig): GeneralResponse<Any?> { return withContext(Dispatchers.IO) { eapApi.setApConfig(deviceDetailContext.deviceMac, config) } } suspend fun addDeviceInMonitoringList(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { val paramsList = listOf( MonitorItem( clientFlag = false, customId = manager.getCurrentSdnSession()?.currentCustomerId, deviceType = IntelliRecoverDeviceType.AP.value, mac = deviceDetailContext.deviceMac ) ) intelliRecoverApi.addDeviceToList(paramsList) } } suspend fun deleteDeviceFromList(): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { intelliRecoverApi.deleteDeviceFromList(listOf(deviceDetailContext.deviceMac), null) } } suspend fun disableHardwareReset(disableHwReset: Boolean): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { v5Api.setApGeneralConfig(deviceDetailContext.deviceMac, ApGeneralConfig(disableHwReset = disableHwReset)) } } suspend fun enableRemoteReset(remoteReset: Boolean): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { v5Api.setApGeneralConfig(deviceDetailContext.deviceMac, ApGeneralConfig(remoteReset = remoteReset)) } } suspend fun saveRememberDevice(config: DeviceRememberMe): GeneralResponse<Unit> { return withContext(Dispatchers.IO) { rememberDeviceApi.setDeviceRememberMeStatus(deviceDetailContext.deviceMac, config) } } suspend fun requestApPortInfo(): GeneralResponse<List<ApPortInfo>> { return _apPortsLiveData.requestThenPostValue { v5Api.getApPortInfo(deviceDetailContext.deviceMac) } } suspend fun requestApWlanGroup(): GeneralResponse<List<SdnApWlanGroup>> { val map = mapOf( "deviceType" to "0" ) return _wlanGroupLiveData.requestThenPostValue { wlanGroupOpenApi.getApWlanGroup(map) } } suspend fun requestApOverrideSsid(): GeneralResponse<SdnApOverrideSsid> { return _overrideSsidLiveData.requestThenPostValue { eapApi.getApOverrideSsid(deviceDetailContext.deviceMac, true) } } fun getApInfo(): SdnBriefApDetail? = _apInfoLiveData.value fun isLicenseStatusActive(): Boolean = getApInfo()?.licenseStatus == 3 fun isDeviceUnActive(): Boolean = getApInfo()?.active == false fun isDisconnect(): Boolean = getApInfo()?.statusCategory?.let { SdnDeviceStatusStrategy.parse(it) } == SdnDeviceStatusStrategy.DISCONNECTED fun isFullCompatible(): Boolean = getApInfo()?.compatible == EnumDeviceCompatibleType.FULL_COMPATIBLE.type fun isConnected(): Boolean = getApInfo()?.statusCategory?.let { SdnDeviceStatusStrategy.parse(it) } == SdnDeviceStatusStrategy.CONNECTED fun isNotPending(): Boolean = getApInfo()?.statusCategory?.let { SdnDeviceStatusStrategy.parse(it) } != SdnDeviceStatusStrategy.PENDING fun getLicenseCategory(): String? = getApInfo()?.category fun getLicenseStatus(): Int? = getApInfo()?.licenseStatus fun apNeedUpgrade(): Boolean = getApInfo()?.needUpgrade == true fun getUnbindLeftCount(): Int = getApInfo()?.licenseUnbindingLimit ?: 0 fun getUnbindLimitCount(): Int = getApInfo()?.initialUnbindingLimit ?: 0 fun isBridgeRoleVisible(): Boolean = getApInfo()?.p2pRole != null fun isWireless(): Boolean = getApInfo()?.wirelessLinked == true fun getDeviceInfoStatus(): Int? = getApInfo()?.status }
最新发布
09-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值