Android 4.2 Wifi Display 之 Settings 源码分析(二)

作者:mznewfacer  时间:2012年12月7日

     在上一回我们一块分析了WifiDisplay有关设备发现的部分,这一回将主要针对设备连接和建立数据流展开分析。

首先,回顾下应用层,当用户在搜寻完设备后,可以选择设备进行连接,当然正在进行连接或已经连接配对的设备,再次点击配置后,会弹出对话框供用户选择断开连接。

packages/apps/Settings/src/com/android/settings/wfd/WifiDisplaySettings.java

[java]  view plain copy
  1. public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,  
  2.            Preference preference) {  
  3.        if (preference instanceof WifiDisplayPreference) {  
  4.            WifiDisplayPreference p = (WifiDisplayPreference)preference;  
  5.            WifiDisplay display = p.getDisplay();  
  6.   
  7.            if (display.equals(mWifiDisplayStatus.getActiveDisplay())) {  
  8.                showDisconnectDialog(display);  
  9.            } else {  
  10.                mDisplayManager.connectWifiDisplay(display.getDeviceAddress());  
  11.            }  
  12.        }  
  13.   
  14.        return super.onPreferenceTreeClick(preferenceScreen, preference);  
  15.    }  

  如同设备发现的调用流程,当用户选择设备进行连接后,程序会调用DisplayManager的connectWifiDisplay()函数接口。该函数会进一步根据DisplayManagerGlobal提供的单实例对象调用AIDL提供的接口函数connectWifiDisplay(),这又是上一回已经提到过的调用模式。其实际的调用实现是Displaymanager service中提供的connectWifiDisplay()函数,

frameworks/base/services/java/com/android/server/display/DisplayManagerService.java

[java]  view plain copy
  1. public void connectWifiDisplay(String address) {  
  2.         if (address == null) {  
  3.             throw new IllegalArgumentException("address must not be null");  
  4.         }  
  5.   
  6.         final boolean trusted = canCallerConfigureWifiDisplay();  
  7.         final long token = Binder.clearCallingIdentity();  
  8.         try {  
  9.             synchronized (mSyncRoot) {  
  10.                 if (mWifiDisplayAdapter != null) {  
  11.                     mWifiDisplayAdapter.requestConnectLocked(address, trusted);  
  12.                 }  
  13.             }  
  14.         } finally {  
  15.             Binder.restoreCallingIdentity(token);  
  16.         }  
  17.     }  

   到此,我们容易发现连接WifiDisplay设备的函数调用流程与发现设备的流程一致,这里将不做多余解释(详见),在此会罗列出之后的基本流程。

frameworks/base/services/java/com/android/server/display/WifiDisplayAdapter.java

[java]  view plain copy
  1. public void requestConnectLocked(final String address, final boolean trusted) {  
  2.         if (DEBUG) {  
  3.             Slog.d(TAG, "requestConnectLocked: address=" + address + ", trusted=" + trusted);  
  4.         }  
  5.   
  6.         if (!trusted) {  
  7.             synchronized (getSyncRoot()) {  
  8.                 if (!isRememberedDisplayLocked(address)) {   //如果设备地址不在保存列表中则忽略不做处理  
  9.                    ...  
  10.                     return;  
  11.                 }  
  12.             }  
  13.         }  
  14.   
  15.         getHandler().post(new Runnable() {  
  16.             @Override  
  17.             public void run() {  
  18.                 if (mDisplayController != null) {  
  19.                     mDisplayController.requestConnect(address);  
  20.                 }  
  21.             }  
  22.         });  
  23.     }  

frameworks/base/services/java/com/android/server/display/WifiDisplayController.java

[java]  view plain copy
  1. public void requestConnect(String address) {  
  2.        for (WifiP2pDevice device : mAvailableWifiDisplayPeers) {  
  3.            if (device.deviceAddress.equals(address)) {  
  4.                connect(device);  
  5.            }  
  6.        }  
  7.    }  
  8.   
  9.    private void connect(final WifiP2pDevice device) {  
  10.        if (mDesiredDevice != null  
  11.                && !mDesiredDevice.deviceAddress.equals(device.deviceAddress)) {  //如果设备已经正在连接则返回  
  12.            if (DEBUG) {  
  13.               ...  
  14.            }  
  15.            return;  
  16.        }  
  17.   
  18.        if (mConnectedDevice != null  
  19.                && !mConnectedDevice.deviceAddress.equals(device.deviceAddress)  
  20.                && mDesiredDevice == null) {//如果设备已经连接则返回  
  21.            if (DEBUG) {  
  22.                 ...  
  23.            }  
  24.            return;  
  25.        }  
  26.   
  27.        mDesiredDevice = device;  
  28.        mConnectionRetriesLeft = CONNECT_MAX_RETRIES; //尝试连接最大次数  
  29.        updateConnection();  
  30.    }  

   接下来,我们将重点看一看updateConnection()函数,此函数是建立Wifidisplay连接,监听RTSP连接的核心实现函数。

[java]  view plain copy
  1.  private void updateConnection() {  
  2.        //在尝试连接到新设备时,需要通知系统这里已经与旧的设备断开连接  
  3.         if (mRemoteDisplay != null && mConnectedDevice != mDesiredDevice) {  
  4.             ...  
  5.             mRemoteDisplay.dispose();  //释放NativeRemoteDisplay资源停止监听  
  6.             mRemoteDisplay = null;   //监听返回对象置为空  
  7.             mRemoteDisplayInterface = null;   //监听端口置为空  
  8.             mRemoteDisplayConnected = false;  //连接标识为未连接  
  9.             mHandler.removeCallbacks(mRtspTimeout);//将挂起的mRtspTimeout线程从消息队列中移除  
  10.   
  11.             setRemoteSubmixOn(false);   //关闭远程混音重建模式  
  12.             unadvertiseDisplay();     
  13.         }  
  14.         if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) {  
  15.              ...  
  16.             unadvertiseDisplay();  
  17.   
  18.             final WifiP2pDevice oldDevice = mConnectedDevice;  
  19.             mWifiP2pManager.removeGroup(mWifiP2pChannel, new ActionListener() {  
  20.                 @Override  
  21.                 public void onSuccess() {  
  22.                     ...  
  23.                     next();  
  24.                 }  
  25.   
  26.                 @Override  
  27.                 public void onFailure(int reason) {  
  28.                    ...  
  29.                     next();  
  30.                 }  
  31.   
  32.                 private void next() {  
  33.                     if (mConnectedDevice == oldDevice) {  //确保连接设备已经不是旧的设备否则递归调用该函数  
  34.                         mConnectedDevice = null;  
  35.                         updateConnection();  
  36.                     }  
  37.                 }  
  38.             });  
  39.             return;   
  40.         }  
  41.   
  42.   
  43.         if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) {  
  44.             ...  
  45.             unadvertiseDisplay();  
  46.             mHandler.removeCallbacks(mConnectionTimeout);  
  47.   
  48.             final WifiP2pDevice oldDevice = mConnectingDevice;  
  49.             mWifiP2pManager.cancelConnect(mWifiP2pChannel, new ActionListener() {  //在尝试连接到新设备之前,取消正在进行的p2p连接  
  50.                 @Override  
  51.                 public void onSuccess() {  
  52.                     ...  
  53.                     next();  
  54.                 }  
  55.   
  56.                 @Override  
  57.                 public void onFailure(int reason) {  
  58.                     ...  
  59.                     next();  
  60.                 }  
  61.   
  62.                 private void next() {  
  63.                     if (mConnectingDevice == oldDevice) {  
  64.                         mConnectingDevice = null;  
  65.                         updateConnection();  
  66.                     }  
  67.                 }  
  68.             });  
  69.             return;   
  70.         }  
  71.     //  如果想断开连接,则任务结束  
  72.         if (mDesiredDevice == null) {  
  73.             unadvertiseDisplay();  
  74.             return;   
  75.         }  
  76.   
  77.         if (mConnectedDevice == null && mConnectingDevice == null) {  
  78.             Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName);  
  79.             mConnectingDevice = mDesiredDevice;  
  80.             WifiP2pConfig config = new WifiP2pConfig();  
  81.             config.deviceAddress = mConnectingDevice.deviceAddress;  
  82.             config.groupOwnerIntent = WifiP2pConfig.MIN_GROUP_OWNER_INTENT;  
  83.   
  84.             WifiDisplay display = createWifiDisplay(mConnectingDevice);  
  85.             advertiseDisplay(display, null000);  
  86.   
  87.             final WifiP2pDevice newDevice = mDesiredDevice;  
  88.             mWifiP2pManager.connect(mWifiP2pChannel, config, new ActionListener() {  
  89.       //以特定的配置信息开启P2P连接,如果当前设备不是P2P组的一部分,会建立P2P小组并发起连接请求;如果当前设备是现存P2P组的一部分,则加入该组的邀请会发送至该配对设备。  
  90.   
  91.                 @Override  
  92.                 public void onSuccess() {  
  93.         //为了防止连接还没有建立成功,这里设定了等待处理函数,如果在定长时间内还没有接受到WIFI_P2P_CONNECTION_CHANGED_ACTION广播,则按照handleConnectionFailure(true)处理。  
  94.                     Slog.i(TAG, "Initiated connection to Wifi display: " + newDevice.deviceName);  
  95.                     mHandler.postDelayed(mConnectionTimeout, CONNECTION_TIMEOUT_SECONDS * 1000);  
  96.                 }  
  97.   
  98.                 @Override  
  99.                 public void onFailure(int reason) {  
  100.                     if (mConnectingDevice == newDevice) {  
  101.                         Slog.i(TAG, "Failed to initiate connection to Wifi display: "  
  102.                                 + newDevice.deviceName + ", reason=" + reason);  
  103.                         mConnectingDevice = null;  
  104.                         handleConnectionFailure(false);  
  105.                     }  
  106.                 }  
  107.             });  
  108.             return;   
  109.         }  
  110.         // 根据连接的网络地址和端口号监听Rtsp流连接  
  111.         if (mConnectedDevice != null && mRemoteDisplay == null) {  
  112.             Inet4Address addr = getInterfaceAddress(mConnectedDeviceGroupInfo);  
  113.             if (addr == null) {  
  114.                 Slog.i(TAG, "Failed to get local interface address for communicating "  
  115.                         + "with Wifi display: " + mConnectedDevice.deviceName);  
  116.                 handleConnectionFailure(false);  
  117.                 return// done  
  118.             }  
  119.   
  120.             setRemoteSubmixOn(true);  
  121.   
  122.             final WifiP2pDevice oldDevice = mConnectedDevice;  
  123.             final int port = getPortNumber(mConnectedDevice);  
  124.             final String iface = addr.getHostAddress() + ":" + port;  
  125.             mRemoteDisplayInterface = iface;  
  126.   
  127.             Slog.i(TAG, "Listening for RTSP connection on " + iface  
  128.                     + " from Wifi display: " + mConnectedDevice.deviceName);  
  129.   
  130.             mRemoteDisplay = RemoteDisplay.listen(iface, new RemoteDisplay.Listener() {  
  131. //开始监听连接上的接口  
  132.                 @Override  
  133.                 public void onDisplayConnected(Surface surface,  
  134.                         int width, int height, int flags) {  
  135.                     if (mConnectedDevice == oldDevice && !mRemoteDisplayConnected) {  
  136.                         Slog.i(TAG, "Opened RTSP connection with Wifi display: "  
  137.                                 + mConnectedDevice.deviceName);  
  138.                         mRemoteDisplayConnected = true;  
  139.                         mHandler.removeCallbacks(mRtspTimeout);  
  140.   
  141.                         final WifiDisplay display = createWifiDisplay(mConnectedDevice);  
  142.                         advertiseDisplay(display, surface, width, height, flags);  
  143.                     }  
  144.                 }  
  145.   
  146.                 @Override  
  147.                 public void onDisplayDisconnected() {  
  148.                     if (mConnectedDevice == oldDevice) {  
  149.                         Slog.i(TAG, "Closed RTSP connection with Wifi display: "  
  150.                                 + mConnectedDevice.deviceName);  
  151.                         mHandler.removeCallbacks(mRtspTimeout);  
  152.                         disconnect();  
  153.                     }  
  154.                 }  
  155.   
  156.                 @Override  
  157.                 public void onDisplayError(int error) {  
  158.                     if (mConnectedDevice == oldDevice) {  
  159.                         Slog.i(TAG, "Lost RTSP connection with Wifi display due to error "  
  160.                                 + error + ": " + mConnectedDevice.deviceName);  
  161.                         mHandler.removeCallbacks(mRtspTimeout);  
  162.                         handleConnectionFailure(false);  
  163.                     }  
  164.                 }  
  165.             }, mHandler);  
  166.   
  167.             mHandler.postDelayed(mRtspTimeout, RTSP_TIMEOUT_SECONDS * 1000);  
  168.         }  
  169.     }  

     至此,我们已经了解了建立WifiDisplay连接的基本流程,当然可以继续向底层深入,只要用户选择尝试连接并且已经确认处于连接断开的状态,则会调用WifiP2pManager中的connect()接口函数,该函数会向Channel中发送CONNECT信号,并注册监听器监听相应结果。在进入P2pStateMachine状态机后,WifiP2pService会分为两种情况进行处理。如果当前的设备不是P2P组的成员,WifiP2pService会调用WifiNative类中的p2pConnect()函数,该函数会继续向底层调用,最终会调用wifi.cwifi_send_command()命令,把groupnegotiation请求发送至wpa_supplicant供其处理;如果这个设备已经是P2P组的成员,或者自己通过WifiNative类中的p2pGroupAdd()函数创建了一个组,那么会进入GroupCreatedState,进一步会调用WifiNative类中的p2pInvite()函数向设备发送邀请请求。具体的有关wpa_supplicant同底层驱动的交互,以及wpa_supplicant同WifiMonitor与WifiP2pService状态机之间的调用流程以后有机会再讨论。

在本文的最后,还想继续讨论一下监听RTSP连接的核心实现函数RemoteDisplay.listen(...),

frameworks/base/media/java/android/media/RemoteDisplay.java

[java]  view plain copy
  1. public static RemoteDisplay listen(String iface, Listener listener, Handler handler) {  
  2. ...  
  3.         RemoteDisplay display = new RemoteDisplay(listener, handler);  
  4.         display.startListening(iface);  
  5.         return display;  
  6.     }  
  7. 可以看到该监听函数会调用以下函数,并把监听端口作为参数进行传递,  
  8.  private void startListening(String iface) {  
  9.         mPtr = nativeListen(iface);  
  10.         if (mPtr == 0) {  
  11.             throw new IllegalStateException("Could not start listening for "  
  12.                     + "remote display connection on \"" + iface + "\"");  
  13.         }  
  14.         mGuard.open("dispose");    
  15.     }  

以上函数最终会调用JNI层的接口函数nativeListen()进行监听。至于CloseGuardmGuard.open(),不理解的话,我们就把它看作是Android提供的一种资源清理机制。

接下来,可以具体看一下RemoteDisplay在JNI层的接口实现,

frameworks/base/core/jni/android_media_RemoteDisplay.cpp

[java]  view plain copy
  1. static jint nativeListen(JNIEnv* env, jobject remoteDisplayObj, jstring ifaceStr) {  
  2.     ScopedUtfChars iface(env, ifaceStr);  //通过智能指针的方式将string类型转化为只读的UTF chars类型  
  3.   
  4.     sp<IServiceManager> sm = defaultServiceManager();  
  5.     sp<IMediaPlayerService> service = interface_cast<IMediaPlayerService>(  
  6.             sm->getService(String16("media.player")));   
  7. //用service manager获得 media player服务的代理实例,即通过interface_cast将其转化成BpMediaPlayerService  (Bridge模式)  
  8.     if (service == NULL) {  
  9.         ALOGE("Could not obtain IMediaPlayerService from service manager");  
  10.         return 0;  
  11.     }  
  12.     sp<NativeRemoteDisplayClient> client(new NativeRemoteDisplayClient(env, remoteDisplayObj));  
  13.     sp<IRemoteDisplay> display = service->listenForRemoteDisplay(  
  14.             client, String8(iface.c_str()));  
  15. //调用BpMediaPlayerService提供的接口函数,与服务端BnMediaPlayerService进行通讯  
  16.     if (display == NULL) {  
  17.         ALOGE("Media player service rejected request to listen for remote display '%s'.",  
  18.                 iface.c_str());  
  19.         return 0;  
  20.     }  
  21.   
  22.     NativeRemoteDisplay* wrapper = new NativeRemoteDisplay(display, client);  
  23.     return reinterpret_cast<jint>(wrapper);  
  24. }  

    这里采用了Binder通信机制,BpMediaPlayerService继承BpInterface<IMediaPlayerService>作为代理端,采用Bridge模式调用listenForRemoteDisplay()接口函数将上层的监听接口以及实例化的NativeRemoteDisplayClient代理对象传递至服务端BnMediaPlayerService进行处理。

/frameworks/av/media/libmedia/IMediaPlayerService.cpp

[cpp]  view plain copy
  1. class BpMediaPlayerService: public BpInterface<IMediaPlayerService>  
  2. {  
  3.   public:  
  4.         …  
  5.   virtual sp<IRemoteDisplay> listenForRemoteDisplay(const sp<IRemoteDisplayClient>& client,  
  6.             const String8& iface)  
  7.     {  
  8.         Parcel data, reply;  
  9.         data.writeInterfaceToken(IMediaPlayerService::getInterfaceDescriptor());  
  10.         data.writeStrongBinder(client->asBinder());  
  11.         data.writeString8(iface);  
  12.         remote()->transact(LISTEN_FOR_REMOTE_DISPLAY, data, &reply);  //向服务端BnMediaPlayerService发送LISTEN_FOR_REMOTE_DISPLAY 处理命令  
  13.         return interface_cast<IRemoteDisplay>(reply.readStrongBinder());  
  14.     }  
  15. };  

   进一步可以看到,NativeRemoteDisplayClient继承于BnRemoteDisplayClient,其实这是IRemoteDisplayClient接口的服务端实现。该类提供了三个接口函数onDisplayConnected()、onDisplayDisconnected()、onDisplayError()是frameworks/base/media/java/android/media/RemoteDisplay.java中RemoteDisplay.Listener{}的三个监听函数在JNI层的实现,特别的,对于onDisplayConnected()函数而言,调用android_view_Surface_createFromISurfaceTexture()函数创建surfaceObj并将其向RemoteDisplay中注册的监听线程传递并进行回调。

frameworks/base/core/jni/android_media_RemoteDisplay.cpp

[cpp]  view plain copy
  1. virtual void onDisplayConnected(const sp<ISurfaceTexture>& surfaceTexture,  
  2.             uint32_t width, uint32_t height, uint32_t flags) {  
  3.         JNIEnv* env = AndroidRuntime::getJNIEnv();  
  4.         jobject surfaceObj = android_view_Surface_createFromISurfaceTexture(env, surfaceTexture);   
  5.   //跟据当前获取的media server的surface texture来创建Surface对象  
  6.         if (surfaceObj == NULL) {  
  7.             ...  
  8.             return;  
  9.         }  
  10.   
  11.         env->CallVoidMethod(mRemoteDisplayObjGlobal,  
  12.                 gRemoteDisplayClassInfo.notifyDisplayConnected,  
  13.                 surfaceObj, width, height, flags);   //将Suface对象作为参数传递至notifyDisplayConnected函数用于监听函数的回调  
  14.         env->DeleteLocalRef(surfaceObj);  
  15.         checkAndClearExceptionFromCallback(env, "notifyDisplayConnected");  
  16.     }  

    接下来,我们继续来看服务端BnMediaPlayerService的实现,其中onTransact函数用于接收来自BpMediaPlayerService发送的命令,如果命令为LISTEN_FOR_REMOTE_DISPLAY,则会读取相应数据并作为参数进行传递。这里的listenForRemoteDisplay()函数是纯虚函数,其实现是由派生类MediaPlayerService来完成的。


[cpp]  view plain copy
  1. status_t BnMediaPlayerService::onTransact(  
  2.     uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)  
  3. {  
  4.     switch (code) {  
  5.        …  
  6.     case LISTEN_FOR_REMOTE_DISPLAY: {  
  7.             CHECK_INTERFACE(IMediaPlayerService, data, reply);  
  8.             sp<IRemoteDisplayClient> client(  
  9.                     interface_cast<IRemoteDisplayClient>(data.readStrongBinder()));  
  10.             String8 iface(data.readString8());  
  11.             sp<IRemoteDisplay> display(listenForRemoteDisplay(client, iface));//调用纯虚函数接口,运行时实际调用派生类MediaPlayerService的函数实现  
  12.             reply->writeStrongBinder(display->asBinder());  
  13.             return NO_ERROR;  
  14.         } break;  
  15.         default:  
  16.             return BBinder::onTransact(code, data, reply, flags);  
  17.     }  
  18. }  

最后,来看一看该函数的实际实现,

frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp

[cpp]  view plain copy
  1. sp<IRemoteDisplay> MediaPlayerService::listenForRemoteDisplay(  
  2.         const sp<IRemoteDisplayClient>& client, const String8& iface) {  
  3.     if (!checkPermission("android.permission.CONTROL_WIFI_DISPLAY")) {  
  4.       //检查是否有WIFI Display权限  
  5.         return NULL;  
  6.     }  
  7.   
  8.     return new RemoteDisplay(client, iface.string());  //直接调用 RemoteDisplay构造函数来开启Wifi display source端  
  9. }  

其中,RemoteDisplay继承于BnRemoteDisplay,也采取了Binder通信机制,代理端BpRemoteDisplay与服务端BnRemoteDisplay的接口实现详见frameworks/av/media/libmedia/IRemoteDisplay.cpp。这里,值得一提的是,函数listenForRemoteDisplay()假设在同一时刻连接到指定网络端口iface的remotedisplay设备最多只有一个。换句话说,在同一时刻只有一个设备能作为WifiDisplay source端设备进行播放。

最后,我们来看一看开启Wifidisplay source端的这个构造函数,

frameworks/av/media/libmediaplayerservice/RemoteDisplay.cpp

[cpp]  view plain copy
  1. RemoteDisplay::RemoteDisplay(  
  2.         const sp<IRemoteDisplayClient> &client, const char *iface)  
  3.     : mLooper(new ALooper),  
  4.       mNetSession(new ANetworkSession),  
  5.       mSource(new WifiDisplaySource(mNetSession, client)) {  
  6.     mLooper->setName("wfd_looper");  
  7.     mLooper->registerHandler(mSource);  //注册了Wifi display 处理线程  
  8.   
  9.     mNetSession->start();  //初始化数据管道,启动NetworkThread线程,进入threadLoop中监听数据流变化等待处理  
  10.     mLooper->start();   //开启消息处理管理线程  
  11.   
  12.     mSource->start(iface);    //将网络端口作为消息载体进行传递处理,并等待响应结果,完成与Wifi Display source端开启播放的相关工作  
  13. }  

其中mLooper,mNetSession, mSource分别为sp<ALooper>mLooper,sp<ANetworkSession>mNetSession以及sp<WifiDisplaySource>mSource等三个强指针,对强指针概念不清的请见此。此处是利用构造函数的初始化列表将这三个强指针指向这三个new出来的对象。之后便是利用这三个指针,调用类中的方法以开启Wifidisplay source端进行播放。这里,ALooper是关于线程以及消息队列等待处理管理相关的一个类。ANetworkSessions是管理所有与数据报文和数据流相关socket的一个单线程帮助类。在此处,该类负责管理与WifiDisplay播放相关的socket,其中相关的数据传递和消息返回通过AMessage类对象和方法进行。WifiDisplaySource光看命名就知道,其主要负责WifiDisplaysource端的开启关闭,以及与其相关的建立Rtsp服务器,管理所有支持的协议连接、数据流传递以及各个状态之间转换处理等内容。此外,该类还定义了关闭WifiDisplay source端,停止相关线程、关闭socket以及释放资源等内容。

     至此,有关WifiDisplay设备连接和建立数据流的流程已经交代清楚了,可以看到应用层建立的连接是与source端相关的。Sink端的主程序在frameworks/av/media/libstagefright/wifi-display/wfd.cpp中,与sink端实现相关的程序在frameworks/av/media/libstagefright/wifi-display/sink目录下面。关于source如何建立rtsp连接,开始通信,各个状态之间的转换以及与sink端的交互将在下回介绍。


原文在http://blog.csdn.net/mznewfacer/article/details/8268930

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值