[Network]Android N 新wifi scan流程分析

Android N wifi

Android N 的wifi架构真的是改动挺大,从文件目录看,添加了不少文件,实际上则是对整个wifi模块进行大卸耦,很多东西被才成独立的模块,便于维护,添加feature,这里貌似采用了门面设计模式,可以看FrameworkFacade.java,。下面就看下android N wifi scan都做了哪些修改。

startScan

startScan的API没有做什么变化,也是从wifiManager一直call到WIFiStateMachine里面。
startScan

但是在WiFiStatemachine中处理CMD_START_SCAN这里就开始发生变化了。从handleScanRequest 开始看吧

/*WifiStateMachine.java (frameworks\opt\net\wifi\service\java\com\android\server\wifi)
*/

    private void handleScanRequest(Message message) {
        ScanSettings settings = null;
        WorkSource workSource = null;

        // unbundle parameters
        Bundle bundle = (Bundle) message.obj;

        if (bundle != null) {
            settings = bundle.getParcelable(CUSTOMIZED_SCAN_SETTING);
            workSource = bundle.getParcelable(CUSTOMIZED_SCAN_WORKSOURCE);
        }

        Set<Integer> freqs = null;
        //setting里面记录了fred的信息
        if (settings != null && settings.channelSet != null) {
            freqs = new HashSet<Integer>();
            for (WifiChannel channel : settings.channelSet) {
                freqs.add(channel.freqMHz);
            }
        }

        // Retrieve the list of hidden networkId's to scan for.
        Set<Integer> hiddenNetworkIds = mWifiConfigManager.getHiddenConfiguredNetworkIds();

        // call wifi native to start the scan
        //这里是重点
        if (startScanNative(freqs, hiddenNetworkIds, workSource)) {
            // a full scan covers everything, clearing scan request buffer
            if (freqs == null)
                mBufferedScanMsg.clear();
            messageHandlingStatus = MESSAGE_HANDLING_STATUS_OK;
            if (workSource != null) {
                // External worksource was passed along the scan request,
                // hence always send a broadcast
                mSendScanResultsBroadcast = true;
            }
            return;
        }

        // if reach here, scan request is rejected
      .....
    }

看下这个startScanNative都做了些什么,其实这个函数起了一个wifiScanner和WifiStatemachine之间的桥梁作用。

    // TODO this is a temporary measure to bridge between WifiScanner and WifiStateMachine until
    // scan functionality is refactored out of WifiStateMachine.
    /**
     * return true iff scan request is accepted
     */
    private boolean startScanNative(final Set<Integer> freqs, Set<Integer> hiddenNetworkIds,
            WorkSource workSource) {
            //创建了一个ScanSetting对象,目的是为了设置scan的频率模式
        WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
        if (freqs == null) {
            /*假如我们前面没有指定freqs是什么,这里默认会采用WIFI_BAND_BOTH_WITH_DFS
              Both 2.4 GHz band and 5 GHz band; with DFS channels 
              public static final int WIFI_BAND_BOTH_WITH_DFS = 7;  
                   both bands with DFS channels 
            */
            settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
        } else {
            settings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
            int index = 0;
            settings.channels = new WifiScanner.ChannelSpec[freqs.size()];
            for (Integer freq : freqs) {
                settings.channels[index++] = new WifiScanner.ChannelSpec(freq);
            }
        }
        settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
                | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
        if (hiddenNetworkIds != null && hiddenNetworkIds.size() > 0) {
            int i = 0;
            settings.hiddenNetworkIds = new int[hiddenNetworkIds.size()];
            for (Integer netId : hiddenNetworkIds) {
                settings.hiddenNetworkIds[i++] = netId;
            }
        }
        //创建一个监听对象,这个后面调用mWifiScanner.startScan会用到
        WifiScanner.ScanListener nativeScanListener = new WifiScanner.ScanListener() {
                // ignore all events since WifiStateMachine is registered for the supplicant events
                public void onSuccess() {
                }
                public void onFailure(int reason, String description) {
                    mIsScanOngoing = false;
                    mIsFullScanOngoing = false;
                }
                public void onResults(WifiScanner.ScanData[] results) {
                }
                public void onFullResult(ScanResult fullScanResult) {
                }
                public void onPeriodChanged(int periodInMs) {
                }
            };
        //跳转到mWifiScanner.startScan()
        mWifiScanner.startScan(settings, nativeScanListener, workSource);
        mIsScanOngoing = true;
        mIsFullScanOngoing = (freqs == null);
        lastScanFreqs = freqs;
        return true;
    }

wifiScann是android N新添加进来的东西,其实这个一个client端。在android N,wifi scan被做成一个独立的service。这边是client端,通过asynchannel会发送CMD去service端,跑真正的scan函数。

//WifiScanner.java (frameworks\base\wifi\java\android\net\wifi) 

    /**
     * starts a single scan and reports results asynchronously
     * @param settings specifies various parameters for the scan; for more information look at
     * {@link ScanSettings}
     * @param listener specifies the object to report events to. This object is also treated as a
     *                 key for this scan, and must also be specified to cancel the scan. Multiple
     *                 scans should also not share this object.
     */
    public void startScan(ScanSettings settings, ScanListener listener) {
        startScan(settings, listener, null);
    }

    /**
     * starts a single scan and reports results asynchronously
     * @param settings specifies various parameters for the scan; for more information look at
     * {@link ScanSettings}
     * @param workSource WorkSource to blame for power usage
     * @param listener specifies the object to report events to. This object is also treated as a
     *                 key for this scan, and must also be specified to cancel the scan. Multiple
     *                 scans should also not share this object.
     */
    public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) {
        Preconditions.checkNotNull(listener, "listener cannot be null");
        int key = addListener(listener);
        if (key == INVALID_KEY) return;
        validateChannel();
        Bundle scanParams = new Bundle();
        scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
        scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
        mAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, scanParams);
    }

WifiScanningServiceImpl里面的状态机比较奇怪的,他维护了几类小状态机,针对不同的scan类型做不同的处理,我们这里是简单的从api 调用下来的SingleScan类型。其他的类型,后面再补上。

    public void startService() {
        mClientHandler = new ClientHandler(mLooper);
        mBackgroundScanStateMachine = new WifiBackgroundScanStateMachine(mLooper);
        mWifiChangeStateMachine = new WifiChangeStateMachine(mLooper);
        //
        mSingleScanStateMachine = new WifiSingleScanStateMachine(mLooper);
        mPnoScanStateMachine = new WifiPnoScanStateMachine(mLooper);

        mContext.registerReceiver(
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(Context context, Intent intent) {
                        int state = intent.getIntExtra(
                                WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_DISABLED);
                        if (DBG) localLog("SCAN_AVAILABLE : " + state);
                        if (state == WifiManager.WIFI_STATE_ENABLED) {
                            mBackgroundScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                            mSingleScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                            mPnoScanStateMachine.sendMessage(CMD_DRIVER_LOADED);
                        } else if (state == WifiManager.WIFI_STATE_DISABLED) {
                            mBackgroundScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                            mSingleScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                            mPnoScanStateMachine.sendMessage(CMD_DRIVER_UNLOADED);
                        }
                    }
                }, new IntentFilter(WifiManager.WIFI_SCAN_AVAILABLE));

        mBackgroundScanStateMachine.start();
        mWifiChangeStateMachine.start();
        mSingleScanStateMachine.start();
        mPnoScanStateMachine.start();
    }

mSingleScanStateMachine,里面有几个简单的状态。

/*
WifiScanningServiceImpl.java (frameworks\opt\net\wifi\service\java\com\android\server\wifi\scanner)
*/
    /**
     * State machine that holds the state of single scans. Scans should only be active in the
     * ScanningState. The pending scans and active scans maps are swaped when entering
     * ScanningState. Any requests queued while scanning will be placed in the pending queue and
     * executed after transitioning back to IdleState.
     */
        WifiSingleScanStateMachine(Looper looper) {
            super("WifiSingleScanStateMachine", looper);

            setLogRecSize(128);
            setLogOnlyTransitions(false);

            // CHECKSTYLE:OFF IndentationCheck
            addState(mDefaultState);
                addState(mDriverStartedState, mDefaultState);
                    addState(mIdleState, mDriverStartedState);
                    addState(mScanningState, mDriverStartedState);
            // CHECKSTYLE:ON IndentationCheck

            setInitialState(mDefaultState);
        }
 class DefaultState extends State {
            @Override
            public void enter() {
                mActiveScans.clear();
                mPendingScans.clear();
            }
            @Override
            public boolean processMessage(Message msg) {
                switch (msg.what) {
                    //前面如果收到发过来的driver已经加载的CMD_DRIVER_LOADED,则调到mIdleState
                    case CMD_DRIVER_LOADED:
                        transitionTo(mIdleState);
                        ....
  }

        class IdleState extends State {
            @Override
            public void enter() {
                //这里会去做一次尝试扫描,即当driver加载成功的时候,会自己做一次scan的操作
                tryToStartNewScan();
            }

            @Override
            public boolean processMessage(Message msg) {
                return NOT_HANDLED;
            }
        }

所以我们当前其实是处于WifiSingleScanStateMachine.IdleState,当收到CMD_START_SINGLE_SCAN这个CMD的时候,IdleState自己根本无法处理,所以交个他的上级。即在DriverStartedState中处理

        /**
         * State representing when the driver is running. This state is not meant to be transitioned
         * directly, but is instead indented as a parent state of ScanningState and IdleState
         * to hold common functionality and handle cleaning up scans when the driver is shut down.
         */
        class DriverStartedState extends State {
            @Override
            public void exit() {
                mWifiMetrics.incrementScanReturnEntry(
                        WifiMetricsProto.WifiLog.SCAN_FAILURE_INTERRUPTED,
                        mPendingScans.size());
                sendOpFailedToAllAndClear(mPendingScans, WifiScanner.REASON_UNSPECIFIED,
                        "Scan was interrupted");
            }

            @Override
            public boolean processMessage(Message msg) {
                ClientInfo ci = mClients.get(msg.replyTo);

                switch (msg.what) {
                    case WifiScanner.CMD_START_SINGLE_SCAN:
                        mWifiMetrics.incrementOneshotScanCount();
                        int handler = msg.arg2;
                        Bundle scanParams = (Bundle) msg.obj;
                        if (scanParams == null) {
                            logCallback("singleScanInvalidRequest",  ci, handler, "null params");
                            replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
                            return HANDLED;
                        }
                        scanParams.setDefusable(true);
                        ScanSettings scanSettings =
                                scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
                        WorkSource workSource =
                                scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
                        if (validateAndAddToScanQueue(ci, handler, scanSettings, workSource)) {
                            replySucceeded(msg);
                            // If were not currently scanning then try to start a scan. Otherwise
                            // this scan will be scheduled when transitioning back to IdleState
                            // after finishing the current scan.
                            //假如当前没有在scanning,那就try 
                            if (getCurrentState() != mScanningState) {
                                tryToStartNewScan();
                            }
                        } else {
                            logCallback("singleScanInvalidRequest",  ci, handler, "bad request");
                            replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
                            mWifiMetrics.incrementScanReturnEntry(
                                    WifiMetricsProto.WifiLog.SCAN_FAILURE_INVALID_CONFIGURATION, 1);
                        }
                        return HANDLED;
     .....

tryToStartNewScan中做了一次scan之后,如果没有失败,则会跳转到mScanningState

 void tryToStartNewScan() {
            if (mScannerImpl.startSingleScan(settings, this)) {
                // swap pending and active scan requests
                RequestList<ScanSettings> tmp = mActiveScans;
                mActiveScans = mPendingScans;
                mPendingScans = tmp;
                // make sure that the pending list is clear
                mPendingScans.clear();
                transitionTo(mScanningState);
  }

mScannerImpl其实是抽象类WifiScannerImpl的对象,这只是一个接口,要找到实现方,才知道做了什么。google卸耦真是干净利落。

/*
SupplicantWifiScannerImpl.java (frameworks\opt\net\wifi\service\java\com\android\server\wifi\scanner)   
*/
public class SupplicantWifiScannerImpl extends WifiScannerImpl implements Handler.Callback {
    .........
    @Override
    public boolean startSingleScan(WifiNative.ScanSettings settings,
            WifiNative.ScanEventHandler eventHandler) {
        if (eventHandler == null || settings == null) {
            Log.w(TAG, "Invalid arguments for startSingleScan: settings=" + settings
                    + ",eventHandler=" + eventHandler);
            return false;
        }
        if (mPendingSingleScanSettings != null
                || (mLastScanSettings != null && mLastScanSettings.singleScanActive)) {
            Log.w(TAG, "A single scan is already running");
            return false;
        }
        synchronized (mSettingsLock) {
            mPendingSingleScanSettings = settings;
            mPendingSingleScanEventHandler = eventHandler;
            //因为有多种scan共存,为防止打架,所以google搞了这个函数来做指挥
            processPendingScans();
            return true;
        }
    }
.......
    private void processPendingScans() {
        synchronized (mSettingsLock) {
            // Wait for the active scan result to come back to reschedule other scans,
            // unless if HW pno scan is running. Hw PNO scans are paused it if there
            // are other pending scans,
            if (mLastScanSettings != null && !mLastScanSettings.hwPnoScanActive) {
                return;
            }

            ChannelCollection allFreqs = mChannelHelper.createChannelCollection();
            Set<Integer> hiddenNetworkIdSet = new HashSet<Integer>();
            final LastScanSettings newScanSettings =
                    new LastScanSettings(mClock.elapsedRealtime());

            // Update scan settings if there is a pending scan
            if (!mBackgroundScanPaused) {
                if (mPendingBackgroundScanSettings != null) {
                    mBackgroundScanSettings = mPendingBackgroundScanSettings;
                    mBackgroundScanEventHandler = mPendingBackgroundScanEventHandler;
                    mNextBackgroundScanPeriod = 0;
                    mPendingBackgroundScanSettings = null;
                    mPendingBackgroundScanEventHandler = null;
                    mBackgroundScanPeriodPending = true;
                }
                if (mBackgroundScanPeriodPending && mBackgroundScanSettings != null) {
                    int reportEvents = WifiScanner.REPORT_EVENT_NO_BATCH; // default to no batch
                    for (int bucket_id = 0; bucket_id < mBackgroundScanSettings.num_buckets;
                            ++bucket_id) {
                        WifiNative.BucketSettings bucket =
                                mBackgroundScanSettings.buckets[bucket_id];
                        if (mNextBackgroundScanPeriod % (bucket.period_ms
                                        / mBackgroundScanSettings.base_period_ms) == 0) {
                            if ((bucket.report_events
                                            & WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN) != 0) {
                                reportEvents |= WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
                            }
                            if ((bucket.report_events
                                            & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
                                reportEvents |= WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
                            }
                            // only no batch if all buckets specify it
                            if ((bucket.report_events
                                            & WifiScanner.REPORT_EVENT_NO_BATCH) == 0) {
                                reportEvents &= ~WifiScanner.REPORT_EVENT_NO_BATCH;
                            }

                            allFreqs.addChannels(bucket);
                        }
                    }
                    if (!allFreqs.isEmpty()) {
                        newScanSettings.setBackgroundScan(mNextBackgroundScanId++,
                                mBackgroundScanSettings.max_ap_per_scan, reportEvents,
                                mBackgroundScanSettings.report_threshold_num_scans,
                                mBackgroundScanSettings.report_threshold_percent);
                    }

                    int[] hiddenNetworkIds = mBackgroundScanSettings.hiddenNetworkIds;
                    if (hiddenNetworkIds != null) {
                        int numHiddenNetworkIds = Math.min(hiddenNetworkIds.length,
                                MAX_HIDDEN_NETWORK_IDS_PER_SCAN);
                        for (int i = 0; i < numHiddenNetworkIds; i++) {
                            hiddenNetworkIdSet.add(hiddenNetworkIds[i]);
                        }
                    }

                    mNextBackgroundScanPeriod++;
                    mBackgroundScanPeriodPending = false;
                    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                            mClock.elapsedRealtime() + mBackgroundScanSettings.base_period_ms,
                            BACKGROUND_PERIOD_ALARM_TAG, mScanPeriodListener, mEventHandler);
                }
            }

            if (mPendingSingleScanSettings != null) {
                boolean reportFullResults = false;
                ChannelCollection singleScanFreqs = mChannelHelper.createChannelCollection();
                for (int i = 0; i < mPendingSingleScanSettings.num_buckets; ++i) {
                    WifiNative.BucketSettings bucketSettings =
                            mPendingSingleScanSettings.buckets[i];
                    if ((bucketSettings.report_events
                                    & WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT) != 0) {
                        reportFullResults = true;
                    }
                    singleScanFreqs.addChannels(bucketSettings);
                    allFreqs.addChannels(bucketSettings);
                }
                newScanSettings.setSingleScan(reportFullResults, singleScanFreqs,
                        mPendingSingleScanEventHandler);
                int[] hiddenNetworkIds = mPendingSingleScanSettings.hiddenNetworkIds;
                if (hiddenNetworkIds != null) {
                    int numHiddenNetworkIds = Math.min(hiddenNetworkIds.length,
                            MAX_HIDDEN_NETWORK_IDS_PER_SCAN);
                    for (int i = 0; i < numHiddenNetworkIds; i++) {
                        hiddenNetworkIdSet.add(hiddenNetworkIds[i]);
                    }
                }
                mPendingSingleScanSettings = null;
                mPendingSingleScanEventHandler = null;
            }

            if ((newScanSettings.backgroundScanActive || newScanSettings.singleScanActive)
                    && !allFreqs.isEmpty()) {
                pauseHwPnoScan();
                Set<Integer> freqs = allFreqs.getSupplicantScanFreqs();
                boolean success = mWifiNative.scan(freqs, hiddenNetworkIdSet);
                if (success) {
                    // TODO handle scan timeout
                    if (DBG) {
                        Log.d(TAG, "Starting wifi scan for freqs=" + freqs
                                + ", background=" + newScanSettings.backgroundScanActive
                                + ", single=" + newScanSettings.singleScanActive);
                    }
                    mLastScanSettings = newScanSettings;
                    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                            mClock.elapsedRealtime() + SCAN_TIMEOUT_MS,
                            TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler);
                } else {
                    Log.e(TAG, "Failed to start scan, freqs=" + freqs);
                    // indicate scan failure async
                    mEventHandler.post(new Runnable() {
                            public void run() {
                                if (newScanSettings.singleScanEventHandler != null) {
                                    newScanSettings.singleScanEventHandler
                                            .onScanStatus(WifiNative.WIFI_SCAN_FAILED);
                                }
                            }
                        });
                    // TODO(b/27769665) background scans should be failed too if scans fail enough
                }
            } else if (isHwPnoScanRequired()) {
                newScanSettings.setHwPnoScan(mPnoEventHandler);
                if (startHwPnoScan()) {
                    mLastScanSettings = newScanSettings;
                } else {
                    Log.e(TAG, "Failed to start PNO scan");
                    // indicate scan failure async
                    mEventHandler.post(new Runnable() {
                        public void run() {
                            if (mPnoEventHandler != null) {
                                mPnoEventHandler.onPnoScanFailed();
                            }
                            // Clean up PNO state, we don't want to continue PNO scanning.
                            mPnoSettings = null;
                            mPnoEventHandler = null;
                        }
                    });
                }
            }
        }
    }

好吧上面的东西有点多,抽点重点出来看

            if ((newScanSettings.backgroundScanActive || newScanSettings.singleScanActive)
                    && !allFreqs.isEmpty()) {
                pauseHwPnoScan();
                Set<Integer> freqs = allFreqs.getSupplicantScanFreqs();
                //下一个scan的命令下去
                boolean success = mWifiNative.scan(freqs, hiddenNetworkIdSet);
                if (success) {
                    // TODO handle scan timeout
                    if (DBG) {
                        Log.d(TAG, "Starting wifi scan for freqs=" + freqs
                                + ", background=" + newScanSettings.backgroundScanActive
                                + ", single=" + newScanSettings.singleScanActive);
                    }
                    mLastScanSettings = newScanSettings;
                    mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                            mClock.elapsedRealtime() + SCAN_TIMEOUT_MS,
                            TIMEOUT_ALARM_TAG, mScanTimeoutListener, mEventHandler);
                } else {
                    Log.e(TAG, "Failed to start scan, freqs=" + freqs);
                    // indicate scan failure async
                    mEventHandler.post(new Runnable() {
                            public void run() {
                                if (newScanSettings.singleScanEventHandler != null) {
                                    newScanSettings.singleScanEventHandler
                                            .onScanStatus(WifiNative.WIFI_SCAN_FAILED);
                                }
                            }
                        });
                    // TODO(b/27769665) background scans should be failed too if scans fail enough
                }

最后跑到wifinative,这个和之前的差别不大,

    /**
     * Start a scan using wpa_supplicant for the given frequencies.
     * @param freqs list of frequencies to scan for, if null scan all supported channels.
     * @param hiddenNetworkIds List of hidden networks to be scanned for.
     */
    public boolean scan(Set<Integer> freqs, Set<Integer> hiddenNetworkIds) {
        String freqList = null;
        String hiddenNetworkIdList = null;
        if (freqs != null && freqs.size() != 0) {
            freqList = createCSVStringFromIntegerSet(freqs);
        }
        if (hiddenNetworkIds != null && hiddenNetworkIds.size() != 0) {
            hiddenNetworkIdList = createCSVStringFromIntegerSet(hiddenNetworkIds);
        }
        return scanWithParams(freqList, hiddenNetworkIdList);
    }

    private boolean scanWithParams(String freqList, String hiddenNetworkIdList) {
        StringBuilder scanCommand = new StringBuilder();
        scanCommand.append("SCAN TYPE=ONLY");
        if (freqList != null) {
            scanCommand.append(" freq=" + freqList);
        }
        if (hiddenNetworkIdList != null) {
            scanCommand.append(" scan_id=" + hiddenNetworkIdList);
        }
        return doBooleanCommand(scanCommand.toString());
    }

setScanResults

scanResults的上报,还是跟之前一样,接收supplicant的CMD。但是这个函数被重构了,写得更优雅了。用了一个类NetworkDetail来描述热点的信息。

总结

在scan模块,google主要添加了一个scanservice,独立出来一个service,添加了类似管理中心的东西,便于处理多种不同场景下的scan需求。在之前,wifi的scan确实引发了不少bug,希望重构之后,这个模块不要再打到其他模块了。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值