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解析的更多相关文章

  1. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  2. .NET Core中的认证管理解析

    .NET Core中的认证管理解析 0x00 问题来源 在新建.NET Core的Web项目时选择“使用个人用户账户”就可以创建一个带有用户和权限管理的项目,已经准备好了用户注册.登录等很多页面,也可 ...

  3. Html Agility Pack 解析Html

    Hello 好久不见 哈哈,今天给大家分享一个解析Html的类库 Html Agility Pack.这个适用于想获取某网页里面的部分内容.今天就拿我的Csdn的博客列表来举例. 打开页面  用Fir ...

  4. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

  5. 【原】Android热更新开源项目Tinker源码解析系列之二:资源文件热更新

    上一篇文章介绍了Dex文件的热更新流程,本文将会分析Tinker中对资源文件的热更新流程. 同Dex,资源文件的热更新同样包括三个部分:资源补丁生成,资源补丁合成及资源补丁加载. 本系列将从以下三个方 ...

  6. 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...

  7. Asp.Net WebApi核心对象解析(下篇)

    在接着写Asp.Net WebApi核心对象解析(下篇)之前,还是一如既往的扯扯淡,元旦刚过,整个人还是处于晕的状态,一大早就来处理系统BUG,简直是坑爹(好在没让我元旦赶过来该BUG),队友挖的坑, ...

  8. 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye

    一.写在前面 对于C++来说,内存泄漏就是new出来的对象没有delete,俗称野指针:而对于java来说,就是new出来的Object放在Heap上无法被GC回收:而这里就把我之前的一篇内存泄漏的总 ...

  9. SQL Server 数据加密功能解析

    SQL Server 数据加密功能解析 转载自: 腾云阁 https://www.qcloud.com/community/article/194 数据加密是数据库被破解.物理介质被盗.备份被窃取的最 ...

  10. .NetCore中的日志(1)日志组件解析

    .NetCore中的日志(1)日志组件解析 0x00 问题的产生 日志记录功能在开发中很常用,可以记录程序运行的细节,也可以记录用户的行为.在之前开发时我一般都是用自己写的小工具来记录日志,输出目标包 ...

随机推荐

  1. 一文详解 JuiceFS 读性能:预读、预取、缓存、FUSE 和对象存储

    在高性能计算场景中,往往采用全闪存架构和内核态并行文件系统,以满足性能要求.随着数据规模的增加和分布式系统集群规模的增加,全闪存的高成本和内核客户端的运维复杂性成为主要挑战. JuiceFS,是一款全 ...

  2. Scratch作品-巴黎2024奥运会

    ​ <Scratch作品-巴黎2024奥运会>是一款以巴黎2024年奥运会为主题的互动作品,专为儿童和青少年设计.通过Scratch编程语言,这个作品生动地再现了奥运会的精彩瞬间,结合了动 ...

  3. docker 容器卷

    创建各种卷 [root@docker ~]# docker volume create mqy-vo101 mqy-vo101 [root@docker ~]# docker inspect mqy- ...

  4. 【MySQL】java.sql.SQLException: Illegal mix of collations (utf8mb4_0900_ai_ci,IMPLICIT) and (utf8mb4_general_ci,IMPLICIT) for operation '='

    问题原因参考: http://t.zoukankan.com/zhulei2-p-13451554.html collations 排序规则 Illegal mix 非法混合 SQL报错指出,操作符等 ...

  5. 【Mybatis】08 ResultMap、Association、分步查询、懒加载

    ResultMap自定义结果集 可以把查询返回的结果集封装成复杂的JavaBean对象 原来的ResultType属性,只能把查询到的结果集转换为简单的JavaBean 什么是简单的JavaBean对 ...

  6. 【Spring-Mail】

    需要的pom依赖: <dependency> <groupId>org.springframework</groupId> <artifactId>sp ...

  7. 预处理共轭梯度算法(Preconditioned Conjugate Gradients Method)

    预处理共轭梯度算法(Preconditioned Conjugate Gradients Method) 给出百度百科上的解释: 预处理共轭梯度法 预处理共轭梯度法是.不必预先估计参数等特点. 共轭梯 ...

  8. NoSQL之事务和相关特性

    NoSQL 简介 NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL". 在现代的计算系统上每天网络上都会产生庞大的数据量. 这些数据有很大一部分是由关 ...

  9. Java IO流的简单使用 通俗易懂 超详细 【内含案例】

    IO流简单使用 InputStream 字节输入流 OutputStream 字节输出流 Reader 字符输入流 Writer 字符输出流 代码示例 输入和输出是相对于程序来说的,读取到程序中叫做输 ...

  10. Docker不同宿主机网络打通

    本方式使用docker Swarm集群的方式创建overlay 网络进行打通 背景 因java微服务使用nacos做配置中心,为了解决Nacos服务注册使用Docker容器内网ip问题,使用此方案 前 ...