• 初探Android S 双STA


    因为特性研发需求,回归framework层看些相关的东西(组里能做fwk开发的少之又少)
    以下是对双STA android fwk实现的一些研读(高效起见,不拿Andoird R的源码做对比了(双STA是Android S新增特性))

    写这篇文章时参考 Wi-Fi STA/STA并发

    双STA区分双连接,首先是针对原来单一的 ClientModeManager 补充了
    Primary + Second 的设计

    一些配置相关
    //ActiveModeWarden.java
        /**
         * @return Returns whether the device can support at least two concurrent client mode managers
         * and the local only use-case is enabled.
         */
        public boolean isStaStaConcurrencySupportedForLocalOnlyConnections() {
            return mWifiNative.isStaStaConcurrencySupported()
                    && mContext.getResources().getBoolean(
                            R.bool.config_wifiMultiStaLocalOnlyConcurrencyEnabled);
        }
    
        /**
         * @return Returns whether the device can support at least two concurrent client mode managers
         * and the mbb wifi switching is enabled.
         */
        public boolean isStaStaConcurrencySupportedForMbb() {
            return mWifiNative.isStaStaConcurrencySupported()
                    && mContext.getResources().getBoolean(
                            R.bool.config_wifiMultiStaNetworkSwitchingMakeBeforeBreakEnabled);
        }
    
        /**
         * @return Returns whether the device can support at least two concurrent client mode managers
         * and the restricted use-case is enabled.
         */
        public boolean isStaStaConcurrencySupportedForRestrictedConnections() {
            return mWifiNative.isStaStaConcurrencySupported()
                    && mContext.getResources().getBoolean(
                            R.bool.config_wifiMultiStaRestrictedConcurrencyEnabled);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    WifiNative是按 driver capa来判断是否支持的;另外有三种开关在config.xml中
    以MTK机器为例:
    /vendor/mediatek/proprietary/packages/overlay/vendor/WifiResOverlay/concurrency/dual_sta/res/values/config.xml
    /packages/modules/Wifi/service/ServiceWifiResources/res/values/config.xml

    mWifiNative.isStaStaConcurrencySupported() ->> 取决于 HalDeviceManager对接口combo的配置(要允许 [STA, 2])

    //WifiVendorHal.java
    	/**
    	 * Returns whether STA + STA concurrency is supported or not.
    	 */
    	public boolean isStaStaConcurrencySupported() {
    	    synchronized (sLock) {
    	        return mHalDeviceManager.canSupportIfaceCombo(new SparseArray<Integer>() {{
    	                put(IfaceType.STA, 2);
    	            }});
    	    }
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    触发连接
    //WifiServiceImpl.java
    public void connect(WifiConfiguration config, int netId, @Nullable IActionListener callback){
    	mMakeBeforeBreakManager.stopAllSecondaryTransientClientModeManagers(() ->
                        mConnectHelper.connectToNetwork(result, wrapper, uid));
    }
    
    //ConnectHelper.java
    public void connectToNetwork(
                @NonNull ClientModeManager clientModeManager,
                @NonNull NetworkUpdateResult result,
                @NonNull ActionListenerWrapper wrapper,
                int callingUid) {
    	clientModeManager.connectNetwork(result, wrapper, callingUid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    连接动作由"指派"的 clientModeManager 进行

    关注下
    mMakeBeforeBreakManager.stopAllSecondaryTransientClientModeManagers(() -> mConnectHelper.connectToNetwork(result, wrapper, uid));

    ActiveModeWarden维护的只有一个 PrimaryClientModeManager 但可能会有很多 SecondaryClientModeManager
    SecondaryClientModeManager 有机会转变成 PrimaryClientModeManager (ClientRole的转变)

    这里摆一下下ClientRole的继承关系
    在这里插入图片描述

    SecondaryTransient 角色的ClientModeManager将很快转换自己的角色,为了避免混乱,MBB要求所有的 transient 都先停下来,全部停下来只有再进行连接动作。源码这里调用的是不带 ClienModeManager 的写法,即让默认的 PrimaryClientModeManager 去触发连接。实际这里需要做些修改。(09-14回顾,这里这样写是没有问题的,这种WifiManager定死ConnectHelper用PrimaryClientModeManager的写法意味着,双STA默认不允许用户在wifi主界面去自主选择第二个网络来连接,第二个网络的选择和连接应该交由framework来完成,用户能做的只是决定 这个特性 的开关)

    这里我需要先看下ClientModeManager的创建情况。

    //ActiveModeWarden.java
    //wifi开关是打开的,则Primary,否则看 Location + WiFi Scanning -> ScanOnly
        private ActiveModeManager.ClientRole getRoleForPrimaryOrScanOnlyClientModeManager() {
            if (mSettingsStore.isWifiToggleEnabled()) {
                return ROLE_CLIENT_PRIMARY;
            } else if (mWifiController.shouldEnableScanOnlyMode()) {
                return ROLE_CLIENT_SCAN_ONLY;
            } else {
                Log.e(TAG, "Something is wrong, no client mode toggles enabled");
                return null;
            }
        }
    
    class WifiController {
            @Override
            public void start() {
                ActiveModeManager.ClientRole role = getRoleForPrimaryOrScanOnlyClientModeManager();
                if (role == ROLE_CLIENT_PRIMARY) {
                    startPrimaryClientModeManager(mLastPrimaryClientModeManagerRequestorWs);
                    setInitialState(mEnabledState);
                } else if (role == ROLE_CLIENT_SCAN_ONLY) {
                    startScanOnlyClientModeManager(mLastScanOnlyClientModeManagerRequestorWs);
                    setInitialState(mEnabledState);
                } else {
                    setInitialState(mDisabledState);
                }
                
                // Initialize the lower layers before we start.
                mWifiNative.initialize();
                super.start();
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    初始设计为:
    只有wifi打开时会创建PrimaryClientModeManager
    那什么时候会创建第二个,且不同ClientRole 的 ClientModeManager 呢?

    关注ActiveModeWarden中

    private final Set<ConcreteClientModeManager> mClientModeManagers = new ArraySet<>();
    
    • 1

    这个Set的Add和Remove动作,溯源即得到以下流程逻辑

    1. WifiConnectivityManager.handleScanResults
    private void handleScanResults(@NonNull List<ScanDetail> scanDetails,
    		@NonNull String listenerName,
    		boolean isFullScan,
    		@NonNull HandleScanResultsListener handleScanResultsListener) {
    
    	boolean hasExistingSecondaryCmm = false;
    	for (ClientModeManager clientModeManager :
    			mActiveModeWarden.getInternetConnectivityClientModeManagers()) {
    		if (clientModeManager.getRole() == ROLE_CLIENT_SECONDARY_LONG_LIVED) {
    			hasExistingSecondaryCmm = true;
    		}
    	}
    
    	// We don't have any existing secondary CMM, but are we allowed to create a secondary CMM
    	// and do we have a request for OEM_PAID/OEM_PRIVATE request? If yes, we need to perform
    	// network selection to check if we have any potential candidate for the secondary CMM
    	// creation.
    	if (!hasExistingSecondaryCmm
    			&& (mOemPaidConnectionAllowed || mOemPrivateConnectionAllowed)) {
    		// prefer OEM PAID requestor if it exists.
    		WorkSource oemPaidOrOemPrivateRequestorWs =
    				mOemPaidConnectionRequestorWs != null
    						? mOemPaidConnectionRequestorWs
    						: mOemPrivateConnectionRequestorWs;
    		if (oemPaidOrOemPrivateRequestorWs != null
    				&& mActiveModeWarden.canRequestMoreClientModeManagersInRole(
    						oemPaidOrOemPrivateRequestorWs,
    						ROLE_CLIENT_SECONDARY_LONG_LIVED)) {
    			// Add a placeholder CMM state to ensure network selection is performed for a
    			// potential second STA creation.
    			cmmStates.add(new WifiNetworkSelector.ClientModeManagerState());
    		}
    	}
    
    	// Check if any blocklisted BSSIDs can be freed.
    	//...
    
    	List<WifiCandidates.Candidate> candidates = mNetworkSelector.getCandidatesFromScan(
    			scanDetails, bssidBlocklist, cmmStates, mUntrustedConnectionAllowed,
    			mOemPaidConnectionAllowed, mOemPrivateConnectionAllowed);
    
    	// We have an oem paid/private network request and device supports STA + STA, check if there
    	// are oem paid/private suggestions.
    	if ((mOemPaidConnectionAllowed || mOemPrivateConnectionAllowed)
    			&& mActiveModeWarden.isStaStaConcurrencySupportedForRestrictedConnections()) {
    		// Split the candidates based on whether they are oem paid/oem private or not.
    		Map<Boolean, List<WifiCandidates.Candidate>> candidatesPartitioned =
    				candidates.stream()
    						.collect(Collectors.groupingBy(c -> c.isOemPaid() || c.isOemPrivate()));
    		List<WifiCandidates.Candidate> primaryCmmCandidates =
    				candidatesPartitioned.getOrDefault(false, Collections.emptyList());
    		List<WifiCandidates.Candidate> secondaryCmmCandidates =
    				candidatesPartitioned.getOrDefault(true, Collections.emptyList());
    		// Some oem paid/private suggestions found, use secondary cmm flow.
    		if (!secondaryCmmCandidates.isEmpty()) {
    			handleCandidatesFromScanResultsUsingSecondaryCmmIfAvailable(
    					listenerName, primaryCmmCandidates, secondaryCmmCandidates,
    					handleScanResultsListener);
    			return;
    		}
    		// intentional fallthrough: No oem paid/private suggestions, fallback to legacy flow.
    	}
    	handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable(
    			listenerName, candidates, handleScanResultsListener);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    总结:在对scan results的处理中,如果扫描到的网络中有 OEM_PAID 或 OEM_PRIVATE 类型的网络,则按 非oem_paid/oem_private 和 oem_paid/oem_private 将候选网络分成两类,前者塞到 primaryCmmCandidates 中去,后者塞到 secondaryCmmCandidates 中去
    之后调用 handleCandidatesFromScanResultsUsingSecondaryCmmIfAvailable 方法对 oem_paid/oem_private网络 进行处理,看有无必要创建一个 CMM

    1. 对 secondaryCmmCandidates 进行WNS选网操作,如果有合适的网络,就 requestSecondaryLongLivedClientModeManager ,即创建一个ClientRole == SECONDARY_LONG_LIVED 的CMM,并 connectToNetworkUsingCmmWithoutMbb 触发连接
    private void handleCandidatesFromScanResultsUsingSecondaryCmmIfAvailable(
    		@NonNull String listenerName,
    		@NonNull List<WifiCandidates.Candidate> primaryCmmCandidates,
    		@NonNull List<WifiCandidates.Candidate> secondaryCmmCandidates,
    		@NonNull HandleScanResultsListener handleScanResultsListener) {
    	// Perform network selection among secondary candidates.
    	WifiConfiguration secondaryCmmCandidate =
    			mNetworkSelector.selectNetwork(secondaryCmmCandidates);
    	// No oem paid/private selected, fallback to legacy flow (should never happen!).
    	if (secondaryCmmCandidate == null
    			|| secondaryCmmCandidate.getNetworkSelectionStatus().getCandidate() == null
    			|| (!secondaryCmmCandidate.oemPaid && !secondaryCmmCandidate.oemPrivate)) {
    		handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable(
    				listenerName,
    				Stream.concat(primaryCmmCandidates.stream(), secondaryCmmCandidates.stream())
    						.collect(Collectors.toList()),
    				handleScanResultsListener);
    		return;
    	}
    	String secondaryCmmCandidateBssid =
    			secondaryCmmCandidate.getNetworkSelectionStatus().getCandidate().BSSID;
    
    	// At this point secondaryCmmCandidate must be either oemPaid, oemPrivate, or both.
    	// OEM_PAID takes precedence over OEM_PRIVATE, so attribute to OEM_PAID requesting app.
    	WorkSource secondaryRequestorWs = secondaryCmmCandidate.oemPaid
    			? mOemPaidConnectionRequestorWs : mOemPrivateConnectionRequestorWs;
    
    	WifiConfiguration primaryCmmCandidate =
    			mNetworkSelector.selectNetwork(primaryCmmCandidates);
    	// Request for a new client mode manager to spin up concurrent connection
    	mActiveModeWarden.requestSecondaryLongLivedClientModeManager(
    			(cm) -> {
    				// Don't use make before break for these connection requests.
    
    				// If we also selected a primary candidate trigger connection.
    				if (primaryCmmCandidate != null) {
    					localLog(listenerName + ":  WNS candidate(primary)-"
    							+ primaryCmmCandidate.SSID);
    					connectToNetworkUsingCmmWithoutMbb(
    							getPrimaryClientModeManager(), primaryCmmCandidate);
    				}
    
    				localLog(listenerName + ":  WNS candidate(secondary)-"
    						+ secondaryCmmCandidate.SSID);
    				// Secndary candidate cannot be null (otherwise we would have switched to legacy flow above)
    				connectToNetworkUsingCmmWithoutMbb(cm, secondaryCmmCandidate);
    
    				handleScanResultsWithCandidate(handleScanResultsListener);
    			}, secondaryRequestorWs,
    			secondaryCmmCandidate.SSID,
    			mConnectivityHelper.isFirmwareRoamingSupported()
    					? null : secondaryCmmCandidateBssid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    1. 如果候选网络中没有上述的 oem_paid/oem_private 类型的网络或者经过选网没有合适的网络可以加入,那么就走legacy flow,即 handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable
    private void handleCandidatesFromScanResultsForPrimaryCmmUsingMbbIfAvailable(
    		@NonNull String listenerName, @NonNull List<WifiCandidates.Candidate> candidates,
    		@NonNull HandleScanResultsListener handleScanResultsListener) {
    	WifiConfiguration candidate = mNetworkSelector.selectNetwork(candidates);
    	if (candidate != null) {
    		localLog(listenerName + ":  WNS candidate-" + candidate.SSID);
    		connectToNetworkForPrimaryCmmUsingMbbIfAvailable(candidate);
    		handleScanResultsWithCandidate(handleScanResultsListener);
    	} else {
    		localLog(listenerName + ":  No candidate");
    		handleScanResultsWithNoCandidate(handleScanResultsListener);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    重点在于 connectToNetworkForPrimaryCmmUsingMbbIfAvailable

    /**
     * Trigger network connection for primary client mode manager using make before break.
     *
     * Note: This may trigger make before break on a secondary STA if available which will
     * eventually become primary after validation or torn down if it does not become primary.
     */
     private void connectToNetworkForPrimaryCmmUsingMbbIfAvailable(
    		@NonNull WifiConfiguration candidate) {
    	ClientModeManager primaryManager = mActiveModeWarden.getPrimaryClientModeManager();
    	connectToNetworkUsingCmm(
    			primaryManager, candidate,
    			new ConnectHandler() {
    				@Override
    				public void triggerConnectWhenDisconnected(
    						WifiConfiguration targetNetwork,
    						String targetBssid) {
    					triggerConnectToNetworkUsingCmm(primaryManager, targetNetwork, targetBssid);
    					// since using primary manager to connect, stop any existing managers in the
    					// secondary transient role since they are no longer needed.
    					mActiveModeWarden.stopAllClientModeManagersInRole(
    							ROLE_CLIENT_SECONDARY_TRANSIENT);
    				}
    
    				@Override
    				public void triggerConnectWhenConnected(
    						WifiConfiguration currentNetwork,
    						WifiConfiguration targetNetwork,
    						String targetBssid) {
    					// If both the current & target networks have MAC randomization disabled,
    					// we cannot use MBB because then both ifaces would need to use the exact
    					// same MAC address (the "designated" factory MAC for the device), which is
    					// illegal. Fallback to single STA behavior.
    					if (currentNetwork.macRandomizationSetting == RANDOMIZATION_NONE
    							&& targetNetwork.macRandomizationSetting == RANDOMIZATION_NONE) {
    						triggerConnectToNetworkUsingCmm(
    								primaryManager, targetNetwork, targetBssid);
    						// since using primary manager to connect, stop any existing managers in
    						// the secondary transient role since they are no longer needed.
    						mActiveModeWarden.stopAllClientModeManagersInRole(
    								ROLE_CLIENT_SECONDARY_TRANSIENT);
    						return;
    					}
    					// Else, use MBB if available.
    					triggerConnectToNetworkUsingMbbIfAvailable(targetNetwork, targetBssid);
    				}
    
    				@Override
    				public void triggerRoamWhenConnected(
    						WifiConfiguration currentNetwork,
    						WifiConfiguration targetNetwork,
    						String targetBssid) {
    					triggerRoamToNetworkUsingCmm(
    							primaryManager, targetNetwork, targetBssid);
    					// since using primary manager to connect, stop any existing managers in the
    					// secondary transient role since they are no longer needed.
    					mActiveModeWarden.stopAllClientModeManagersInRole(
    							ROLE_CLIENT_SECONDARY_TRANSIENT);
    				}
    			});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    对于双STA,我们关注 triggerConnectWhenConnected 情景下 ,当前网络和目标网络都支持 MAC randomization ,即调用triggerConnectToNetworkUsingMbbIfAvailable 方法
    此方法核心在于创建一个 ClientRole == SECONDARY_TRANSIENT 的CMM,并用得到的CMM去triggerConnectToNetworkUsingCmm

    /**
     * Trigger connection to a new wifi network while being connected to another network.
     * Depending on device configuration, this uses
     *  - MBB make before break (Dual STA), or
     *  - BBM break before make (Single STA)
     */
    private void triggerConnectToNetworkUsingMbbIfAvailable(
    		@NonNull WifiConfiguration targetNetwork, @NonNull String targetBssid) {
    	// Request a ClientModeManager from ActiveModeWarden to connect with - may be an existing
    	// CMM or a newly created one (potentially switching networks using Make-Before-Break)
    	mActiveModeWarden.requestSecondaryTransientClientModeManager(
    			(@Nullable ClientModeManager clientModeManager) -> {
    				localLog("connectToNetwork: received requested ClientModeManager "
    						+ clientModeManager);
    				// we don't know which ClientModeManager will be allocated to us. Thus, double
    				// check if we're already connected before connecting.
    				if (isClientModeManagerConnectedOrConnectingToCandidate(
    						clientModeManager, targetNetwork)) {
    					localLog("connectToNetwork: already connected or connecting to candidate="
    							+ targetNetwork + " on " + clientModeManager);
    					return;
    				}
    				triggerConnectToNetworkUsingCmm(clientModeManager, targetNetwork, targetBssid);
    			},
    			ActiveModeWarden.INTERNAL_REQUESTOR_WS,
    			targetNetwork.SSID,
    			mConnectivityHelper.isFirmwareRoamingSupported() ? null : targetBssid);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    09-29 补充:完善MBB机制

    上述基本没有涉及MBB机制
    这里直接放下我实测的MBB流程

    1. 创建一个 ClientRole == SECONDARY_TRANSIENT 的CMM,并用得到的CMM去triggerConnectToNetworkUsingCmm
      这时观察wpa_supplicant的log可以发现有创建wlan1接口以及connect动作,直到 CTRL-EVENT-CONNECTED 事件上报

    2. SecondaryTransientCMM 注册 WifiNetworkAgent 到 CnnectivityService,probe http/https,通过后
      MakeBeforeBreakManager中ClientModeImplMonitor注册的监听器回调 onInternetValidated
      具体操作有: 将原PrimaryCMM的角色设置成 ROLE_CLIENT_SECONDARY_TRANSIENT

    3. 有CMM角色发生改变,即触发MakeBeforeBreakManager 注册的回调onActiveModeManagerRoleChanged
      执行 recoveryPrimary以及maybeContinueMakeBeforeBreak方法
      具体操作有: 将 SecondaryTransientCMM 的角色设置成 PRIMARY, 并将原PrimaryCMM(新的SecondaryTransientCMM )减掉一些网络评分

    4. 由于新的SecondaryTransientCMM在ConnectivityService中的分低,触发linger,timer定时(30s)到了之后会被teardown
      注:这方面源码不大确定,读者可自行查阅ConnectivityService handleLingerComplete(teardownUnneededNetwork)方法

    显然,对于双STA来说,希望在MBB的基础上保持被从主wifi位置上拉下来的辅wifi的连接,所以方法目前我只想到两种

    1. 注释减分动作,实测可行
    2. 无限期延长linger时长(linger时长默认先从系统属性中读取,没有就取30s,试过设置这个系统属性,无效,也许是我手法问题)

    到这里,双STA在fwk的设计基本就介绍完了,简单来说,要在MBB的基础上做一些魔改

  • 相关阅读:
    (2023|ICML,LLM,标记掩蔽,并行解码)Muse:使用掩蔽生成 Transformer 的文本到图像生成
    Linux操作系统——系统用户与用户组管理
    PTA作业10单链表6-1 链表拼接
    IO流核心模块与基本原理
    React-1 基础知识
    【Java|golang】1413. 逐步求和得到正数的最小值
    淘宝商品详情接口,商品属性接口,商品信息查询,商品详细信息接口,h5详情,淘宝APP详情
    【优化布局】基于遗传算法实现风电场集电系统优化附matlab代码
    清空回收站的照片还能找回来吗?照片恢复用这招
    ubuntu22.01安装及配置
  • 原文地址:https://blog.csdn.net/AngryDog1024/article/details/126780101