从刚接触WiFi时跟过wifi的开启流程,当时还是android9。到了Android11代码架构有了不小的改动,在这里重新梳理一遍,便于在工作中更快速的跟踪代码。

一、Settings里改动不大,还是从WifiEnabler开始,调用WiFiManager的setWifiEnabled。
packages/apps/Settings/src/com/android/settings/wifi/WifiEnabler.java

if (!mWifiManager.setWifiEnabled(isChecked)) {// ErrormSwitchWidget.setEnabled(true);Toast.makeText(mContext, R.string.wifi_error, Toast.LENGTH_SHORT).show();}

二、这里要注意了,Android11默认加入了支持双WiFi的代码。这里打开WiFi就提供了俩个接口
frameworks/base/wifi/java/android/net/wifi/WifiManager.java
正常打开WiFi是调用这个单参的函数。

public boolean setWifiEnabled(boolean enabled) {try {return mService.setWifiEnabled(mContext.getOpPackageName(), enabled);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}
}

如果是指定打开哪个STA,就要调用双参的函数。

public boolean setWifiEnabled(int staId, boolean enabled) {try {return mService.setWifiEnabled2(mContext.getOpPackageName(), staId, enabled);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}
}

三、可以看到Wifimanager中正常打开WiFi和指定打开哪个STA的区别就是在WifiServiceImpl中setWifiEnabled2的参数不同。如果是打开第一个WiFi,则参数2为STA_PRIMARY,如果是打开其他WiFi,则参数2为传入的staId
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java

public synchronized boolean setWifiEnabled(String packageName, boolean enable) {return setWifiEnabled2(packageName, STA_PRIMARY, enable);
}
public synchronized boolean setWifiEnabled2(String packageName, int staId,boolean enable) {if (enforceChangePermission(packageName) != MODE_ALLOWED) {return false;}boolean isPrivileged = isPrivileged(Binder.getCallingPid(), Binder.getCallingUid());if (!isPrivileged && !isDeviceOrProfileOwner(Binder.getCallingUid(), packageName)&& !mWifiPermissionsUtil.isTargetSdkLessThan(packageName, Build.VERSION_CODES.Q,Binder.getCallingUid())&& !isSystem(packageName, Binder.getCallingUid())) {mLog.info("setWifiEnabled not allowed for uid=%").c(Binder.getCallingUid()).flush();return false;}// If Airplane mode is enabled, only privileged apps are allowed to toggle Wifiif (mSettingsStore.isAirplaneModeOn() && !isPrivileged) {mLog.err("setWifiEnabled in Airplane mode: only Settings can toggle wifi").flush();return false;}// If SoftAp is enabled, only privileged apps are allowed to toggle wifiif (!isPrivileged && mTetheredSoftApTracker.getState() == WIFI_AP_STATE_ENABLED) {mLog.err("setWifiEnabled with SoftAp enabled: only Settings can toggle wifi").flush();return false;}mLog.info("setWifiEnabled package=% uid=% enable=%").c(packageName).c(Binder.getCallingUid()).c(enable).flush();long ident = Binder.clearCallingIdentity();try {if (staId == STA_PRIMARY && !mSettingsStore.handleWifiToggled(enable)) {// Nothing to do if wifi cannot be toggledreturn true;}} finally {Binder.restoreCallingIdentity(ident);}if (mWifiPermissionsUtil.checkNetworkSettingsPermission(Binder.getCallingUid())) {mWifiMetrics.logUserActionEvent(enable ? UserActionEvent.EVENT_TOGGLE_WIFI_ON: UserActionEvent.EVENT_TOGGLE_WIFI_OFF);}if (!mIsControllerStarted) {Log.e(TAG,"WifiController is not yet started, abort setWifiEnabled");return false;}mWifiMetrics.incrementNumWifiToggles(isPrivileged, enable);
if(staId == STA_PRIMARY)mActiveModeWarden.wifiToggled();
else if(staId == STA_SECONDARY && (getNumConcurrentStaSupported() > 1) && (getWifiEnabledState() == WifiManager.WIFI_STATE_ENABLED))
mActiveModeWarden.qtiWifiToggled(staId, enable);elseLog.e(TAG,"setWifiEnabled not allowed for Id: " + staId);return true;
}

四、可以看到wifiservice调用了ActiveModeWarden的wifiToggled,发送了CMD_WIFI_TOGGLED的消息,通知WiFi切换了。
frameworks/opt/net/wifi/service/java/com/android/server/wifi/ActiveModeWarden.java

public void wifiToggled() {mWifiController.sendMessage(WifiController.CMD_WIFI_TOGGLED);
}

五、我们看WifiController是怎么处理这个消息的。WifiController是ActiveModeWarden中的一个状态机,用来管理WiFi的操作,包括热点啊飞行模式什么的。
打开WiFi之前,状态机应该是在Disabled状态,我们看Disable状态里的处理。

class DisabledState extends BaseState {public boolean processMessageFiltered(Message msg) {switch (msg.what) {case CMD_WIFI_TOGGLED:case CMD_SCAN_ALWAYS_MODE_CHANGED:if (shouldEnableSta()) {startClientModeManager();transitionTo(mEnabledState);}break;

启动一个新的客户端管理。

private boolean startClientModeManager() {Log.d(TAG, "Starting ClientModeManager");ClientListener listener = new ClientListener();ClientModeManager manager = mWifiInjector.makeClientModeManager(listener);listener.setActiveModeManager(manager);manager.start();if (!switchClientModeManagerRole(manager)) {return false;}mActiveModeManagers.add(manager);return true;
}

六、start了ClientModeManager
frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeManager.java

public void start() {Log.d(TAG, "Starting with role ROLE_CLIENT_SCAN_ONLY");mRole = ROLE_CLIENT_SCAN_ONLY;mTargetRole = ROLE_CLIENT_SCAN_ONLY;mStateMachine.sendMessage(ClientModeStateMachine.CMD_START);
}

看一下是谁处理了这个START消息呢

private class IdleState extends State {@Overridepublic boolean processMessage(Message message) {switch (message.what) {case CMD_START:// Always start in scan mode first.mClientInterfaceName =mWifiNative.setupInterfaceForClientInScanMode(mWifiNativeInterfaceCallback);if (TextUtils.isEmpty(mClientInterfaceName)) {Log.e(TAG, "Failed to create ClientInterface. Sit in Idle");mModeListener.onStartFailure();break;}transitionTo(mScanOnlyModeState);break;}
}

七、这里可以看出,WifiNative先去启动HAL
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiNative.java

public String setupInterfaceForClientInScanMode(@NonNull InterfaceCallback interfaceCallback) {synchronized (mLock) {if (!startHal()) {mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToHal();return null;}Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_STA_FOR_SCAN);iface.externalListener = interfaceCallback;iface.name = createStaIface(iface);if (!mWifiCondManager.setupInterfaceForClientMode(iface.name, Runnable::run,new NormalScanEventCallback(iface.name),new PnoScanEventCallback(iface.name))) {Log.e(TAG, "Failed to setup iface in wificond=" + iface.name);teardownInterface(iface.name);mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToWificond();return null;}iface.networkObserver = new NetworkObserverInternal(iface.id);if (!registerNetworkObserver(iface.networkObserver)) {teardownInterface(iface.name);return null;}mWifiMonitor.startMonitoring(iface.name);onInterfaceStateChanged(iface, isInterfaceUp(iface.name));iface.featureSet = getSupportedFeatureSetInternal(iface.name);return iface.name;}
}

八、启动HAL

WifiVendorHal.java-->startVendorHal --> HalDeviceManager.java --> startWifi --> IWifi.start

mWifi.start()方法是启动实际加载WiFi动作的调用,这里涉及HIDL机制调用。通过获取IWifi接口对象,调用其方法。这里IWifi接口对象是IWifi.hal文件中实现。

android/hardware/interfaces/wifi/1.0/IWifi.hal

在编译时,编译器会将IWifi.hal解析为IWifi.java文件,直接看该文件中的start方法实现即可。

android/out/soong//.intermediates/hardware/interfaces/wifi/1.0/android.hardware.wifi-V1.0-java_gen_java/gen/srcs/android/hardware/wifi/V1_0/IWifi.javapublic android.hardware.wifi.V1_0.WifiStatus start() throws android.os.RemoteException {try {... ... ... ...mRemote.transact(3 /* start */, _hidl_request, _hidl_reply, 0 /* flags */);_hidl_reply.verifySuccess();_hidl_request.releaseTemporaryStorage();return _hidl_out_status;} finally {_hidl_reply.release();}}

通过binder调用,将调用到wifi.cpp中的start()方法.

android/hardware/interfaces/wifi/1.4/default/wifi.cppReturn<void> Wifi::start(start_cb hidl_status_cb) {return validateAndCall(this, WifiStatusCode::ERROR_UNKNOWN,&Wifi::startInternal, hidl_status_cb);}wifi.cpp->start() ==> wifi.cpp->startInternal() ==> wifi.cpp->initializeModeControllerAndLegacyHal()==> WifiModeController->initialize() ==> DriverTool->LoadDriver()

通过调用DriverTool->LoadDriver将返回到Android framework中。下面是LoadDriver()的实现。

android/frameworks/opt/net/wifi/libwifi_hal/include/wifi_hal/driver_tool.cppbool DriverTool::LoadDriver() {return ::wifi_load_driver() == 0;}

在wifi_load_driver()方法中,将调用系统接口加载WiFi驱动ko。关于系统insmod接口的调用,本文不做分析。到这里,已梳理完在WifiNative类中调用的startHal()方法。

android/frameworks/opt/net/wifi/libwifi_hal/wifi_hal_common.cppint wifi_load_driver() {... ... ... ...insmod(file,args);... ... ... ...}

调用WifiNl80211Manager类的setupInterfaceForClientMode()方法。

该类的主要对WiFi 80211nl管理接口的封装,接口在WiFicond守护进程中呈现给WiFi框架。该类提供的接口仅使用与WiFi框架,访问权限受selinux权限保护。

setupInterfaceForClientMode()方法主要为Station模式设置接口。

android/frameworks/base/wifi/java/android/net/wifi/nl80211/WifiNl80211Manager.javapublic boolean setupInterfaceForClientMode(@NonNull String ifaceName,@NonNull @CallbackExecutor Executor executor,@NonNull ScanEventCallback scanCallback, @NonNull ScanEventCallback pnoScanCallback) {... ... ... ...// Refresh HandlersmClientInterfaces.put(ifaceName, clientInterface);try {IWifiScannerImpl wificondScanner = clientInterface.getWifiScannerImpl();mWificondScanners.put(ifaceName, wificondScanner);Binder.allowBlocking(wificondScanner.asBinder());ScanEventHandler scanEventHandler = new ScanEventHandler(executor, scanCallback);mScanEventHandlers.put(ifaceName, scanEventHandler);wificondScanner.subscribeScanEvents(scanEventHandler);PnoScanEventHandler pnoScanEventHandler = new PnoScanEventHandler(executor,pnoScanCallback);mPnoScanEventHandlers.put(ifaceName, pnoScanEventHandler);wificondScanner.subscribePnoScanEvents(pnoScanEventHandler);... ... ... ...}

到这里,ClientModeStateMachine状态机在IdleState状态成功处理完了CMD_START消息。状态机将转到“mScanOnlyModeState”状态,将会执行以下调用流程(具体原因可查看状态机机制)。

IdleState.exit()->StartedState.enter()->StartedState.exit()->ScanOnlyModeState.enter()。

九、启动HAL以后,就要启动supplicant了。
在第五步的时候我们调用了ActiveModeWarden.java的startClientModeManagerh函数。start以后会执行switchClientModeManagerRole

 private boolean switchClientModeManagerRole(@NonNull ClientModeManager modeManager) {if (mSettingsStore.isWifiToggleEnabled()) {modeManager.setRole(ActiveModeManager.ROLE_CLIENT_PRIMARY);} else if (checkScanOnlyModeAvailable()) {modeManager.setRole(ActiveModeManager.ROLE_CLIENT_SCAN_ONLY);} else {Log.e(TAG, "Something is wrong, no client mode toggles enabled");return false;}return true;
n true;}

十、从上一步可以看出setRole的参数为ROLE_CLIENT_SCAN_ONLY,所以这里发送的是CMD_SWITCH_TO_CONNECT_MODE广播
frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeManager.java

public void setRole(@Role int role) {Preconditions.checkState(CLIENT_ROLES.contains(role));if (role == ROLE_CLIENT_SCAN_ONLY) {mTargetRole = role;// Switch client mode manager to scan only mode.mStateMachine.sendMessage(ClientModeStateMachine.CMD_SWITCH_TO_SCAN_ONLY_MODE);} else if (CLIENT_CONNECTIVITY_ROLES.contains(role)) {mTargetRole = role;// Switch client mode manager to connect mode.mStateMachine.sendMessage(ClientModeStateMachine.CMD_SWITCH_TO_CONNECT_MODE, role);}
}

十一、看一下CMD_SWITCH_TO_CONNECT_MODE的处理,这里先执行了switchClientInterfaceToConnectivityMode

private class StartedState extends State {public boolean processMessage(Message message) {switch(message.what) {case CMD_SWITCH_TO_CONNECT_MODE:mRole = message.arg1; // could be any one of possible connect mode roles.updateConnectModeState(WifiManager.WIFI_STATE_ENABLING,WifiManager.WIFI_STATE_DISABLED);if (!mWifiNative.switchClientInterfaceToConnectivityMode(mClientInterfaceName)) {updateConnectModeState(WifiManager.WIFI_STATE_UNKNOWN,WifiManager.WIFI_STATE_ENABLING);updateConnectModeState(WifiManager.WIFI_STATE_DISABLED,WifiManager.WIFI_STATE_UNKNOWN);mModeListener.onStartFailure();break;}transitionTo(mConnectModeState);break;

十二、可以看到这里启动了supplicant
frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiNative.java

public boolean switchClientInterfaceToConnectivityMode(@NonNull String ifaceName) {synchronized (mLock) {final Iface iface = mIfaceMgr.getIface(ifaceName);if (!startSupplicant()) {Log.e(TAG, "Failed to start supplicant");teardownInterface(iface.name);mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToSupplicant();return false;}if (!mSupplicantStaIfaceHal.setupIface(iface.name)) {Log.e(TAG, "Failed to setup iface in supplicant on " + iface);teardownInterface(iface.name);mWifiMetrics.incrementNumSetupClientInterfaceFailureDueToSupplicant();return false;}iface.type = Iface.IFACE_TYPE_STA_FOR_CONNECTIVITY;iface.featureSet = getSupportedFeatureSetInternal(iface.name);Log.i(TAG, "Successfully switched to connectivity mode on iface=" + iface);return true;}}
private boolean startSupplicant() {synchronized (mLock) {if (!mIfaceMgr.hasAnyStaIfaceForConnectivity()) {if (!startAndWaitForSupplicantConnection()) {Log.e(TAG, "Failed to connect to supplicant");return false;}if (!mSupplicantStaIfaceHal.registerDeathHandler(new SupplicantDeathHandlerInternal())) {Log.e(TAG, "Failed to register supplicant death handler");return false;}}return true;}
}

在这里等待与supplicant建立连接

private boolean startAndWaitForSupplicantConnection() {// Start initialization if not already started.if (!mSupplicantStaIfaceHal.isInitializationStarted()&& !mSupplicantStaIfaceHal.initialize()) {return false;}if (!mSupplicantStaIfaceHal.startDaemon()) {Log.e(TAG, "Failed to startup supplicant");return false;}boolean connected = false;int connectTries = 0;while (!connected && connectTries++ < CONNECT_TO_SUPPLICANT_RETRY_TIMES) {// Check if the initialization is complete.connected = mSupplicantStaIfaceHal.isInitializationComplete();if (connected) {break;}try {Thread.sleep(CONNECT_TO_SUPPLICANT_RETRY_INTERVAL_MS);} catch (InterruptedException ignore) {}}return connected;
}

十三、这里是通过HIDL来打开supplicant的
frameworks/opt/net/wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java

startDaemon -> startDaemon_V1_1 -> getSupplicantMockableV1_1 -> getSupplicantMockable
protected ISupplicant getSupplicantMockable() throws RemoteException, NoSuchElementException {synchronized (mLock) {ISupplicant iSupplicant = ISupplicant.getService();if (iSupplicant == null) {throw new NoSuchElementException("Cannot get root service.");}return iSupplicant;}
}
android/out/soong/intermediates/hardware/interfaces/wifi/supplicant/1.0/android.hardware.wifi.supplicant-V1.0-java_gen_java/gen/srcs/android/hardware/wifi/supplicant/V1_0/ISupplicant.java
public static ISupplicant getService(String serviceName) throws android.os.RemoteException {return ISupplicant.asInterface(android.os.HwBinder.getService("android.hardware.wifi.supplicant@1.0::ISupplicant", serviceName));
}

十四、 在这个方法中将触发启动wpa_supplicant进程,这里需要注意,在manifest.xml中对其需要进行配置,运行时会将服务名称注册到hwservicemanager中。

wpa_supplicant目录下文件调用:

main.c ==> wpa_supplicant.c->wpa_supplicant_init() ==> notify.c->wpas_notify_supplicant_initialized() ==> hidl.cpp->wpas_hidl_init() ==> Hidl_manager.cpp->registerHidlService()
int HidlManager::registerHidlService(struct wpa_global *global){// Create the main hidl service object and register it.supplicant_object_ = new Supplicant(global);if (supplicant_object_->registerAsService("wpa_supplicant") != android::NO_ERROR) {return 1;}return 0;}

十五、将wpa_supplicant添加注册到hwservicemanager,SupplicantStaIfaceHal.getSupplicantMockable()执行完成返回。

这里再深入看下“supplicant_object_->registerAsService(“wpa_supplicant”)”是如何通过调用注册的呢?

android/out/soong/.intermediates/hardware/interfaces/wifi/supplicant/1.3/android.hardware.wifi.supplicant@1.3_genc++/gen/android/hardware/wifi/supplicant/1.3/SupplicantAll.cppandroid/system/libhidl/transport/ServiceManagement.cppandroid/system/hwservicemanager/ServiceManager.cpp
supplicant_object_->registerAsService("wpa_supplicant") ==> ISupplicant.hal
==> ISupplicantAll.cpp->registerAsService()
==> ::android::hardware::details::registerAsServiceInternal(this, serviceName)
==> ServiceManagement.cpp->registerAsServiceInternal()
==> ServiceManager->addWithChain()==> ServiceManager->addImpl()

十六、wpa_supplicant注册完成后,SupplicantStaIfaceHal类中将收到回调通知信息,

private final IServiceNotification mServiceNotificationCallback =
new IServiceNotification.Stub() {public void onRegistration(String fqName, String name, boolean preexisting) {synchronized (mLock) {if (!initSupplicantService()) {supplicantServiceDiedHandler(mDeathRecipientCookie);
}

返回通知的调用逻辑。

SupplicantStaIfaceHal.initSupplicantService() -> SupplicantStaIfaceHal.getSupplicantMockable()

十七、到此位置supplicant已经启动。
switchClientInterfaceToConnectivityMode会继续调用SupplicantStaIfaceHal.setupIface()方法设置接口。设置成功后,就会打印成功的日志。

Log.i(TAG, "Successfully switched to connectivity mode on iface=" + iface);

十八、CMD_SWITCH_TO_CONNECT_MODE消息处理完以后状态机就会切换到ConnectModeState。
这里会调用setOperationalMode

transitionTo(mConnectModeState);
private class ConnectModeState extends State {@Overridepublic void enter() {Log.d(TAG, "entering ConnectModeState");mClientModeImpl.registerModeListener(mClientModeImplListener);mClientModeImpl.setOperationalMode(ClientModeImpl.CONNECT_MODE,mClientInterfaceName);}

十九、这里会进入到mDisconnectedState
frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeImpl.java

public void setOperationalMode(int mode, String ifaceName) {if (mVerboseLoggingEnabled) {log("setting operational mode to " + String.valueOf(mode) + " for iface: " + ifaceName);}mModeChange = true;if (mode != CONNECT_MODE) {// we are disabling client mode...   need to exit connect mode nowtransitionTo(mDefaultState);} else {// do a quick sanity check on the iface name, make sure it isn't nullif (ifaceName != null) {mInterfaceName = ifaceName;updateInterfaceCapabilities(ifaceName);transitionTo(mDisconnectedState);mWifiScoreReport.setInterfaceName(ifaceName);} else {Log.e(TAG, "supposed to enter connect mode, but iface is null -> DefaultState");transitionTo(mDefaultState);}}// use the CMD_SET_OPERATIONAL_MODE to force the transitions before other messages are// handled.sendMessageAtFrontOfQueue(CMD_SET_OPERATIONAL_MODE);
}

二十、ActiveModeWarden类中设置的ClientLister将被触发回调。 wifiScaner.setScanningEnabled()发送消息CMD_ENABLE,给到WiFiscanningSerivceimpl类中。
到这里,WiFi已处于打开状态,并将进行扫描网络,待连接。WiFi打开流程分析完成。

ActiveModeWarden.ClientListener -> ScanRequestProxy.enableScanning() ->
ScanRequestProxy.enableScanningInternal() -> wifiScaner.setScanningEnabled()

Android 11 WiFi开启流程相关推荐

  1. Android 11 WiFi启动流程

    欢迎大家一起学习探讨通信之WLAN.本节重点基于Android11分析讨论WiFi开启流程.用户点击一下"WiFi"开关,WiFi开启了.看似如此简单操作,但系统流程调用还是相当复 ...

  2. Android 11 WiFi扫描流程梳理

    上一篇我们梳理了WiFi的开启流程,Android11 WiFi开启流程,在最后我们说到ActiveModeWarden中注册了ClientListener监听器.我们接着这个逻辑继续梳理一下打开Wi ...

  3. Android R WiFi热点流程浅析

    Android R WiFi热点流程浅析 Android上的WiFi SoftAp功能是用户常用的功能之一,它能让我们分享手机的网络给其他设备使用. 那Android系统是如何实现SoftAp的呢,这 ...

  4. android移植wifi驱动流程porting

    android载入wifi驱动流程 wifi_load_driver check_wifi_chip_type_string get_wifi_device_id save_wifi_chip_typ ...

  5. (四十四)Android O WiFi启动流程梳理

    前言:最近又重新拿起来WiFi模块,从WiFi 各个流程梳理开始复习一下. 参考博客:https://blog.csdn.net/csdn_of_coder/article/details/51541 ...

  6. Android 11 Wifi之ConnectivityService流程

    一.NetworkFactory注册 frameworks/base/services/java/com/android/server/SystemServer.java frameworks/opt ...

  7. Android之wifi工作流程

    Android Wifi的工作流程 一.WIFI工作相关部分 Wifi 网卡状态 1.    WIFI_STATE_DISABLED:WIFI网卡不可用 2.    WIFI_STATE_DISABL ...

  8. andriod R wifi 开启流程

    前言: wifi chip 的开机初始化流程在这里我们就后面加上,本博客就暂时只有wifi的开启到wifi的连接成功. 虽然都是andriod R,但是还是有可能不同的基线版本代码还是有些许差别,所以 ...

  9. Android 11 热点(softap)流程分析

    最近在做Android 11中热点的功能,主要是网络共享,一个是usb网络共享,一个是热点网络共享,本文只是记录热点分享的流程. 一. settings 里面打开热点的接口跟原来类似 packages ...

最新文章

  1. access 导入 txt sql语句_从零开始学习 MySQL 系列索引、视图、导入和导出
  2. C++ STL : 模拟实现STL中的关联式容器map和set
  3. 开学测试代码——需求征集系统
  4. oracle 会话实例,返璞归真:Oracle实例级别和会话级别的参数设置辨析
  5. 遥感、制图学中各种图的区别
  6. mysql数据库修改排序规则
  7. hibernate4调用mysql存储过程_Hibernate4.x执行mysql的存储过程
  8. Operations Manager 2007 R2 Beta可下载试用
  9. kernel编译速度提高
  10. Android 下拉刷新组件SwipeToLoadLayout源码解析
  11. 纯CSS一分钟让网站拥有暗黑模式切换功能
  12. 笔记本电脑亮度突然无法用快捷键调节
  13. IEEE754-2008 标准详解(五):异常
  14. Linux有问必答:如何在Linux命令行中刻录ISO或NRG镜像到DVD
  15. Codeforces 1006A
  16. 使用ThinkMusic网站源码配合cpolar,发布本地音乐网站
  17. JS 的5个不良编码习惯,现在就改掉吧
  18. 智能穿戴:致我们触手可及的未来?
  19. mysql 访问寄存器_汇编寄存器(内存访问)基础知识之三---mov指令
  20. CTF线下AWD攻防模式的准备工作及起手式

热门文章

  1. 正则表达正整数/正则表达正整数不包括0
  2. 计算机领域网络顶级会议,【分享】计算机领域的一些顶级会议【已搜索,无重复】 - 信息科学 - 小木虫 - 学术 科研 互动社区...
  3. windows下的中文文件名共享在linux下显示乱码的问题
  4. 论文总结(一)—基于深度学习的普通遥感图像质量改进
  5. 围棋口诀两百句(转)
  6. phonegap 修改app的名称
  7. 兔子数列(斐波拉契数列)javscript的三种写法
  8. QGIS离线GeoJSON数据,使用Cesium加载并根据楼层高度拉伸(weixin公众号【图说GIS】)
  9. 8. Spring Security 5.1之 OAuth 2.0 Login
  10. emi滤波matlab,EMI滤波器的基本原理