UsbHostManager解析
UsbHostManager和UsbDeviceManager的区别在于,UsbDeviceManager是将手机作为一个设备,比如手机连上电脑,使用adb、mtp等;而UsbHostManager,是将手机作为一个host,比如手机连接usb鼠标、usb摄像头等,就会new出一个UsbDevice出来。
UsbHostManager初始化
UsbHostManager和UsbDeviceManager都是在UsbService中创建的
(frameworks\base\services\usb\java\com\android\server\usb\UsbService.java),如下所示:
public UsbService(Context context) {
mContext = context;
mUserManager = context.getSystemService(UserManager.class);
mSettingsManager = new UsbSettingsManager(context, this);
mPermissionManager = new UsbPermissionManager(context, this);
mAlsaManager = new UsbAlsaManager(context);
final PackageManager pm = mContext.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
//创建UsbHostManager
mHostManager = new UsbHostManager(context, mAlsaManager, mPermissionManager);
}
if (new File("/sys/class/android_usb").exists()) {
//创建UsbDeviceManager
mDeviceManager = new UsbDeviceManager(context, mAlsaManager, mSettingsManager,
mPermissionManager);
}
if (mHostManager != null || mDeviceManager != null) {
mPortManager = new UsbPortManager(context);
}
BroadcastReceiver receiver = new BroadcastReceiver() {
......
};
...
mContext.registerReceiver(receiver, filter, null, null);
}
上面为UsbService的构造函数,其在构造函数中new了UsbHostManager和UsbDeviceManager。UsbService.java中一个systemReady()方法,该方法如下:
public void systemReady() {
mAlsaManager.systemReady();
if (mDeviceManager != null) {
mDeviceManager.systemReady();
}
if (mHostManager != null) {
mHostManager.systemReady();
}
if (mPortManager != null) {
mPortManager.systemReady();
}
}
该方法会分别调用在UsbService构造函数中new出的mAlsaManager、mDeviceManager、mHostManager、和mPortManager的systemReady()方法。那我们现在来看UsbHostManager,UsbHostManager在UsbHostManager.java中(.\frameworks\base\services\usb\java\com\android\server\usb\UsbHostManager.java),在UsbHostManager中,有一个成员变量如下:
// 包含所有连接的USB设备
private final HashMap<String, UsbDevice> mDevices = new HashMap<>();
和host连接的USB设备会保存在这个HashMap中。
刚才已经分析了UsbService的systemReady()方法会调用UsbHostManager的systemReady(),该方法如下:
public void systemReady() {
synchronized (mLock) {
// 创建一个线程调用本机代码等待USB主机事件。
// 这个线程会在usbDeviceAdded和usbdevicermoved上回调我们。
Runnable runnable = this::monitorUsbHostBus;
new Thread(null, runnable, "UsbService host thread").start();
}
}
可以看到,UsbHostManager的systemReady()方法会创建一个线程,该线程会运行monitorUsbHostBus函数,该函数在什么地方呢?
private native void monitorUsbHostBus();
在UsbHostManager类中有如下声明,可以看出来这个函数是一个Native函数,即该调用是一个JNI调用。
UsbHostManager的Native层
该函数对应在com_android_server_UsbHostManager.cpp中
(frameworks\base\services\core\jni\com_android_server_UsbHostManager.cpp)ndroid_server_UsbHostManager_monitorUsbHostBus函数,如下所示:
static void android_server_UsbHostManager_monitorUsbHostBus(JNIEnv* /* env */, jobject thiz)
{
struct usb_host_context* context = usb_host_init();
if (!context) {
ALOGE("usb_host_init failed");
return;
}
// 这个永远不会返回,所以直接传递这个是安全的
usb_host_run(context, usb_device_added, usb_device_removed, NULL, (void *)thiz);
}
该函数进来之后通过usb_host_init()函数创建了一个struct usb_host_context对象,usb_host_init()函数在system\core\libusbhost\usbhost.c中定义,如下:
struct usb_host_context *usb_host_init()
{
struct usb_host_context *context = calloc(1, sizeof(struct usb_host_context));
if (!context) {
fprintf(stderr, "out of memory in usb_host_context\n");
return NULL;
}
context->fd = inotify_init();
if (context->fd < 0) {
fprintf(stderr, "inotify_init failed\n");
free(context);
return NULL;
}
return context;
}
可以看到函数进来分配了一块struct usb_host_context内存之后,使用inotify_init()新建了一个INotify,并且将这个INotify的fd赋值给了usb_host_context的成员变量。什么是INotify呢?
简单说一下,INotify是一个内核用于通知用户空间程序文件系统发生变化的一种机制,例如文件增加、删除等事件都可以立刻让用户得知。类似的机制还有,Hotplug 是一种内核向用户态应用通报关于热插拔设备一些事件发生的机制,桌面系统能够利用它对设备进行有效的管理;udev 动态地维护 /dev 下的设备文件等。后面再写关于INotify的内容。
再回来,usb_host_init创建了一个INotify之后,将这个INotify的fd赋值给了usb_host_context的成员变量,然后在android_server_UsbHostManager_monitorUsbHostBus中就进入了usb_host_run函数,需要注意这里将JNI中的jobject thiz作为指针也传给了usb_host_run。该函数也在usbhost.c中,如下:
void usb_host_run(struct usb_host_context *context,
usb_device_added_cb added_cb,
usb_device_removed_cb removed_cb,
usb_discovery_done_cb discovery_done_cb,
void *client_data)
{
int done;
done = usb_host_load(context, added_cb, removed_cb, discovery_done_cb, client_data);
while (!done) {
done = usb_host_read_event(context);
}
} /* usb_host_run() */
android_server_UsbHostManager_monitorUsbHostBus调用usb_host_run的时候传入了两个比较关键的函数指针,usb_device_added和usb_device_removed,看名字就知道这两个函数和USB设备的添加和删除有关,两个函数都定义在com_android_server_UsbHostManager.cpp文件中,我们后面再对这两个函数作分析。
usb_host_run()进来之后直接就调用了usb_host_load函数,同时将两个函数指针传了进去(discovery_done_cb为NULL),我们接着看usb_host_load函数,也是在usbhost.c中,如下:
int usb_host_load(struct usb_host_context *context,
usb_device_added_cb added_cb,
usb_device_removed_cb removed_cb,
usb_discovery_done_cb discovery_done_cb,
void *client_data)
{
int done = 0;
int i;
context->cb_added = added_cb;
context->cb_removed = removed_cb;
context->data = client_data;
/* 观察在USB_FS_DIR中添加和删除的文件 */
context->wddbus = -1;
for (i = 0; i < MAX_USBFS_WD_COUNT; i++)
context->wds[i] = -1;
/* 查看根目录是否有新的子目录 #define DEV_DIR "/dev" */
context->wdd = inotify_add_watch(context->fd, DEV_DIR, IN_CREATE | IN_DELETE);
if (context->wdd < 0) {
fprintf(stderr, "inotify_add_watch failed\n");
if (discovery_done_cb)
discovery_done_cb(client_data);
return done;
}
//#define MAX_USBFS_WD_COUNT 10
watch_existing_subdirs(context, context->wds, MAX_USBFS_WD_COUNT);
/* 在我们设置inotify之后,首先检查现有的设备 */
done = find_existing_devices(added_cb, client_data);
if (discovery_done_cb)
done |= discovery_done_cb(client_data);
return done;
} /* usb_host_load() */
在usb_host_load中,首先将两个函数指针赋值给struct usb_host_context中的成员变量,client_data是在JNI中传入的thiz指针,discovery_done_cb传入的值为NULL。这里又看到了inotify开头的函数inotify_add_watch,这个也是和inotify机制相关的东西,这里再简单的说一下。
在inotify机制中,一个称作watches 的对象管理文件系统的变化事件,每一个 watch 是一个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的 inotify 事件,每一个位对应一个 inotify 事件。具体的一个Watch 对象通过 watch描述符引用, watch描述符就是inotify_add_watch函数返回的int值,添加一个watch通过目标文件或目录的路径名来添加,如果添加的watch是一个目录,那么该watches将会根据设置的事件掩码来返回在该目录下的所有文件上面发生的事件。
再回到代码中来,在usb_host_load中,首先通过inotify_add_watch,添加了一个目录watch("/dev"),其事件掩码设置为IN_CREATE | IN_DELETE(即观察创建和删除事件),并将这个watch的描述符储存在usb_host_context的成员变量wdd中。后面紧接着就调用watch_existing_subdirs函数,该函数也在usbhost.c中,如下所示:
static void watch_existing_subdirs(struct usb_host_context *context,
int *wds, int wd_count)
{
char path[100];
int i, ret;
//#define USB_FS_DIR "/dev/bus/usb"
wds[0] = inotify_add_watch(context->fd, USB_FS_DIR, IN_CREATE | IN_DELETE);
if (wds[0] < 0)
return;
/* 查看USB_FS_DIR的现有子目录 #define USB_FS_DIR "/dev/bus/usb" */
for (i = 1; i < wd_count; i++) {
snprintf(path, sizeof(path), USB_FS_DIR "/%03d", i);//
ret = inotify_add_watch(context->fd, path, IN_CREATE | IN_DELETE);
if (ret >= 0)
wds[i] = ret;
}
}
wds是usb_host_context中的一个int数组,其长度为MAX_USBFS_WD_COUNT。这里又添加了一个目录watch("/dev/usb"),其事件掩码也设置为IN_CREATE | IN_DELETE,然后将watch描述符储存在wds[0]的位置。
紧接着就是一个for循环,该for循环将"/dev/bus/usb"目录下的设备目录(从001开始到MAX_USBFS_WD_COUNT),都添加到inotify中去,作为目录watch进行观察,然后各个watch描述符依次存放在wds数组剩余的位置中。
watch_existing_subdirs函数返回之后,在usb_host_load函数中接着又调用了find_existing_devices函数。(usbhost.c)如下所示:
/* 如果其中一个回调指示我们完成,则返回true */
static int find_existing_devices(usb_device_added_cb added_cb,
void *client_data)
{
char busname[32];
DIR *busdir;
struct dirent *de;
int done = 0;
//#define USB_FS_DIR "/dev/bus/usb"
busdir = opendir(USB_FS_DIR);
if(busdir == 0) return 0;
while ((de = readdir(busdir)) != 0 && !done) {
if(badname(de->d_name)) continue;
snprintf(busname, sizeof(busname), USB_FS_DIR "/%s", de->d_name);
done = find_existing_devices_bus(busname, added_cb,
client_data);
} //end of busdir while
closedir(busdir);
return done;
}
该函数首先打开"/dev/bus/usb"目录,然后对该目录下的目录调用find_existing_devices_bus函数(usbhost.c),如下:
static int find_existing_devices_bus(char *busname,
usb_device_added_cb added_cb,
void *client_data)
{
char devname[32];
DIR *devdir;
struct dirent *de;
int done = 0;
devdir = opendir(busname);
if(devdir == 0) return 0;
while ((de = readdir(devdir)) && !done) {
if(badname(de->d_name)) continue;
snprintf(devname, sizeof(devname), "%s/%s", busname, de->d_name);
done = added_cb(devname, client_data);
} // end of devdir while
closedir(devdir);
return done;
}
该函数会打开之前传入的目录,例如:"/dev/bus/usb/001",然后会遍历该目录下的所有文件,将文件名和之前的目录路径组合到一起就可以得到准确的文件位置,这个文件就代表了一个USB设备,该USB设备是在UsbHostManager运行之前就挂载好了的,所以此时上层还不知道该USB设备的存在,所以需要对该USB设备向上层做补充的通知,即通过added_cb函数指针调用usb_device_added函数。
这里我们还是先不分析usb_device_added函数,只需要知道find_existing_devices函数的作用是,将"/dev/bus/usb"子目录下,在UsbHostManager运行之前就挂载好了的USB设备向上做补充的通知,让上层感知到这些USB设备的存在。
这样usb_host_load函数就返回了,该函数返回之后,usb_host_run会进入一个while循环去执行usb_host_read_event函数(usbhost.c),usb_host_read_event函数非常的长如下:
int usb_host_read_event(struct usb_host_context *context)
{
struct inotify_event* event;
char event_buf[512];
char path[100];
int i, ret, done = 0;
int offset = 0;
int wd;
ret = read(context->fd, event_buf, sizeof(event_buf));
if (ret >= (int)sizeof(struct inotify_event)) {
while (offset < ret && !done) {
event = (struct inotify_event*)&event_buf[offset];
done = 0;
wd = event->wd;
if (wd == context->wdd) {
if ((event->mask & IN_CREATE) && !strcmp(event->name, "bus")) {
context->wddbus = inotify_add_watch(context->fd, DEV_BUS_DIR, IN_CREATE | IN_DELETE);
if (context->wddbus < 0) {
done = 1;
} else {
watch_existing_subdirs(context, context->wds, MAX_USBFS_WD_COUNT);
done = find_existing_devices(context->cb_added, context->data);
}
}
} else if (wd == context->wddbus) {
if ((event->mask & IN_CREATE) && !strcmp(event->name, "usb")) {
watch_existing_subdirs(context, context->wds, MAX_USBFS_WD_COUNT);
done = find_existing_devices(context->cb_added, context->data);
} else if ((event->mask & IN_DELETE) && !strcmp(event->name, "usb")) {
for (i = 0; i < MAX_USBFS_WD_COUNT; i++) {
if (context->wds[i] >= 0) {
inotify_rm_watch(context->fd, context->wds[i]);
context->wds[i] = -1;
}
}
}
} else if (wd == context->wds[0]) {
i = atoi(event->name);
snprintf(path, sizeof(path), USB_FS_DIR "/%s", event->name);
D("%s subdirectory %s: index: %d\n", (event->mask & IN_CREATE) ?
"new" : "gone", path, i);
if (i > 0 && i < MAX_USBFS_WD_COUNT) {
int local_ret = 0;
if (event->mask & IN_CREATE) {
local_ret = inotify_add_watch(context->fd, path,
IN_CREATE | IN_DELETE);
if (local_ret >= 0)
context->wds[i] = local_ret;
done = find_existing_devices_bus(path, context->cb_added,
context->data);
} else if (event->mask & IN_DELETE) {
inotify_rm_watch(context->fd, context->wds[i]);
context->wds[i] = -1;
}
}
} else {
for (i = 1; (i < MAX_USBFS_WD_COUNT) && !done; i++) {
if (wd == context->wds[i]) {
snprintf(path, sizeof(path), USB_FS_DIR "/%03d/%s", i, event->name);
if (event->mask == IN_CREATE) {
D("new device %s\n", path);
done = context->cb_added(path, context->data);
} else if (event->mask == IN_DELETE) {
D("gone device %s\n", path);
done = context->cb_removed(path, context->data);
}
}
}
}
offset += sizeof(struct inotify_event) + event->len;
}
}
return done;
} /* usb_host_read_event() */
大致就是使用read调用去读取context->fd中的数据,这个fd就是一开始创建的inotify的fd,可以对这个fd做select、poll()等操作。read读到的数据可以转换为struct inotify_event类型的结构体,通过这个结构体中的一些字段来走不同的分支,这里先关注最后一个分支,即是wds数组中,索引0之外的watch描述符发生了变化就会走该分支,根据mask来判断是发生了IN_CREATE事件还是IN_DELETE事件,前者调用usb_device_added函数,后者调用usb_device_removed函数。
添加和删除USB设备时的回调函数
usb_device_added函数
当mask为IN_CREATE会调用该回调函数。
static int usb_device_added(const char *devAddress, void* clientData) {
struct usb_device *device = usb_device_open(devAddress);
if (!device) {
ALOGE("usb_device_open failed\n");
return 0;
}
const usb_device_descriptor* deviceDesc = usb_device_get_device_descriptor(device);
int classID = deviceDesc->bDeviceClass;
int subClassID = deviceDesc->bDeviceSubClass;
// get the raw descriptors
int numBytes = usb_device_get_descriptors_length(device);
if (numBytes > 0) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
jobject thiz = (jobject)clientData;
jstring deviceAddress = env->NewStringUTF(devAddress);
jbyteArray descriptorsArray = env->NewByteArray(numBytes);
const jbyte* rawDescriptors = (const jbyte*)usb_device_get_raw_descriptors(device);
env->SetByteArrayRegion(descriptorsArray, 0, numBytes, rawDescriptors);
env->CallBooleanMethod(thiz, method_usbDeviceAdded,
deviceAddress, classID, subClassID, descriptorsArray);
env->DeleteLocalRef(descriptorsArray);
env->DeleteLocalRef(deviceAddress);
checkAndClearExceptionFromCallback(env, __FUNCTION__);
} else {
// TODO return an error code here?
ALOGE("error reading descriptors\n");
}
usb_device_close(device);
return 0;
}
参数devAddress为发生变化的文件的路径,clientData为android_server_UsbHostManager_monitorUsbHostBus时设置的jobject thiz指针。该函数通过JNI调用UsbHostManager.java中的usbDeviceAdded方法:
private boolean usbDeviceAdded(String deviceAddress, int deviceClass, int deviceSubclass,
byte[] descriptors) {
if (DEBUG) {
Slog.d(TAG, "usbDeviceAdded(" + deviceAddress + ") - start");
}
if (isBlackListed(deviceAddress)) {
if (DEBUG) {
Slog.d(TAG, "device address is black listed");
}
return false;
}
if (isBlackListed(deviceClass, deviceSubclass)) {
if (DEBUG) {
Slog.d(TAG, "device class is black listed");
}
return false;
}
UsbDescriptorParser parser = new UsbDescriptorParser(deviceAddress, descriptors);
if (deviceClass == UsbConstants.USB_CLASS_PER_INTERFACE
&& !checkUsbInterfacesBlackListed(parser)) {
return false;
}
// Potentially can block as it may read data from the USB device.
logUsbDevice(parser);
if (isBlackListed(parser)) {
if (DEBUG) {
Slog.d(TAG, "device UsbDescriptorParser is black listed");
}
return false;
}
synchronized (mLock) {
if (mDevices.get(deviceAddress) != null) {
Slog.w(TAG, "device already on mDevices list: " + deviceAddress);
//TODO If this is the same peripheral as is being connected, replace
// it with the new connection.
return false;
}
UsbDevice.Builder newDeviceBuilder = parser.toAndroidUsbDeviceBuilder();
if (newDeviceBuilder == null) {
Slog.e(TAG, "Couldn't create UsbDevice object.");
// Tracking
addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT_BADDEVICE,
parser.getRawDescriptors());
} else {
UsbSerialReader serialNumberReader = new UsbSerialReader(mContext,
mPermissionManager, newDeviceBuilder.serialNumber);
UsbDevice newDevice = newDeviceBuilder.build(serialNumberReader);
serialNumberReader.setDevice(newDevice);
mDevices.put(deviceAddress, newDevice);
Slog.d(TAG, "Added device " + newDevice);
// It is fine to call this only for the current user as all broadcasts are
// sent to all profiles of the user and the dialogs should only show once.
ComponentName usbDeviceConnectionHandler = getUsbDeviceConnectionHandler();
if (usbDeviceConnectionHandler == null) {
getCurrentUserSettings().deviceAttached(newDevice);
} else {
getCurrentUserSettings().deviceAttachedForFixedHandler(newDevice,
usbDeviceConnectionHandler);
}
mUsbAlsaManager.usbDeviceAdded(deviceAddress, newDevice, parser);
// Tracking
addConnectionRecord(deviceAddress, ConnectionRecord.CONNECT,
parser.getRawDescriptors());
// Stats collection
FrameworkStatsLog.write(FrameworkStatsLog.USB_DEVICE_ATTACHED,
newDevice.getVendorId(), newDevice.getProductId(),
parser.hasAudioInterface(), parser.hasHIDInterface(),
parser.hasStorageInterface(),
FrameworkStatsLog.USB_DEVICE_ATTACHED__STATE__STATE_CONNECTED, 0);
}
}
if (DEBUG) {
Slog.d(TAG, "beginUsbDeviceAdded(" + deviceAddress + ") end");
}
return true;
}
这个方法里面会向mDevices(一开始的那个HashMap)中填加UsbDevice。
usb_device_removed函数
该函数和上面的流程相同,这里直接贴代码。
static int usb_device_removed(const char *devAddress, void* clientData) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
jobject thiz = (jobject)clientData;
jstring deviceAddress = env->NewStringUTF(devAddress);
env->CallVoidMethod(thiz, method_usbDeviceRemoved, deviceAddress);
env->DeleteLocalRef(deviceAddress);
checkAndClearExceptionFromCallback(env, __FUNCTION__);
return 0;
}
private void usbDeviceRemoved(String deviceAddress) {
if (DEBUG) {
Slog.d(TAG, "usbDeviceRemoved(" + deviceAddress + ") end");
}
synchronized (mLock) {
UsbDevice device = mDevices.remove(deviceAddress);
if (device != null) {
Slog.d(TAG, "Removed device at " + deviceAddress + ": " + device.getProductName());
mUsbAlsaManager.usbDeviceRemoved(deviceAddress);
mPermissionManager.usbDeviceRemoved(device);
getCurrentUserSettings().usbDeviceRemoved(device);
ConnectionRecord current = mConnected.get(deviceAddress);
// Tracking
addConnectionRecord(deviceAddress, ConnectionRecord.DISCONNECT, null);
if (current != null) {
UsbDescriptorParser parser = new UsbDescriptorParser(deviceAddress,
current.mDescriptors);
// Stats collection
FrameworkStatsLog.write(FrameworkStatsLog.USB_DEVICE_ATTACHED,
device.getVendorId(), device.getProductId(), parser.hasAudioInterface(),
parser.hasHIDInterface(), parser.hasStorageInterface(),
FrameworkStatsLog.USB_DEVICE_ATTACHED__STATE__STATE_DISCONNECTED,
System.currentTimeMillis() - current.mTimestamp);
}
} else {
Slog.d(TAG, "Removed device at " + deviceAddress + " was already gone");
}
}
}
UsbHostManager解析的更多相关文章
- 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新
本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...
- .NET Core中的认证管理解析
.NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...
- Html Agility Pack 解析Html
Hello 好久不见 哈哈,今天给大家分享一个解析Html的类库 Html Agility Pack.这个适用于想获取某网页里面的部分内容.今天就拿我的Csdn的博客列表来举例. 打开页面 用Fir ...
- 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新
[原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...
- 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新
上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...
- 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例
前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...
- Asp.Net WebApi核心对象解析(下篇)
在接着写Asp.Net WebApi核心对象解析(下篇)之前,还是一如既往的扯扯淡,元旦刚过,整个人还是处于晕的状态,一大早就来处理系统BUG,简直是坑爹(好在没让我元旦赶过来该BUG),队友挖的坑, ...
- 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye
一.写在前面 对于C++来说,内存泄漏就是new出来的对象没有delete,俗称野指针:而对于java来说,就是new出来的Object放在Heap上无法被GC回收:而这里就把我之前的一篇内存泄漏的总 ...
- SQL Server 数据加密功能解析
SQL Server 数据加密功能解析 转载自: 腾云阁 https://www.qcloud.com/community/article/194 数据加密是数据库被破解.物理介质被盗.备份被窃取的最 ...
- .NetCore中的日志(1)日志组件解析
.NetCore中的日志(1)日志组件解析 0x00 问题的产生 日志记录功能在开发中很常用,可以记录程序运行的细节,也可以记录用户的行为.在之前开发时我一般都是用自己写的小工具来记录日志,输出目标包 ...
随机推荐
- 微服务集成springsecurity实现认证
module:auth 1.添加依赖:spring-cloud-starter-security与spring-cloud-starter-oauth2 2.配置WebSecurityConfig:注 ...
- Fiddler使用界面介绍-底部状态栏
底部状态栏 1.Capturing抓包状态 Capturing:Fiddler正在抓包 空白:Fiddler停止抓包 2.All Processes抓取进程类型 All Processes:抓取所有进 ...
- 【H5】04 建立超链接
摘自: https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML/Creating_hyperlinks 超链接 ...
- 【Mybatis-Plus】05 条件构造器 ConditionConstructor
理解: 原来叫条件构造器,我一直以为都是封装条件对象 即SQL的查询条件,不过都一样. 其目的是因为的实际的需求灵活多变,而我们的SQL的筛选条件也需要跟着变化, 但是有一些固定的字段固定的方式可以保 ...
- 绝对要收藏!!! JavaEE开发常用注解
目录 前言 1.Mybatis常用注解 2.SpringMVC常用注解 3.Spring常用注解 1. IoC注解 2. DI注解 3. 事务注解 4.SpringBoot常用注解 5.Lombok注 ...
- 【转载】 Tensorflow学习笔记-模型保存与加载
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/lovelyaiq/article/det ...
- 浪潮计算平台之AI方向——AI_Station开发环境的使用总结
概览: 1. 开发环境 使用默认的设置,不改挂载路径: 可以看到在容器内对挂载的目录进行文件操作是可以真实记录到实际的文件目录内的. 对挂载路径的另一种设置: 不使用默认的设置,手动更改挂载路径: ...
- 分享某Python下的mpi教程 —— A Python Introduction to Parallel Programming with MPI 1.0.2 documentation
如题: 无意中发现了一个Python下的mpi教程<A Python Introduction to Parallel Programming with MPI 1.0.2 documentat ...
- 强化学习算法如何将GPU利用率提高到100%——在线强化学习如何将GPU利用率提升至100%
一直有个疑问,那就是"强化学习算法如何将GPU利用率提高到100%",在一些论坛中也有人会提出这样的问题,但是一直也没有人比较正面的回答过这个问题,为此正好自己又想到了这么一个问题 ...
- 20-canvas之形变
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...