Android -- Wifi启动流程分析

Android网络各个模式中,Wifi应该是目前最常用的一种网络方式了;下面就简单介绍下Android中Wifi的启动流程。
当我在Setting菜单里点击打开Wifi时,调用的入口函数是WifiManager::setWifiEnabled(boolean enabled):

    /*** Enable or disable Wi-Fi.* @param enabled {@code true} to enable, {@code false} to disable.* @return {@code true} if the operation succeeds (or if the existing state*         is the same as the requested state).*/public boolean setWifiEnabled(boolean enabled) {try {return mService.setWifiEnabled(enabled);} catch (RemoteException e) {return false;}}

通过AIDL方式,在Android6.0中,实际调用的是WifiServiceImpl::setWifiEnabled(boolean enable):

    /*** see {@link android.net.wifi.WifiManager#setWifiEnabled(boolean)}* @param enable {@code true} to enable, {@code false} to disable.* @return {@code true} if the enable/disable operation was*         started or is already in the queue.*/public synchronized boolean setWifiEnabled(boolean enable) {enforceChangePermission();Slog.d(TAG, "setWifiEnabled: " + enable + " pid=" + Binder.getCallingPid()+ ", uid=" + Binder.getCallingUid());if (DBG) {Slog.e(TAG, "Invoking mWifiStateMachine.setWifiEnabled\n");}/** Caller might not have WRITE_SECURE_SETTINGS,* only CHANGE_WIFI_STATE is enforced*/long ident = Binder.clearCallingIdentity();try {if (! mSettingsStore.handleWifiToggled(enable)) {// Nothing to do if wifi cannot be toggledreturn true;}} finally {Binder.restoreCallingIdentity(ident);}mWifiController.sendMessage(CMD_WIFI_TOGGLED);return true;}

从代码可以看出,这里主要的操作是将wifi是否enable的状态存入数据库、向WiFiController发送了CMD_WIFI_TOGGLED消息。

WifiController实际上是一个状态机,相比WifiStateMachine,它的状态较少,结构也比较简单。WifiController的定义及构造函数:

class WifiController extends StateMachine {...WifiController(Context context, WifiServiceImpl service, Looper looper) {super(TAG, looper);...  addState(mDefaultState);addState(mApStaDisabledState, mDefaultState);addState(mStaEnabledState, mDefaultState);addState(mDeviceActiveState, mStaEnabledState);addState(mDeviceInactiveState, mStaEnabledState);addState(mScanOnlyLockHeldState, mDeviceInactiveState);addState(mFullLockHeldState, mDeviceInactiveState);addState(mFullHighPerfLockHeldState, mDeviceInactiveState);addState(mNoLockHeldState, mDeviceInactiveState);addState(mStaDisabledWithScanState, mDefaultState);addState(mApEnabledState, mDefaultState);addState(mEcmState, mDefaultState);boolean isAirplaneModeOn = mSettingsStore.isAirplaneModeOn();boolean isWifiEnabled = mSettingsStore.isWifiToggleEnabled();boolean isScanningAlwaysAvailable = mSettingsStore.isScanAlwaysAvailable();log("isAirplaneModeOn = " + isAirplaneModeOn +", isWifiEnabled = " + isWifiEnabled +", isScanningAvailable = " + isScanningAlwaysAvailable);if (isScanningAlwaysAvailable) {setInitialState(mStaDisabledWithScanState);} else {setInitialState(mApStaDisabledState);}...}...
}
WifiController状态机的创建、开启工作在WifiServiceImpl中完成:

    public WifiServiceImpl(Context context) {mContext = context;mInterfaceName =  SystemProperties.get("wifi.interface", "wlan0");mTrafficPoller = new WifiTrafficPoller(mContext, mInterfaceName);mWifiStateMachine = new WifiStateMachine(mContext, mInterfaceName, mTrafficPoller);mWifiStateMachine.enableRssiPolling(true);mBatteryStats = BatteryStatsService.getService();mPowerManager = context.getSystemService(PowerManager.class);mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);mUserManager = UserManager.get(mContext);mNotificationController = new WifiNotificationController(mContext, mWifiStateMachine);mSettingsStore = new WifiSettingsStore(mContext);HandlerThread wifiThread = new HandlerThread("WifiService");wifiThread.start();mClientHandler = new ClientHandler(wifiThread.getLooper());mWifiStateMachineHandler = new WifiStateMachineHandler(wifiThread.getLooper());mWifiController = new WifiController(mContext, this, wifiThread.getLooper());}

WifiController中的各状态之间的关系如图:

WifiControlle状态机的初始状态由一些配置信息决定。当ApStaDisabledState为初始状态时,看对CMD_WIFI_TOGGLED消息的处理:
    class ApStaDisabledState extends State {...@Overridepublic boolean processMessage(Message msg) {switch (msg.what) {case CMD_WIFI_TOGGLED:case CMD_AIRPLANE_TOGGLED:if (mSettingsStore.isWifiToggleEnabled()) {if (doDeferEnable(msg)) {if (mHaveDeferredEnable) {//  have 2 toggles now, inc serial number an ignore bothmDeferredEnableSerialNumber++;}mHaveDeferredEnable = !mHaveDeferredEnable;break;}if (mDeviceIdle == false) {transitionTo(mDeviceActiveState);} else {checkLocksAndTransitionWhenDeviceIdle();}} else if (mSettingsStore.isScanAlwaysAvailable()) {transitionTo(mStaDisabledWithScanState);}break;case CMD_SCAN_ALWAYS_MODE_CHANGED:if (mSettingsStore.isScanAlwaysAvailable()) {transitionTo(mStaDisabledWithScanState);}break;...default:return NOT_HANDLED;}return HANDLED;}private boolean doDeferEnable(Message msg) {long delaySoFar = SystemClock.elapsedRealtime() - mDisabledTimestamp;if (delaySoFar >= mReEnableDelayMillis) {return false;}log("WifiController msg " + msg + " deferred for " +(mReEnableDelayMillis - delaySoFar) + "ms");// need to defer this action.Message deferredMsg = obtainMessage(CMD_DEFERRED_TOGGLE);deferredMsg.obj = Message.obtain(msg);deferredMsg.arg1 = ++mDeferredEnableSerialNumber;sendMessageDelayed(deferredMsg, mReEnableDelayMillis - delaySoFar + DEFER_MARGIN_MS);return true;}}

调试过程中发现,ApStaDisabledState会同时忽略两个时间间隔小于500ms的 CMD_WIFI_TOGGLED消息,接着转换到DeviceActiveState状态。StaEnabledState是它的父状态,由StateMachine的知识可知,转换到该状态时,会依次调用父、子状态的enter()函数。我们看两个状态的enter()函数:

    class StaEnabledState extends State {@Overridepublic void enter() {mWifiStateMachine.setSupplicantRunning(true);}...}}/* Parent: StaEnabledState */class DeviceActiveState extends State {@Overridepublic void enter() {mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);mWifiStateMachine.setDriverStart(true);mWifiStateMachine.setHighPerfModeEnabled(false);//此处分析忽略,关系不大}...}

这里依次会向WifiStateMachine发送三个消息,最后一个消息这里忽略:

  1. WifiStateMachine.setSupplicantRunning(true):发送CMD_START_SUPPLICANT消息
  2. WifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE):发送CMD_SET_OPERATIONAL_MODE消息,参数是CONNECT_MODE
  3. WifiStateMachine.setDriverStart(true):发送CMD_START_DRIVER消息
在此之前,我们先看下第二点中的参数CONNECT_MODE的含义。在WifiStateMachine中,已经有了如下定义:

    /* Wifi state machine modes of operation *//* CONNECT_MODE - connect to any 'known' AP when it becomes available */public static final int CONNECT_MODE = 1;/* SCAN_ONLY_MODE - don't connect to any APs; scan, but only while apps hold lock */public static final int SCAN_ONLY_MODE = 2;/* SCAN_ONLY_WITH_WIFI_OFF - scan, but don't connect to any APs */public static final int SCAN_ONLY_WITH_WIFI_OFF_MODE = 3;
    /* 3 operational states for STA operation: CONNECT_MODE, SCAN_ONLY_MODE, SCAN_ONLY_WIFI_OFF_MODE* In CONNECT_MODE, the STA can scan and connect to an access point* In SCAN_ONLY_MODE, the STA can only scan for access points* In SCAN_ONLY_WIFI_OFF_MODE, the STA can only scan for access points with wifi toggle being off*/private int mOperationalMode = CONNECT_MODE;

可知Wifi状态机一共有三种处理模式:

  1. CONNECT_MODE:该状态下Wifi可以扫描AP,也可以连接AP
  2. SCAN_ONLY_MODE:该状态下Wifi尽可以扫描AP
  3. SCAN_ONLY_WIFI_OFF_MODE:该状态下,Wifi仅可以当Wifi toogle off时允许扫描AP
现在,我们将处理的过程转换到WifiStateMachine。WifiStateMachine是一个复杂的状态机,它维护了Wifi的启动、扫描、连接、断开等多个状态。它运行在自己独有的线程中,拥有自己的消息队列。WifiStateMachine中各状态的关系如图所示:
分别来看WifiStateMachine是怎么处理这三个消息的。InitialState首先接收到CMD_START_SUPPLICANT消息并处理:

           case CMD_START_SUPPLICANT:if (mWifiNative.loadDriver()) {//加载驱动try {mNwService.wifiFirmwareReload(mInterfaceName, "STA");//加载wlan固件} catch (Exception e) {loge("Failed to reload STA firmware " + e);// Continue}try {// A runtime crash can leave the interface up and// IP addresses configured, and this affects// connectivity when supplicant starts up.// Ensure interface is down and we have no IP// addresses before a supplicant start.mNwService.setInterfaceDown(mInterfaceName);mNwService.clearInterfaceAddresses(mInterfaceName);// Set privacy extensionsmNwService.setInterfaceIpv6PrivacyExtensions(mInterfaceName, true);// IPv6 is enabled only as long as access point is connected since:// - IPv6 addresses and routes stick around after disconnection// - kernel is unaware when connected and fails to start IPv6 negotiation// - kernel can start autoconfiguration when 802.1x is not completemNwService.disableIpv6(mInterfaceName);} catch (RemoteException re) {loge("Unable to change interface settings: " + re);} catch (IllegalStateException ie) {loge("Unable to change interface settings: " + ie);}/* Stop a running supplicant after a runtime restart* Avoids issues with drivers that do not handle interface down* on a running supplicant properly.*/mWifiMonitor.killSupplicant(mP2pSupported);if (WifiNative.startHal() == false) {/* starting HAL is optional */loge("Failed to start HAL");}if (mWifiNative.startSupplicant(mP2pSupported)) {//启动wpa_ssetWifiState(WIFI_STATE_ENABLING);if (DBG) log("Supplicant start successful");mWifiMonitor.startMonitoring();//建立与wpa_s之间的socket通信连接;开启线程,循环接收来自wpa_s的event,并分发处理transitionTo(mSupplicantStartingState);} else {loge("Failed to start supplicant!");}} else {loge("Failed to load driver");}break;

主要的处理过程包括:

  1. WifiNative.loadDriver():加载Wifi驱动,实际的实现是在wifi.c中
  2. NetworkManagementService.wifiFirmwareReload(mInterfaceName, "STA"):加载wlan固件
  3. WifiNative.startSupplicant(mP2pSupported):启动wpa_supplicant
  4. WifiMonitor.startMonitoring():在前面一篇博文中已经介绍过WifiMonitor,这一步主要是在WifiMonitor中建立与wpa_supplicant通信的socket通道、创建一个线程接收底层事件并分发处理。这里会创建两个socket通道与wpa_s通信,一个用于下发指令,另一个用于接收事件。成功后WifiMonitor会向WifiStateMachine发送一个代表socket通信建立成功的消息:SUP_CONNECTION_EVENT;收到这个消息就表示Wifi已经启动成功了。
  5. 切换到SupplicantStartingState。
进入SupplicantStartingState后,第一个消息就处理完毕,这时消息队列中按处理先后顺序仍有三个消息:
  1. CMD_SET_OPERATIONAL_MODE,参数是CONNECT_MODE
  2. CMD_START_DRIVER
  3. SUP_CONNECTION_EVENT
切换到SupplicantStartingState,消息队列中的前两个消息在该状态会被延迟处理,直接看SUP_CONNECTION_EVENT的处理过程:

case WifiMonitor.SUP_CONNECTION_EVENT:if (DBG) log("Supplicant connection established");setWifiState(WIFI_STATE_ENABLED);mSupplicantRestartCount = 0;/* Reset the supplicant state to indicate the supplicant* state is not known at this time */mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);/* Initialize data structures */mLastBssid = null;mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;mLastSignalLevel = -1;mWifiInfo.setMacAddress(mWifiNative.getMacAddress());/* set frequency band of operation */setFrequencyBand();mWifiNative.enableSaveConfig();mWifiConfigStore.loadAndEnableAllNetworks();//加载并enable保存的APif (mWifiConfigStore.enableVerboseLogging.get() > 0) {enableVerboseLogging(mWifiConfigStore.enableVerboseLogging.get());}initializeWpsDetails();sendSupplicantConnectionChangedBroadcast(true);//广播通知Wpa_s连接已建立,此时已经可以准备连接或扫描Wifi了transitionTo(mDriverStartedState);break;

这里主要是调用WifiConfigStore.loadAndEnableAllNetworks()加载并enable所有保存在wpa_s中的AP,然后做一些其他的初始化工作,切换到DriverStartedState状态。关注其父状态和自身的enter()函数:

 class SupplicantStartedState extends State {@Overridepublic void enter() {/* Wifi is available as long as we have a connection to supplicant */mNetworkInfo.setIsAvailable(true);if (mNetworkAgent != null) mNetworkAgent.sendNetworkInfo(mNetworkInfo);int defaultInterval = mContext.getResources().getInteger(R.integer.config_wifi_supplicant_scan_interval);mSupplicantScanIntervalMs = Settings.Global.getLong(mContext.getContentResolver(),Settings.Global.WIFI_SUPPLICANT_SCAN_INTERVAL_MS,defaultInterval);mWifiNative.setScanInterval((int)mSupplicantScanIntervalMs / 1000);//设置扫描时间间隔mWifiNative.setExternalSim(true);/* turn on use of DFS channels */WifiNative.setDfsFlag(true);/* set country code */setCountryCode();setRandomMacOui();mWifiNative.enableAutoConnect(false); //可以事先注意该设置}
}class DriverStartedState extends State {@Overridepublic void enter() {if (PDBG) {logd("DriverStartedState enter");}mWifiLogger.startLogging(mVerboseLoggingLevel > 0);mIsRunning = true;mInDelayedStop = false;mDelayedStopCounter++;updateBatteryWorkSource(null);/*** Enable bluetooth coexistence scan mode when bluetooth connection is active.* When this mode is on, some of the low-level scan parameters used by the* driver are changed to reduce interference with bluetooth*/mWifiNative.setBluetoothCoexistenceScanMode(mBluetoothConnectionActive);/* initialize network state */setNetworkDetailedState(DetailedState.DISCONNECTED);/* Remove any filtering on Multicast v6 at start */mWifiNative.stopFilteringMulticastV6Packets();/* Reset Multicast v4 filtering state */if (mFilteringMulticastV4Packets.get()) {mWifiNative.startFilteringMulticastV4Packets();} else {mWifiNative.stopFilteringMulticastV4Packets();}mDhcpActive = false;if (mOperationalMode != CONNECT_MODE) {mWifiNative.disconnect();mWifiConfigStore.disableAllNetworks();if (mOperationalMode == SCAN_ONLY_WITH_WIFI_OFF_MODE) {setWifiState(WIFI_STATE_DISABLED);}transitionTo(mScanModeState);} else {// Status pulls in the current supplicant state and network connection state// events over the monitor connection. This helps framework sync up with// current supplicant state// TODO: actually check th supplicant status string and make sure the supplicant// is in disconnecte4d state.mWifiNative.status();// Transitioning to Disconnected state will trigger a scan and subsequently AutoJointransitionTo(mDisconnectedState);transitionTo(mDisconnectedState);}// We may have missed screen update at bootif (mScreenBroadcastReceived.get() == false) {PowerManager powerManager = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE);handleScreenStateChanged(powerManager.isScreenOn());} else {// Set the right suspend mode settingsmWifiNative.setSuspendOptimizations(mSuspendOptNeedsDisabled == 0&& mUserWantsSuspendOpt.get());}mWifiNative.setPowerSave(true);if (mP2pSupported) {if (mOperationalMode == CONNECT_MODE) {mWifiP2pChannel.sendMessage(WifiStateMachine.CMD_ENABLE_P2P);//支持p2p,则会发送命令enbale p2p} else {// P2P statemachine starts in disabled state, and is not enabled until// CMD_ENABLE_P2P is sent from here; so, nothing needs to be done to// keep it disabled.}}final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WIFI_STATE_ENABLED);mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);mHalFeatureSet = WifiNative.getSupportedFeatureSet();if ((mHalFeatureSet & WifiManager.WIFI_FEATURE_HAL_EPNO)== WifiManager.WIFI_FEATURE_HAL_EPNO) {mHalBasedPnoDriverSupported = true;}// Enable link layer stats gatheringmWifiNative.setWifiLinkLayerStats("wlan0", 1);if (PDBG) {logd("Driverstarted State enter done, epno=" + mHalBasedPnoDriverSupported+ " feature=" + mHalFeatureSet);}}
}

SupplicantStartedState的enter()函数设置了Wifi扫描间隔;DriverStartedState的enter()函数主要进行了一些相关的设置工作,根据配置启动p2p。最后在enter()函数中会将状态切换到DisconnectedState。消息队列中被延迟的两条消息此时会被处理:

  1. CMD_SET_OPERATIONAL_MODE消息在DisconnectedState被处理,将mOperationalMode设置为CONNECT_MODE;Wifi状态机中该字段的默认值也是该值。
  2. CMD_START_DRIVER消息则在DriverStartedState中被处理。
最后转换到DisconnectedState状态,关注enter()函数:

public void enter() {// We dont scan frequently if this is a temporary disconnect// due to p2pif (mTemporarilyDisconnectWifi) {mWifiP2pChannel.sendMessage(WifiP2pServiceImpl.DISCONNECT_WIFI_RESPONSE);return;}if (PDBG) {logd(" Enter DisconnectedState scan interval "+ mWifiConfigStore.wifiDisconnectedShortScanIntervalMilli.get()+ " mLegacyPnoEnabled= " + mLegacyPnoEnabled+ " screenOn=" + mScreenOn+ " useGscan=" + mHalBasedPnoDriverSupported + "/"+ mWifiConfigStore.enableHalBasedPno.get());}/** clear the roaming state, if we were roaming, we failed */mAutoRoaming = WifiAutoJoinController.AUTO_JOIN_IDLE;if (useHalBasedAutoJoinOffload()) {startGScanDisconnectedModeOffload("disconnectedEnter");} else {if (mScreenOn) {/*** screen lit and => delayed timer*/startDelayedScan(500, null, null);} else {/*** screen dark and PNO supported => scan alarm disabled*/if (mBackgroundScanSupported) {/* If a regular scan result is pending, do not initiate background* scan until the scan results are returned. This is needed because* initiating a background scan will cancel the regular scan and* scan results will not be returned until background scanning is* cleared*/if (!mIsScanOngoing) {enableBackgroundScan(true);//启动wifi扫描,随后会触发autojoin,进行连接操作}} else {setScanAlarm(true);//启动一个扫描定时器}}}/*** If we have no networks saved, the supplicant stops doing the periodic scan.* The scans are useful to notify the user of the presence of an open network.* Note that these are not wake up scans.*/if (mNoNetworksPeriodicScan != 0 && !mP2pConnected.get()&& mWifiConfigStore.getConfiguredNetworks().size() == 0) {sendMessageDelayed(obtainMessage(CMD_NO_NETWORKS_PERIODIC_SCAN,++mPeriodicScanToken, 0), mNoNetworksPeriodicScan);}mDisconnectedTimeStamp = System.currentTimeMillis();mDisconnectedPnoAlarmCount = 0;}

在enter()函数中,会进行scan动作;WifiStateMachine处理SCAN_RESULTS_EVENT消息时,就会进入autojoin流程,尝试AP重连。

PS:流程图下载: http://download.csdn.net/detail/csdn_of_coder/9702484

Android -- Wifi启动流程分析相关推荐

  1. 【SemiDrive源码分析】【X9芯片启动流程】27 - AP1 Android Preloader启动流程分析(加载atf、tos、bootloader镜像后进入BL31环境)

    [SemiDrive源码分析][X9芯片启动流程]27 - AP1 Android Preloader启动流程分析(加载atf.tos.bootloader镜像后进入BL31环境) 一.Android ...

  2. Android service启动流程分析.

    文章仅仅用于个人的学习记录,基本上内容都是网上各个大神的杰作,此处摘录过来以自己的理解学习方式记录一下. 参考链接: https://my.oschina.net/youranhongcha/blog ...

  3. android app启动流程分析,Android应用开发之Android 7.0 Launcher3的启动和加载流程分析...

    本文将带你了解Android应用开发Android 7.0 Launcher3的启动和加载流程分析,希望本文对大家学Android有所帮助. Android 7.0 Launcher3的启动和加载流程 ...

  4. Android WiFi —softAP流程分析

    Android WiFi - Ap功能实现与源码分析 0. 前言 wifiAp的ip WifiAp的config分析 2.1 默认的config 2.2 修改wifiAp的config配置流程 开启/ ...

  5. Android | Activity 启动流程分析

    前言 Activity 类是 android 应用的关键组件,在日常开发中,绝对少不了组件.既然用了这么久,你知道他的启动流程

  6. 【Android 12.0】Android S WiFi启动业务流程分析(UML图)

    以下两张为Android S的WiFi启动(start)业务流程UML图,业务流程的过程详情因公司规定不可复制源码出来,所以想看业务流程详情的同学可以参考其他博主所发的博客.业务流程参考内容链接我会放 ...

  7. 【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一)

    [SemiDrive源码分析][X9芯片启动流程]30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一) 一.Android Kernel 启动流程分析 ...

  8. Android Q-wifi启动流程(一)

    Android Q WiFi 启动流程分析     1.WiFi 启动流程简介     2.WiFi 启动流程应用层分析     3.WiFi 启动流程服务层分析         3.1 WifiCo ...

  9. Android系统开机到Launcher启动流程分析

    本文基于Android10.0的源码. 由于google团队在对framework层代码进行大量重构,所以代码变动还是挺大的. 常见基础问题: SystemServer系统服务进程是如何创建的?Lau ...

最新文章

  1. 浮点数内存表示---记录一道题目
  2. android 刷机 备份,安卓刷机后如何还原以前ROM和系统备份
  3. linux下中文的wchar转char,Linux 下char转换为wchar_t
  4. Javascript模板引擎handlebars使用实例及技巧
  5. vf6.0 如何把命令窗口字体变大些_终端命令行工具iTerm2 for Mac免费版
  6. activemq 性能测试_ActiveMQ性能测试
  7. Illustrator、Indesign与Photoshop
  8. leetcode1028. 从先序遍历还原二叉树(dfs/栈)
  9. js 将点击事件当作参数引入_NodeJS中的事件驱动程序实现原理解析
  10. python template languages_更换Django默认的模板引擎为jinja2的实现方法
  11. MySQL Left Join,Right Join
  12. 学习攻略 | Python数据挖掘学习路线图
  13. 华为网络配置(ACL)
  14. C++内存分配(operator new)
  15. 第68页的gtk+编程例子——选择菜单
  16. 推荐一个 推理屋 网站
  17. python 悬浮窗_python浮窗
  18. java 数组总结(赋值,反转,添加,查找)
  19. 【python】求两条直线的交点
  20. 网盘搜索引擎(原创: 涔歌 涔歌 2018-03-25 特别说明,不同网盘搜索引擎搜录内容、搜索算法、提供的功能不同造成搜索结果差异较大,找不到时可以多尝试几个)...

热门文章

  1. 人工神经网络心得体会_人工神经网络
  2. ardino 不用舵机控制板直接控制六自由度机械臂
  3. oracle 锁表如何解决_「技术分享」高并发下的接口幂等性解决方案
  4. 图形与动画——什么是粒子系统
  5. api接口测试的工具
  6. 显示秒针(Wibdwos10)
  7. Couldn't find a suitable web browser! Set the BROWSER environment variable to your desired browser.
  8. HPB开发节点搭建指南
  9. 海康威视-测试-面经(一面+hr面)
  10. 护士排班问题matlab,护士排班问题的研究