wifi连接流程

32 篇文章 0 订阅
29 篇文章 16 订阅

在平时的android开发中,经常会用到wifi相关操作,其实就应用而言,系统都是通过WifiManager对应的api来进行对应的操作
我们可以从源码的frameworks/base/api目录中看到当前系统提供的所有api

public class WifiManager {
    method public int addNetwork(android.net.wifi.WifiConfiguration);
    method public static int calculateSignalLevel(int, int);
    method public void cancelWps(android.net.wifi.WifiManager.WpsCallback);
    method public static int compareSignalLevel(int, int);
    method public android.net.wifi.WifiManager.MulticastLock createMulticastLock(java.lang.String);
    method public android.net.wifi.WifiManager.WifiLock createWifiLock(int, java.lang.String);
    method public android.net.wifi.WifiManager.WifiLock createWifiLock(java.lang.String);
    method public boolean disableNetwork(int);
    method public boolean disconnect();
    method public boolean enableNetwork(int, boolean);
    method public java.util.List<android.net.wifi.WifiConfiguration> getConfiguredNetworks();
    method public android.net.wifi.WifiInfo getConnectionInfo();
    method public android.net.DhcpInfo getDhcpInfo();
    method public java.util.List<android.net.wifi.ScanResult> getScanResults();
    method public int getWifiState();
    method public boolean is5GHzBandSupported();
    method public boolean isDeviceToApRttSupported();
    method public boolean isEnhancedPowerReportingSupported();
    method public boolean isP2pSupported();
    method public boolean isPreferredNetworkOffloadSupported();
    method public boolean isScanAlwaysAvailable();
    method public boolean isTdlsSupported();
    method public boolean isWifiEnabled();
    method public boolean pingSupplicant();
    method public boolean reassociate();
    method public boolean reconnect();
    method public boolean removeNetwork(int);
    method public boolean saveConfiguration();
    method public void setTdlsEnabled(java.net.InetAddress, boolean);
    method public void setTdlsEnabledWithMacAddress(java.lang.String, boolean);
    method public boolean setWifiEnabled(boolean);
    method public boolean startScan();
    method public void startWps(android.net.wifi.WpsInfo, android.net.wifi.WifiManager.WpsCallback);
    method public int updateNetwork(android.net.wifi.WifiConfiguration);
    field public static final java.lang.String ACTION_PICK_WIFI_NETWORK = "android.net.wifi.PICK_WIFI_NETWORK";
    field public static final java.lang.String ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE = "android.net.wifi.action.REQUEST_SCAN_ALWAYS_AVAILABLE";
    field public static final int ERROR_AUTHENTICATING = 1; // 0x1
    field public static final java.lang.String EXTRA_BSSID = "bssid";
    field public static final java.lang.String EXTRA_NETWORK_INFO = "networkInfo";
    field public static final java.lang.String EXTRA_NEW_RSSI = "newRssi";
    field public static final java.lang.String EXTRA_NEW_STATE = "newState";
    field public static final java.lang.String EXTRA_PREVIOUS_WIFI_STATE = "previous_wifi_state";
    field public static final java.lang.String EXTRA_RESULTS_UPDATED = "resultsUpdated";
    field public static final java.lang.String EXTRA_SUPPLICANT_CONNECTED = "connected";
    field public static final java.lang.String EXTRA_SUPPLICANT_ERROR = "supplicantError";
    field public static final java.lang.String EXTRA_WIFI_INFO = "wifiInfo";
    field public static final java.lang.String EXTRA_WIFI_STATE = "wifi_state";
    field public static final java.lang.String NETWORK_IDS_CHANGED_ACTION = "android.net.wifi.NETWORK_IDS_CHANGED";
    field public static final java.lang.String NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE";
    field public static final java.lang.String RSSI_CHANGED_ACTION = "android.net.wifi.RSSI_CHANGED";
    field public static final java.lang.String SCAN_RESULTS_AVAILABLE_ACTION = "android.net.wifi.SCAN_RESULTS";
    field public static final java.lang.String SUPPLICANT_CONNECTION_CHANGE_ACTION = "android.net.wifi.supplicant.CONNECTION_CHANGE";
    field public static final java.lang.String SUPPLICANT_STATE_CHANGED_ACTION = "android.net.wifi.supplicant.STATE_CHANGE";
    field public static final int WIFI_MODE_FULL = 1; // 0x1
    field public static final int WIFI_MODE_FULL_HIGH_PERF = 3; // 0x3
    field public static final int WIFI_MODE_SCAN_ONLY = 2; // 0x2
    field public static final java.lang.String WIFI_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_STATE_CHANGED";
    field public static final int WIFI_STATE_DISABLED = 1; // 0x1
    field public static final int WIFI_STATE_DISABLING = 0; // 0x0
    field public static final int WIFI_STATE_ENABLED = 3; // 0x3
    field public static final int WIFI_STATE_ENABLING = 2; // 0x2
    field public static final int WIFI_STATE_UNKNOWN = 4; // 0x4
    field public static final int WPS_AUTH_FAILURE = 6; // 0x6
    field public static final int WPS_OVERLAP_ERROR = 3; // 0x3
    field public static final int WPS_TIMED_OUT = 7; // 0x7
    field public static final int WPS_TKIP_ONLY_PROHIBITED = 5; // 0x5
    field public static final int WPS_WEP_PROHIBITED = 4; // 0x4
  }

WifiManager和WifiServiceImpl远程通信

其实关于WifiManager操作基本都是通过远程服务端实现,这一点稍后讨论,WifiManager中的常用方法:

// 连接到指定网络
public void connect(int networkId, ActionListener listener) {
        if (networkId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
}

// 保存网络
public void save(WifiConfiguration config, ActionListener listener) {
        if (config == null) throw new IllegalArgumentException("config cannot be null");
        validateChannel();
        sAsyncChannel.sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
}

// 忘记网络
public void forget(int netId, ActionListener listener) {
        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(FORGET_NETWORK, netId, putListener(listener));
}

可以面这种操作网络的实际上都是通过sAsyncChannel中进一步实现的,sAsyncChannel是一个AsyncChannel类型,其实AsyncChannel是连接WifiManager和WifiServiceImpl的桥梁

AsyncChannel

在WifiManager构造方法中,调用了init()方法来初始化一些参数

private void init() {
        synchronized (sThreadRefLock) {
            if (++sThreadRefCount == 1) {
                // 获取服务端(WifiServiceImpl)中对应的mClientHandler,WifiServiceImpl继承自IWifiManager.Stub
                Messenger messenger = getWifiServiceMessenger();
                if (messenger == null) {
                    sAsyncChannel = null;
                    return;
                }

                sHandlerThread = new HandlerThread("WifiManager");
                sAsyncChannel = new AsyncChannel();
                // CountDownLatch是一个同步辅助类,犹如倒计时计数器,创建对象时通过构造方法设置初始值,调用CountDownLatch对象的await()方法则处于等待状态,调用countDown()方法就将计数器减1,当计数到达0时,则所有等待者或单个等待者开始执行。
                sConnected = new CountDownLatch(1);

                sHandlerThread.start();
                Handler handler = new ServiceHandler(sHandlerThread.getLooper());
                // 1. 调用连接AsyncChannel.connect进行半连接
                sAsyncChannel.connect(mContext, handler, messenger);
                try {
                    sConnected.await();
                } catch (InterruptedException e) {
                    Log.e(TAG, "interrupted wait at init");
                }
            }
        }
}

上面代码主要做了两件事:
1.通过getWifiServiceMessenger()获取WifiServiceImpl中对应的mClientHandler
2.调用连接AsyncChannel.connect进行半连接
为什么第二步是半连接?分析代码吧

我们看下AsyncChannel#connect方法

public class AsyncChannel {
    ....
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // 连接srcHandler和dstMessenger
        connected(srcContext, srcHandler, dstMessenger);
        // 在replyHalfConnected中会发送CMD_CHANNEL_HALF_CONNECTED到srcHandler处理
        replyHalfConnected(STATUS_SUCCESSFUL);
}

public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        if (DBG) log("connected srcHandler to the dstMessenger  E");

        // Initialize source fields
        mSrcContext = srcContext;
        mSrcHandler = srcHandler;
        mSrcMessenger = new Messenger(mSrcHandler);

        // 这里的dstMessenger就是传递过来的WifiServiceImpl中对应的mClientHandler
        mDstMessenger = dstMessenger;

        if (DBG) log("connected srcHandler to the dstMessenger X");
}


private void replyHalfConnected(int status) {
        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
        msg.arg1 = status;
        msg.obj = this;
        msg.replyTo = mDstMessenger;
        // 创建一个死亡监听,当当前mDstMessenger对应的binder死亡以后,会在DeathMonitor中发送一个"STATUS_REMOTE_DISCONNECTION"消息
        if (mConnection == null) {
            mDeathMonitor = new DeathMonitor();
            try {
                mDstMessenger.getBinder().linkToDeath(mDeathMonitor, 0);
            } catch (RemoteException e) {
                mDeathMonitor = null;
                // Override status to indicate failure
                msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
            }
        }
        // 发送CMD_CHANNEL_HALF_CONNECTED消息到mSrcHandler也就是在init方法中1.中传递进来的ServiceHandler
        mSrcHandler.sendMessage(msg);
    }
    ....
}

上述mSrcHandler.sendMessage(msg);中mSrcHandler是WifiManager$ServiceHandler的一个内部类

private static class ServiceHandler extends Handler {
        ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message message) {
            Object listener = removeListener(message.arg2);
            switch (message.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
                    if (message.arg1 == AsyncChannel) {
                        sAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
                    } else {
                        sAsyncChannel = null;
                    }
                    sConnected.countDown();
                    break;
           }
      }
      ....
}

在ServiceHandler处理” CMD_CHANNEL_HALF_CONNECTED”消息,会继续通过AsyncChannel发送一个”CMD_CHANNEL_FULL_CONNECTION”消息

public void sendMessage(Message msg) {
        // 2.这里会向mDstMessenger对应的handler发送一条消息,然后由其处理
        msg.replyTo = mSrcMessenger;
        try {
            mDstMessenger.send(msg);
        } catch (RemoteException e) {
            replyDisconnected(STATUS_SEND_UNSUCCESSFUL);
        }
}

上述mDstMessenger就是在1.处传递的Messenger,也就是通过WifiServiceImpl#getWifiServiceMessenger的

public Messenger getWifiServiceMessenger() {
        enforceAccessPermission();
        enforceChangePermission();
        return new Messenger(mClientHandler);
}

因此,2.处发送的”CMD_CHANNEL_FULL_CONNECTION”消息,将会交给WifiServiceImpl中的ClientHandler来处理

看下WifiServiceImpl的内部类ClientHandler,ClientHandler#handleMessage方法

public void handleMessage(Message msg) {
    ....
    public void handleMessage(Message msg) {
        case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
                    AsyncChannel ac = new AsyncChannel();
                    // 这里的msg.replyTo就是上一步的mSrcMessenger
                    ac.connect(mContext, this, msg.replyTo);
                    break;
                }
    }
    ....

}

此时程序又走了一边AsyncChannel#connect方法

public class AsyncChannel {
    ....
public void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {
        // 连接srcHandler(WifiServiceImpl$ClientHandler)和dstMessenger(对应WifiManager$ServiceHandler)
        connected(srcContext, srcHandler, dstMessenger);
        // 在replyHalfConnected中会发送CMD_CHANNEL_HALF_CONNECTED到ClientHandler处理
        replyHalfConnected(STATUS_SUCCESSFUL);
}
只不过此时的mSrcHandle就是ClientHandler自己,mDstMessenger对应处理的Handler是WifiManager中的ServiceHandler,在replyHalfConnected方法中
private void replyHalfConnected(int status) {
        Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);
        msg.arg1 = status;
        msg.obj = this;
        msg.replyTo = mDstMessenger;
        // 创建一个死亡监听,当当前mDstMessenger对应的binder死亡以后,会在DeathMonitor中发送一个"STATUS_REMOTE_DISCONNECTION"消息
        if (mConnection == null) {
            mDeathMonitor = new DeathMonitor();
            try {
                mDstMessenger.getBinder().linkToDeath(mDeathMonitor, 0);
            } catch (RemoteException e) {
                mDeathMonitor = null;
                // Override status to indicate failure
                msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;
            }
        }
        // 发送CMD_CHANNEL_HALF_CONNECTED消息到ClientHandler
        mSrcHandler.sendMessage(msg);
    }
    ....
}

所以这次发出的”CMD_CHANNEL_HALF_CONNECTED”消息,交给了WifiServiceImpl$ClientHandler处理

public void handleMessage(Message msg) {
            switch (msg.what) {
                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {
                    if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
                        // 在WifiTrafficPoller中将当前客户端的所有Messenger(和WifiManager$ServiceHandler相关联的),添加到mClients对应的list集合中
                        mTrafficPoller.addClient(msg.replyTo);
                    } else {
                        Slog.e(TAG, "Client connection failure, error=" + msg.arg1);
                    }
                    break;
}

到此为止WifiManager和WifiServiceImpl通过AsyncChannel(实质上是通过messenger和handler)建立的连接就完成了,这里类似于TCP中的三次握手,猜测是确保建立连接万无一失

总结一下:

  • 在WifiManager的init方法中,通过AsyncChannel的connect方法

    a. 先发送CMD_CHANNEL_HALF_CONNECTED到自己WifiManager内部类ServiceHandler的处理
    b. 在WifiManager内部类ServiceHandler处理CMD_CHANNEL_HALF_CONNECTED消息时候,发送CMD_CHANNEL_FULL_CONNECTION消息,由于上一步在AsyncChannel#connect方法中的初始化,这里会交给WifiServiceImpl内部类ClientHandler处理

  • 在WifiServiceImpl内部类ClientHandler处理”CMD_CHANNEL_FULL_CONNECTION”消息

    会再次建立连接,只不过此时的mSrcHandle就是ClientHandler自己,mDstMessenger对应处理的Handler是WifiManager中的ServiceHandler,最终会在Service服务端的WifiTrafficPoller中将当前和客户端建立的所有Messenger(和WifiManager内部类ServiceHandler相关联的),添加到mClients对应的list集合中

如下图:
这里写图片描述

连接流程

WifiManager#connect

public void connect(WifiConfiguration config, ActionListener listener) {
        if (config == null) throw new IllegalArgumentException("config cannot be null");
        validateChannel();
        sAsyncChannel.sendMessage(CONNECT_NETWORK, WifiConfiguration.INVALID_NETWORK_ID,
                putListener(listener), config);
}

这里通过sAsyncChannel发送了一个”CONNECT_NETWORK”消息到WifiServiceImpl服务端处理,在WifiServiceImpl$ClientHandler中对于CONNECT_NETWORK消息进一步交给WifiStateMachine处理

class ConnectModeState extends State {
    public boolean processMessage(Message message) {
         case WifiManager.CONNECT_NETWORK:
                    /**
                     *  The connect message can contain a network id passed as arg1 on message or
                     * or a config passed as obj on message.
                     * For a new network, a config is passed to create and connect.
                     * For an existing network, a network id is passed
                     */
                    netId = message.arg1;
                    config = (WifiConfiguration) message.obj;
                    mWifiConnectionStatistics.numWifiManagerJoinAttempt++;
                    boolean updatedExisting = false;

                    /* Save the network config */
                    if (config != null) {
                        // When connecting to an access point, WifiStateMachine wants to update the
                        // relevant config with administrative data. This update should not be
                        // considered a 'real' update, therefore lockdown by Device Owner must be
                        // disregarded.
                        if (!recordUidIfAuthorized(config, message.sendingUid,
                                /* onlyAnnotate */ true)) {
                            replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                           WifiManager.NOT_AUTHORIZED);
                            break;
                        }

                        String configKey = config.configKey(true /* allowCached */);
                        WifiConfiguration savedConfig =
                                mWifiConfigStore.getWifiConfiguration(configKey);
                        if (savedConfig != null) {
                            config = savedConfig;
                            logd("CONNECT_NETWORK updating existing config with id=" +
                                    config.networkId + " configKey=" + configKey);
                            config.ephemeral = false;
                            config.autoJoinStatus = WifiConfiguration.AUTO_JOIN_ENABLED;
                            updatedExisting = true;
                        }
                        // 1.保存当前网络
                        result = mWifiConfigStore.saveNetwork(config, message.sendingUid);
                        netId = result.getNetworkId();
                    }
                    config = mWifiConfigStore.getWifiConfiguration(netId);

                    if (config == null) {
                        logd("CONNECT_NETWORK no config for id=" + Integer.toString(netId) + " "
                                + mSupplicantStateTracker.getSupplicantStateName() + " my state "
                                + getCurrentState().getName());
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.ERROR);
                        break;
                    } else {
                        String wasSkipped = config.autoJoinBailedDueToLowRssi ? " skipped" : "";
                    }

                    autoRoamSetBSSID(netId, "any");

                    if (message.sendingUid == Process.WIFI_UID
                        || message.sendingUid == Process.SYSTEM_UID) {
                        clearConfigBSSID(config, "CONNECT_NETWORK");
                    }

                    if (deferForUserInput(message, netId, true)) {
                        break;
                    } else if (mWifiConfigStore.getWifiConfiguration(netId).userApproved ==
                                                                    WifiConfiguration.USER_BANNED) {
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.NOT_AUTHORIZED);
                        break;
                    }

                    mAutoRoaming = WifiAutoJoinController.AUTO_JOIN_IDLE;

                    boolean persist =
                        mWifiConfigStore.checkConfigOverridePermission(message.sendingUid);
                    mWifiAutoJoinController.updateConfigurationHistory(netId, true, persist);

                    mWifiConfigStore.setLastSelectedConfiguration(netId);

                    didDisconnect = false;
                    if (mLastNetworkId != WifiConfiguration.INVALID_NETWORK_ID
                            && mLastNetworkId != netId) {
                        /** Supplicant will ignore the reconnect if we are currently associated,
                         * hence trigger a disconnect
                         */
                        didDisconnect = true;
                        mWifiNative.disconnect();
                    }

                    // Make sure the network is enabled, since supplicant will not reenable it
                    mWifiConfigStore.enableNetworkWithoutBroadcast(netId, false);
                    // 2.选择当前网络,通过mWifiNative.reconnect()连接
                    if (mWifiConfigStore.selectNetwork(config, /* updatePriorities = */ true,
                            message.sendingUid) && mWifiNative.reconnect()) {
                        lastConnectAttemptTimestamp = System.currentTimeMillis();
                        targetWificonfiguration = mWifiConfigStore.getWifiConfiguration(netId);

                        /* The state tracker handles enabling networks upon completion/failure */
                        mSupplicantStateTracker.sendMessage(WifiManager.CONNECT_NETWORK);
                        3. 回调回去WifiManager$ServiceHandler处理
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_SUCCEEDED);
                        if (didDisconnect) {
                            /* Expect a disconnection from the old connection */
                            transitionTo(mDisconnectingState);
                        } else if (updatedExisting && getCurrentState() == mConnectedState &&
                                getCurrentWifiConfiguration().networkId == netId) {
                            // Update the current set of network capabilities, but stay in the
                            // current state.
                            updateCapabilities(config);
                        } else {
                            /**
                             *  Directly go to disconnected state where we
                             * process the connection events from supplicant
                             **/
                            transitionTo(mDisconnectedState);
                        }
                    } else {
                        loge("Failed to connect config: " + config + " netId: " + netId);
                        replyToMessage(message, WifiManager.CONNECT_NETWORK_FAILED,
                                WifiManager.ERROR);
                        break;
                    }
                    break;
     }
}

忘记网络

public void forget(int netId, ActionListener listener) {
        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
        validateChannel();
        sAsyncChannel.sendMessage(FORGET_NETWORK, netId, putListener(listener));
}

同样的发送消息到WifiServiceImpl服务端,在由WifiServiceImpl发送消息到WifiStateMachine

case WifiManager.FORGET_NETWORK:
                    // Debug only, remember last configuration that was forgotten
                    WifiConfiguration toRemove
                            = mWifiConfigStore.getWifiConfiguration(message.arg1);
                    if (toRemove == null) {
                        lastForgetConfigurationAttempt = null;
                    } else {
                        lastForgetConfigurationAttempt = new WifiConfiguration(toRemove);
                    }
                    // check that the caller owns this network
                    netId = message.arg1;

                    if (!mWifiConfigStore.canModifyNetwork(message.sendingUid, netId,
                            /* onlyAnnotate */ false)) {
                        logw("Not authorized to forget network "
                             + " cnid=" + netId
                             + " uid=" + message.sendingUid);
                        replyToMessage(message, WifiManager.FORGET_NETWORK_FAILED,
                                WifiManager.NOT_AUTHORIZED);
                        break;
                    }
                    // 忘记网络
                    if (mWifiConfigStore.forgetNetwork(message.arg1)) {
                        replyToMessage(message, WifiManager.FORGET_NETWORK_SUCCEEDED);
                        broadcastWifiCredentialChanged(WifiManager.WIFI_CREDENTIAL_FORGOT,
                                (WifiConfiguration) message.obj);
                    } else {
                        loge("Failed to forget network");
                        replyToMessage(message, WifiManager.FORGET_NETWORK_FAILED,
                                WifiManager.ERROR);
                    }
                    break;

其他的操作都是类似的,算是简单的一个记录,今天就到这里了。

Android的WiFi连接流程图主要包括以下步骤: 1. 打开WiFi:用户进入设置界面,打开设备的WiFi开关,使设备开始搜索可用的WiFi网络。 2. 搜索和扫描:Android设备开始搜索周围的WiFi网络,扫描到的网络信息将会显示在WiFi设置列表中,包括网络名称(SSID)、信号强度等。 3. 用户选择网络:用户根据列表中显示的网络信息,选择要连接WiFi网络。 4. 请求连接:Android设备通过向选择的WiFi网络发送请求连接的请求,包括认证信息和其他必要的参数。 5. 连接认证:WiFi网络收到连接请求后,会进行身份验证,包括密码验证、MAC地址过滤等。如果认证通过,网络将发送认可的连接响应。 6. IP地址获取:一旦连接成功,Android设备将向WiFi网络请求分配一个IP地址,以便在网络上进行通信。 7. 地址分配:WiFi网络会为设备分配一个合法的IP地址,并将该信息通过DHCP(动态主机配置协议)返回给设备。 8. 连接成功:设备接收到IP地址和其他网络配置信息后,与WiFi网络建立连接成功。 9. 数据传输:现在,设备可以通过WiFi网络进行数据传输,包括浏览互联网、收发电子邮件等。 10. 断开连接:当用户关闭设备的WiFi开关或者设备离开WiFi网络的范围时,设备会与WiFi网络断开连接。 总的来说,Android的WiFi连接流程图涵盖了搜索、选择、连接认证、IP地址获取、数据传输等多个环节,确保设备可以顺利地连接和使用WiFi网络。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值