Android ART运行时无缝替换Dalvik虚拟机的过程分析
目录
一,概述
二,dex文件优化
一,概述
Android 4.4发布了一个ART运行时,准备用来替换掉之前一直使用的Dalvik虚拟机,希望籍此解决饱受诟病的性能问题。老罗不打算分析ART的实现原理,只是很有兴趣知道ART是如何无缝替换掉原来的Dalvik虚拟机的。毕竟在原来的系统中,大量的代码都是运行在Dalvik虚拟机里面的。开始觉得这个替换工作是挺复杂的,但是分析了相关代码之后,发现思路是很清晰的。本文就详细分析这个无缝的替换过程。
我们知道,Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。因此,ART运行时最理想的方式也是实现为一个Java虚拟机的形式,这样就可以很容易地将Dalvik虚拟机替换掉。注意,我们这里说实现为Java虚拟机的形式,实际上是指提供一套完全与Java虚拟机兼容的接口。例如,Dalvik虚拟机在接口上与Java虚拟机是一致的,但是它的内部可以是完全不一样的东西。
实际上,ART运行时就是真的和Dalvik虚拟机一样,实现了一套完全兼容Java虚拟机的接口
图1 Java虚拟机、Dalvik虚拟机和ART运行时的关系
从图1可以知道,Dalvik虚拟机和ART虚拟机都实现了三个用来抽象Java虚拟机的接口:
- JNI_GetDefaultJavaVMInitArgs -- 获取虚拟机的默认初始化参数
- JNI_CreateJavaVM -- 在进程中创建虚拟机实例
- JNI_GetCreatedJavaVMs -- 获取进程中创建的虚拟机实例
在Android系统中,Davik虚拟机实现在libdvm.so中,ART虚拟机实现在libart.so中。也就是说,libdvm.so和libart.so导出了JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM和JNI_GetCreatedJavaVMs这三个接口,供外界调用。
此外,Android系统还提供了一个系统属性persist.sys.dalvik.vm.lib,它的值要么等于libdvm.so,要么等于libart.so。当等于libdvm.so时,就表示当前用的是Dalvik虚拟机,而当等于libart.so时,就表示当前用的是ART虚拟机。
以上描述的Dalvik虚拟机和ART虚拟机的共同之处,当然它们之间最显著还是不同之处。不同的地方就在于,Dalvik虚拟机执行的是dex字节码,ART虚拟机执行的是本地机器码。这意味着Dalvik虚拟机包含有一个解释器,用来执行dex字节码,具体可以参考Dalvik虚拟机简要介绍和学习计划这个系列的文章。当然,Android从2.2开始,也包含有JIT(Just-In-Time),用来在运行时动态地将执行频率很高的dex字节码翻译成本地机器码,然后再执行。通过JIT,就可以有效地提高Dalvik虚拟机的执行效率。但是,将dex字节码翻译成本地机器码是发生在应用程序的运行过程中的,并且应用程序每一次重新运行的时候,都要做重做这个翻译工作的。因此,即使用采用了JIT,Dalvik虚拟机的总体性能还是不能与直接执行本地机器码的ART虚拟机相比。
那么,ART虚拟机执行的本地机器码是从哪里来的呢?Android的运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者要将重新将自己的应用直接编译成目标机器码。也就是说,开发者开发出的应用程序经过编译和打包之后,仍然是一个包含dex字节码的APK文件。既然应用程序包含的仍然是dex字节码,而ART虚拟机需要的是本地机器码,这就必然要有一个翻译的过程。这个翻译的过程当然不能发生应用程序运行的时候,否则的话就和Dalvik虚拟机的JIT一样了。在计算机的世界里,与JIT相对的是AOT。AOT进Ahead-Of-Time的简称,它发生在程序运行之前。我们用静态语言(例如C/C++)来开发应用程序的时候,编译器直接就把它们翻译成目标机器码。这种静态语言的编译方式也是AOT的一种。但是前面我们提到,ART虚拟机并不要求开发者将自己的应用直接编译成目标机器码。这样,将应用的dex字节码翻译成本地机器码的最恰当AOT时机就发生在应用安装的时候。
我们知道,没有ART虚拟机之前,应用在安装的过程,其实也会执行一次“翻译”的过程。只不过这个“翻译”的过程是将dex字节码进行优化,也就是由dex文件生成odex文件。这个过程由安装服务PackageManagerService请求守护进程installd来执行的。从这个角度来说,在应用安装的过程中将dex字节码翻译成本地机器码对原来的应用安装流程基本上就不会产生什么影响。
有了以上的背景知识之后,我们接下来就从两个角度来了解ART虚拟机是如何做到无缝替换Dalvik虚拟机的:
- ART虚拟机的启动过程;
- Dex字节码翻译成本地机器码的过程。
我们知道,Android系统在启动的时候,会创建一个Zygote进程,充当应用程序进程孵化器。Zygote进程在启动的过程中,又会创建一个Dalvik虚拟机。Zygote进程是通过复制自己来创建新的应用程序进程的。这意味着Zygote进程会将自己的Dalvik虚拟机复制给应用程序进程。通过这种方式就可以大大地提高应用程序的启动速度,因为这种方式避免了每一个应用程序进程在启动的时候都要去创建一个Dalvik。事实上,Zygote进程通过自我复制的方式来创建应用程序进程,省去的不仅仅是应用程序进程创建Dalvik虚拟机的时间,还能省去应用程序进程加载各种系统库和系统资源的时间,因为它们在Zygote进程中已经加载过了,并且也会连同Dalvik虚拟机一起复制到应用程序进程中去
Zygote进程中的Dalvik虚拟机是从AndroidRuntime::start这个函数开始创建的。因此,接下来我们就看看这个函数的实现:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{ALOGD(">>>>>> START %s uid %d <<<<<<\n",className != NULL ? className : "(unknown)", getuid());static const String8 startSystemServer("start-system-server");/** 'startSystemServer == true' means runtime is obsolete and not run from* init.rc anymore, so we print out the boot start event here.*/for (size_t i = 0; i < options.size(); ++i) {if (options[i] == startSystemServer) {/* track our progress through the boot sequence */const int LOG_BOOT_PROGRESS_START = 3000;LOG_EVENT_LONG(LOG_BOOT_PROGRESS_START, ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));}}const char* rootDir = getenv("ANDROID_ROOT");if (rootDir == NULL) {rootDir = "/system";if (!hasDir("/system")) {LOG_FATAL("No root directory specified, and /android does not exist.");return;}setenv("ANDROID_ROOT", rootDir, 1);}//const char* kernelHack = getenv("LD_ASSUME_KERNEL");//ALOGD("Found LD_ASSUME_KERNEL='%s'\n", kernelHack);/* start the virtual machine */JniInvocation jni_invocation;jni_invocation.Init(NULL);JNIEnv* env;if (startVm(&mJavaVM, &env, zygote) != 0) {return;}onVmCreated(env);/** Register android functions.*/if (startReg(env) < 0) {ALOGE("Unable to register all android natives\n");return;}/** We want to call main() with a String array with arguments in it.* At present we have two arguments, the class name and an option string.* Create an array to hold them.*/jclass stringClass;jobjectArray strArray;jstring classNameStr;stringClass = env->FindClass("java/lang/String");assert(stringClass != NULL);strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);assert(strArray != NULL);classNameStr = env->NewStringUTF(className);assert(classNameStr != NULL);env->SetObjectArrayElement(strArray, 0, classNameStr);for (size_t i = 0; i < options.size(); ++i) {jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());assert(optionsStr != NULL);env->SetObjectArrayElement(strArray, i + 1, optionsStr);}/** Start VM. This thread becomes the main thread of the VM, and will* not return until the VM exits.*/char* slashClassName = toSlashClassName(className != NULL ? className : "");jclass startClass = env->FindClass(slashClassName);
这个函数定义在文件/Volumes/aosp/android-8.1.0_r52/frameworks/base/core/jni/AndroidRuntime.cpp中。
AndroidRuntime类的成员函数start最主要是做了以下三件事情:
- 创建一个JniInvocation实例,并且调用它的成员函数init来初始化JNI环境;
- 调用AndroidRuntime类的成员函数startVm来创建一个虚拟机及其对应的JNI接口,即创建一个JavaVM接口和一个JNIEnv接口;
- 有了上述的JavaVM接口和JNIEnv接口之后,就可以在Zygote进程中加载指定的class了。
其中,第1件事情和第2件事情又是最关键的。因此,接下来我们继续分析它们所对应的函数的实现。
JniInvocation类的成员函数init的实现如下所示
class JniInvocation {public:JniInvocation();~JniInvocation();// Initialize JNI invocation API. library should specifiy a valid// shared library for opening via dlopen providing a JNI invocation// implementation, or null to allow defaulting via// persist.sys.dalvik.vm.lib.bool Init(const char* library);// Exposes which library is actually loaded from the given name. The// buffer of size PROPERTY_VALUE_MAX will be used to load the system// property for the default library, if necessary. If no buffer is// provided, the fallback value will be used.static const char* GetLibrary(const char* library, char* buffer);private:bool FindSymbol(void** pointer, const char* symbol);static JniInvocation& GetJniInvocation();jint JNI_GetDefaultJavaVMInitArgs(void* vmargs);jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args);jint JNI_GetCreatedJavaVMs(JavaVM** vms, jsize size, jsize* vm_count);static JniInvocation* jni_invocation_;void* handle_;jint (*JNI_GetDefaultJavaVMInitArgs_)(void*);jint (*JNI_CreateJavaVM_)(JavaVM**, JNIEnv**, void*);jint (*JNI_GetCreatedJavaVMs_)(JavaVM**, jsize, jsize*);friend jint JNI_GetDefaultJavaVMInitArgs(void* vm_args);friend jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args);friend jint JNI_GetCreatedJavaVMs(JavaVM** vms, jsize size, jsize* vm_count);
};
JniInvocation类的成员函数init所做的事情很简单。它首先是读取系统属性persist.sys.dalvik.vm.lib的值
无论加载的是哪一个so,都要求它导出JNI_GetDefaultJavaVMInitArgs、JNI_CreateJavaVM和JNI_GetCreatedJavaVMs这三个接口,并且分别保存在JniInvocation类的三个成员变量JNI_GetDefaultJavaVMInitArgs_、JNI_CreateJavaVM_和JNI_GetCreatedJavaVMs_中。这三个接口也就是前面我们提到的用来抽象Java虚拟机的三个接口。
从这里就可以看出,JniInvocation类的成员函数init实际上就是根据系统属性persist.sys.dalvik.vm.lib来初始化Dalvik虚拟机或者ART虚拟机环境。
这个函数定义在文件libnativehelper/JniInvocation.cpp中。
JniInvocation类的成员变量JNI_CreateJavaVM_指向的就是前面所加载的libdvm.so或者libart.so所导出的函数JNI_CreateJavaVM,因此,JniInvocation类的成员函数JNI_CreateJavaVM返回的JavaVM接口指向的要么是Dalvik虚拟机,要么是ART虚拟机。
通过上面的分析,我们就很容易知道,Android系统通过将ART运行时抽象成一个Java虚拟机,以及通过系统属性persist.sys.dalvik.vm.lib和一个适配层JniInvocation,就可以无缝地将Dalvik虚拟机替换为ART运行时。这个替换过程设计非常巧妙,因为涉及到的代码修改是非常少的。
二,dex文件优化
以上就是ART虚拟机的启动过程,接下来我们再分析应用程序在安装过程中将dex字节码翻译为本地机器码的过程。
Android系统通过PackageManagerService来安装APK,在安装的过程,PackageManagerService会通过另外一个类Installer的成员函数dexopt来对APK里面的dex字节码进行优化:
public void dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet,int dexoptNeeded, @Nullable String outputPath, int dexFlags,String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries,@Nullable String seInfo, boolean downgrade)throws InstallerException {assertValidInstructionSet(instructionSet);if (!checkBeforeRemote()) return;try {mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath,dexFlags, compilerFilter, volumeUuid, sharedLibraries, seInfo, downgrade);} catch (Exception e) {throw InstallerException.from(e);}}
Installer通过socket向守护进程installd发送一个dexopt请求,这个请求是由installd里面的函数dexopt来处理的
frameworks/native/cmds/installd/InstalldNativeService.cpp
binder::Status InstalldNativeService::dexopt(const std::string& apkPath, int32_t uid,const std::unique_ptr<std::string>& packageName, const std::string& instructionSet,int32_t dexoptNeeded, const std::unique_ptr<std::string>& outputPath, int32_t dexFlags,const std::string& compilerFilter, const std::unique_ptr<std::string>& uuid,const std::unique_ptr<std::string>& classLoaderContext,const std::unique_ptr<std::string>& seInfo, bool downgrade) {ENFORCE_UID(AID_SYSTEM);CHECK_ARGUMENT_UUID(uuid);if (packageName && *packageName != "*") {CHECK_ARGUMENT_PACKAGE_NAME(*packageName);}std::lock_guard<std::recursive_mutex> lock(mLock);const char* apk_path = apkPath.c_str();const char* pkgname = packageName ? packageName->c_str() : "*";const char* instruction_set = instructionSet.c_str();const char* oat_dir = outputPath ? outputPath->c_str() : nullptr;const char* compiler_filter = compilerFilter.c_str();const char* volume_uuid = uuid ? uuid->c_str() : nullptr;const char* class_loader_context = classLoaderContext ? classLoaderContext->c_str() : nullptr;const char* se_info = seInfo ? seInfo->c_str() : nullptr;int res = android::installd::dexopt(apk_path, uid, pkgname, instruction_set, dexoptNeeded,oat_dir, dexFlags, compiler_filter, volume_uuid, class_loader_context, se_info,downgrade);return res ? error(res, "Failed to dexopt") : ok();
}
/Volumes/aosp/android-8.1.0_r52/frameworks/native/cmds/installd/dexopt.cpp
int dexopt(const char* dex_path, uid_t uid, const char* pkgname, const char* instruction_set,int dexopt_needed, const char* oat_dir, int dexopt_flags, const char* compiler_filter,const char* volume_uuid, const char* class_loader_context, const char* se_info,bool downgrade) {CHECK(pkgname != nullptr);CHECK(pkgname[0] != 0);if ((dexopt_flags & ~DEXOPT_MASK) != 0) {LOG_FATAL("dexopt flags contains unknown fields\n");}if (!validate_dex_path_size(dex_path)) {return -1;}if (class_loader_context != nullptr && strlen(class_loader_context) > PKG_PATH_MAX) {LOG(ERROR) << "Class loader context exceeds the allowed size: " << class_loader_context;return -1;}bool is_public = (dexopt_flags & DEXOPT_PUBLIC) != 0;bool debuggable = (dexopt_flags & DEXOPT_DEBUGGABLE) != 0;bool boot_complete = (dexopt_flags & DEXOPT_BOOTCOMPLETE) != 0;bool profile_guided = (dexopt_flags & DEXOPT_PROFILE_GUIDED) != 0;bool is_secondary_dex = (dexopt_flags & DEXOPT_SECONDARY_DEX) != 0;// Check if we're dealing with a secondary dex file and if we need to compile it.std::string oat_dir_str;std::string dex_real_path;if (is_secondary_dex) {if (process_secondary_dex_dexopt(dex_path, pkgname, dexopt_flags, volume_uuid, uid,instruction_set, compiler_filter, &is_public, &dexopt_needed, &oat_dir_str,&dex_real_path,downgrade)) {oat_dir = oat_dir_str.c_str();dex_path = dex_real_path.c_str();if (dexopt_needed == NO_DEXOPT_NEEDED) {return 0; // Nothing to do, report success.}} else {return -1; // We had an error, logged in the process method.}} else {// Currently these flags are only use for secondary dex files.// Verify that they are not set for primary apks.CHECK((dexopt_flags & DEXOPT_STORAGE_CE) == 0);CHECK((dexopt_flags & DEXOPT_STORAGE_DE) == 0);}// Open the input file.unique_fd input_fd(open(dex_path, O_RDONLY, 0));if (input_fd.get() < 0) {ALOGE("installd cannot open '%s' for input during dexopt\n", dex_path);return -1;}// Create the output OAT file.char out_oat_path[PKG_PATH_MAX];Dex2oatFileWrapper out_oat_fd = open_oat_out_file(dex_path, oat_dir, is_public, uid,instruction_set, is_secondary_dex, out_oat_path);if (out_oat_fd.get() < 0) {return -1;}// Open vdex files.Dex2oatFileWrapper in_vdex_fd;Dex2oatFileWrapper out_vdex_fd;if (!open_vdex_files(dex_path, out_oat_path, dexopt_needed, instruction_set, is_public, uid,is_secondary_dex, profile_guided, &in_vdex_fd, &out_vdex_fd)) {return -1;}// Ensure that the oat dir and the compiler artifacts of secondary dex files have the correct// selinux context (we generate them on the fly during the dexopt invocation and they don't// fully inherit their parent context).// Note that for primary apk the oat files are created before, in a separate installd// call which also does the restorecon. TODO(calin): unify the paths.if (is_secondary_dex) {if (selinux_android_restorecon_pkgdir(oat_dir, se_info, uid,SELINUX_ANDROID_RESTORECON_RECURSE)) {LOG(ERROR) << "Failed to restorecon " << oat_dir;return -1;}}// Create a swap file if necessary.unique_fd swap_fd = maybe_open_dexopt_swap_file(out_oat_path);// Create the app image file if needed.Dex2oatFileWrapper image_fd =maybe_open_app_image(out_oat_path, profile_guided, is_public, uid, is_secondary_dex);// Open the reference profile if needed.Dex2oatFileWrapper reference_profile_fd = maybe_open_reference_profile(pkgname, dex_path, profile_guided, is_public, uid, is_secondary_dex);ALOGV("DexInv: --- BEGIN '%s' ---\n", dex_path);pid_t pid = fork();if (pid == 0) {/* child -- drop privileges before continuing */drop_capabilities(uid);SetDex2OatScheduling(boot_complete);if (flock(out_oat_fd.get(), LOCK_EX | LOCK_NB) != 0) {ALOGE("flock(%s) failed: %s\n", out_oat_path, strerror(errno));_exit(67);}run_dex2oat(input_fd.get(),out_oat_fd.get(),in_vdex_fd.get(),out_vdex_fd.get(),image_fd.get(),dex_path,out_oat_path,swap_fd.get(),instruction_set,compiler_filter,debuggable,boot_complete,reference_profile_fd.get(),class_loader_context);_exit(68); /* only get here on exec failure */} else {int res = wait_child(pid);if (res == 0) {ALOGV("DexInv: --- END '%s' (success) ---\n", dex_path);} else {ALOGE("DexInv: --- END '%s' --- status=0x%04x, process failed\n", dex_path, res);return res;}}update_out_oat_access_times(dex_path, out_oat_path);// We've been successful, don't delete output.out_oat_fd.SetCleanup(false);out_vdex_fd.SetCleanup(false);image_fd.SetCleanup(false);reference_profile_fd.SetCleanup(false);return 0;
}
函数dexopt首先是读取系统属性persist.sys.dalvik.vm.lib的值,接着在/data/dalvik-cache目录中创建一个odex文件。这个odex文件就是作为dex文件优化后的输出文件。再接下来,函数dexopt通过fork来创建一个子进程,那么该子进程就会调用函数run_dexopt来将dex文件优化成odex文件。另一方面
这两个函数定义在文件frameworks/native/cmds/installd/commands.c中。
这从里就可以看出,函数run_dexopt通过调用/system/bin/dexopt来对dex字节码进行优化,而函数run_dex2oat通过调用/system/bin/dex2oat来将dex字节码翻译成本地机器码。注意,无论是对dex字节码进行优化,还是将dex字节码翻译成本地机器码,最终得到的结果都是保存在相同名称的一个odex文件里面的,但是前者对应的是一个dey文件(表示这是一个优化过的dex),后者对应的是一个oat文件(实际上是一个自定义的elf文件,里面包含的都是本地机器指令)。通过这种方式,原来任何通过绝对路径引用了该odex文件的代码就都不需要修改了。
通过上面的分析,我们就很容易知道,只需要将dex文件的优化过程替换成dex文件翻译成本地机器码的过程,就可以轻松地在应用安装过程,无缝地将Dalvik虚拟机替换成ART运行时。
最后,还有一个地方需要注意的是,应用程序的安装发生在两个时机,第一个时机是系统启动的时候,第二个时机系统启动完成后用户自行安装的时候。在第一个时机中,系统除了会对/system/app和/data/app目录下的所有APK进行dex字节码到本地机器码的翻译之外,还会对/system/framework目录下的APK或者JAR文件,以及这些APK所引用的外部JAR,进行dex字节码到本地机器码的翻译。这样就可以保证除了应用之外,系统中使用Java来开发的系统服务,也会统一地从dex字节码翻译成本地机器码。也就是说,将Android系统中的Dalvik虚拟机替换成ART运行时之后,系统中的代码都是由ART运行时来执行的了,这时候就不会对Dalvik虚拟机产生任何的依赖。
至此,我们就分析完成ART运行时无缝替换Dalvik虚拟机的过程了
相关文章:
Android ART运行时无缝替换Dalvik虚拟机的过程分析
目录 一,概述 二,dex文件优化 一,概述 Android 4.4发布了一个ART运行时,准备用来替换掉之前一直使用的Dalvik虚拟机,希望籍此解决饱受诟病的性能问题。老罗不打算分析ART的实现原理,只是很有兴趣知道ART是如何无缝替换掉原来的Dalvik虚拟机…...
node.js为什么产生?
从官网得知介绍如下 https://nodejs.org/zh-cn/learn/getting-started/introduction-to-nodejs Node.js是一个开源和跨平台的JavaScript运行时环境。 Node.js在浏览器之外运行V8 JavaScript引擎,这是Google Chrome的核心。这使得Node.js具有很高的性能。 Node.js应…...
智能工厂边缘计算:从数据采集到实时决策
智能工厂边缘计算:从数据采集到实时决策 引言 在智能制造场景中,传统云计算架构面临三大核心挑战:平均200ms的网络延迟无法满足实时控制需求,90%的工业数据未被有效利用,以及每月高达15TB的数据传输成本。边缘计算技术通过将计算能力下沉到数据源头,正在构建"端-边…...
个人健康中枢的多元化AI网络革新与精准健康路径探析
引言 随着数字化转型的深入推进,个人健康中枢作为集成化健康管理系统,正在从传统的单一功能向多元化的AI驱动方向快速发展。在这一背景下,新兴网络硬件技术,特别是DPU(数据处理单元)和全光网络的出现,为个人健康中枢的革新提供了前所未有的机遇。本研究将深入探讨这些技…...
前端面试宝典---性能优化
一、加载优化 1. 第三方模块放在CDN 例如 leaflet通过cdn引入,这样就不会占用打包体积了 2. prefetch 预加载 例如,之后马上有个场景需要一个图片,我们就可以通过link 的 prefetch 对资源进行预先加载 再例如,我们公司是无网络开…...
【Springboot进阶】springboot+mybatis+jsqlparser实现数据权限控制
文章目录 SpringBoot JSqlParser MyBatis 数据权限实现方案一、环境准备1. 添加依赖 二、用户上下文管理1. 用户上下文持有类 三、数据权限拦截器实现1. MyBatis拦截器核心类 四、Spring Security集成1. 用户信息注入 五、配置项示例application.yml 六、使用示例1. 业务查询…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】2.3 窗口函数与高级聚合(ROW_NUMBER()/RANK()/SUM() OVER())
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 PostgreSQL窗口函数与高级聚合:从排序到动态分析的全场景应用1. 窗口函数核心概念解析1.1 窗口函数语法结构1.2 核心组成要素2. 排名窗口函数深度解析2.1 ROW_NUMBER():唯一顺序排名示例演示2.2 `RANK…...
python全自动爬取m3u8网页视频(各类网站都通用)
当前人工智能,大语言模型的火热,使得python这门编程语言的使用越来越广泛。最近也开始学习了python,发现它在自动化方面的确有得天独厚的优势。python的简单易用,丰富的开源库,完善的生态,使得它有可能成为…...
C++负载均衡远程调用学习之上报功能与存储线程池
目录 1. Lars-reportV0.1 report模块介绍 2.Lars-reporterV0.1 reporter项目目录构建 3.Lars-ReporterV0.1 数据表和proto协议环境搭建 4.Lars-ReporterV0.1上报请求业务处理 5.Lars-ReporterV0.1上报请求模块的测试 6.Lars-ReporterV0.2开辟存储线程池-网络存储分离 1. L…...
今天python练习题
目录 一、每日一言 二、练习题 三、效果展示 四、下次题目 五、总结 一、每日一言 不要害怕失败,失败可能成为我们前进的动力! 二、练习题 有列表lst [[1,2,3],[4,5,6],[7,8,9]],取出其中的元素1/5/9组成新的列表 # 有列表lst [[1,2,3],[4,5,6],[…...
【leetcode100】最长递增子序列
1、题目描述 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 …...
R绘图|3分钟复现瑞士“苏黎世大学”Nature全球地图——基于R包ggplot2+sf等
一、引言 本文我们复现苏黎世大学团队Franois Keck等在Nature最新文章“The global human impact on biodiversity”中的全球地图。 之前的图纸是在平面坐标系里面进行绘制,本次我们在罗宾逊投影中进行绘制。整体代码逻辑非常简单,就是采样点坐标系的转换…...
百度系列产品学习
1.react-bmapgl封装逻辑 Map 分析react-bmapgl库中Map组件的封装流程,并以mermaid图展示。首先分析Map组件的核心实现,包括生命周期方法和子组件渲染逻辑。然后研究WrapperHOC和Component基类的封装模式,理解事件绑定和属性处理的通用逻辑。…...
高等数学第三章---微分中值定理与导数的应用(3.4~3.5)
3.4 函数的单调性与曲线的凹凸性 一、函数的单调性 1. 函数单调性定义回顾 设函数 f ( x ) f(x) f(x) 的定义域为 D D D,区间 I ⊆ D I \subseteq D I⊆D。 如果对任意 x 1 , x 2 ∈ I x_1, x_2 \in I x1,x2∈I,当 x 1 < x 2 x_1 < x…...
idea结合CopilotChat进行样式调整实践
一、前言: 本文主要分享在前端开发中借助AI能力调整样式,提高开发效率 对应视频【idea结合CopilotChat进行样式调整实践-哔哩哔哩】 二、实践: 2-1、现状确认: 表格上方新增了button、swtich、select组件,需要调整…...
668SJBH报刊发行系统
1 前言 随着我国信息产业的迅猛发展,手工管理方式已不适应社务管理的要求,报社的日常管理正面临着信息化的挑战,采用计算机管理以提高服务质量和管理水平势在必行。发行管理是社务管理的一个重要组成部分,是报社和客户联系的纽带…...
格式化字符串漏洞
原理 在c中,printf函数在打印输出变量时通常不是直接输出,而是用一个占位符如%s printf("the number is %d\n",a);//通常用 printf(a);//而不是直接输出 虽然直接输出也没有太大的问题,但如果用格式化输出,没有给后面…...
如何查看电脑IP地址和归属地:全面指南
在数字化时代,了解自己电脑的IP地址和归属地信息变得越来越重要。无论是进行网络故障排查、远程办公设置,还是出于网络安全考虑,掌握这些基本信息都很有必要。本文将详细介绍如何查看电脑的公网IP、内网IP以及归属地信息,并提供常…...
深入解析 MQTT 协议:物联网通信的基石
在当今物联网蓬勃发展的时代,设备之间高效、可靠的通信变得至关重要。MQTT(Message Queuing Telemetry Transport)协议,作为一种轻量级的消息传输协议,正逐渐成为物联网通信的基石,广泛应用于各种场景中。 …...
48变现干货:分销裂变方式提高销量
产品运营活动中,我们可以根据对产品属性和特性,进行选择特定的方法,分销便是一种低成本各方获利的行为之一,但并不一定100%适用所有产品。 分销及裂变的概念 “分销”是指通过用户、达人、KOL等非官方渠道,参与产品的推广与销售,并获得相应收益的机制。它是一种以奖励为…...
AI入门:Prompt提示词写法
提示词(Prompt)是人与AI沟通的桥梁,它不是冰冷的代码指令,而是一场充满智慧与温度的对话。掌握精妙的提示词撰写技巧,能让AI更精准地理解需求,高效输出理想结果。其核心就在于——将AI视作身边真实的朋友、…...
MySQL复合查询全解析:从基础到多表关联与高级技巧
前言: 本文主要讲解了在MySQL中的复合查询,下面是关于本文章所需要数据的建表语句 创建表的语句: DROP database IF EXISTS scott; CREATE database IF NOT EXISTS scott DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;USE scott;D…...
移动 Trae 目录到 E 盘 - 解决 C 盘空间不足问题
移动 Trae 目录到 E 盘 - 解决 C 盘空间不足问题 1️⃣ 准备工作2️⃣ 移动原始文件夹3️⃣ 创建符号链接4️⃣ 清理原始文件夹5️⃣ 验证操作📝 注意事项🔄 常见问题排查1️⃣ 准备工作 关闭 Trae 程序:确保所有 Trae 相关进程已完全退出(包括后台服务)。创建目标文件夹…...
【AI论文】COMPACT:从原子级到复杂级的组合式视觉能力调优
摘要:多模态大语言模型(MLLM)擅长简单的视觉语言任务,但在面对需要多种能力的复杂任务时却很吃力,例如同时识别物体、计算数量和理解它们的空间关系。 这可能部分是由于视觉指令调整(VIT)这一ML…...
【leetcode】队列 + 宽搜,树形结构层序遍历的基础与变化
前言 🌟🌟本期讲解关于力扣的几篇题解的详细介绍~~~ 🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客 🔥 你的点赞就是小编不断更新的最大动力 🎆那么废话不…...
Spring AI聊天模型API:轻松构建智能聊天交互
Spring AI聊天模型API:轻松构建智能聊天交互 前言 在当今数字化时代,智能聊天功能已成为众多应用程序提升用户体验、增强交互性的关键要素。Spring AI的聊天模型API为开发者提供了一条便捷通道,能够将强大的AI驱动的聊天完成功能无缝集成到…...
力扣-链表-2 两数相加
思路 两个指针同时遍历,维护一个进位值,同时还要维护第一个链表的前序,如果第二个链表比第一个长的时候,利用这个前序指针把第二个链表多余的内容,添加到第一个链表的末尾 代码 class Solution {public ListNode ad…...
leetcode 59. 螺旋矩阵 II
题目描述 代码: class Solution { public:vector<vector<int>> generateMatrix(int n) {vector<vector<int>> res(n,vector<int>(n,0));int num 1;int len n;int start 0;while(len > 0){int row start;int column start;if…...
【操作系统】深入理解内存管理:从虚拟内存到OOM Killer
引言 在现代计算机系统中,内存管理是操作系统最核心的功能之一。本文将围绕内存管理的几个关键概念展开讨论,包括虚拟内存机制、内存分配原理、OOM Killer的工作机制以及不同系统架构下的内存限制。 虚拟内存:突破物理限制的关键技术 虚拟…...
《政治最后的日子》章节
政治与中世纪教会的类比性衰落 作者提出现代民族国家正重复中世纪教会的衰落轨迹: 两者均曾作为社会组织核心存在约5个世纪 晚期都成为生产力阻碍(中世纪教会税收负担/现代国家官僚低效) 末期均出现管理者普遍腐败与公众蔑视(…...
Rust Trait 学习
概述 特征(trait)是rust中的概念,类似于其他语言中的接口(interface)。特征定义了一个可以被共享的行为,只要实现了特征,你就能使用该行为。 如果不同的类型具有相同的行为,那么我们…...
基于开源链动2+1模式AI智能名片S2B2C商城小程序的爆品力构建研究
摘要:在兴趣电商生态中,爆品力已成为品牌实现指数级增长的核心竞争力。本文以开源链动21模式AI智能名片S2B2C商城小程序为技术载体,结合抖音平台的内容传播特性,提出“需求挖掘-技术赋能-内容转化”三位一体的爆品力构建模型。通过…...
【SimSession 】2:PacedReceiver:支持与 PacedVideoSender 本地联调
单独的基于libuv的发送能力,如何进一步在SimSession内集成使用?打算进行本地模拟俩线程,发送和接收,进行测试: 单独的发送测试 【SimSession】1:将视频发送逻辑与 libuv 事件循环集成是一个典型的并发设计问题 分析后,D:\XTRANS\thunderbolt\ayame\zhb-bifrost\player-…...
5 什么情况下需要微调
这个问题其实很重要,因为现代大模型训练出来已经非常强大,可能真的不需要微调。 我们可以通过 RAG 或提示词工程来实现目标。 需要微调的场景与替代方案分析 微调(Fine-tuning)确实不是所有场景都必需的,特别是考虑到现代大型语言模型(LLM…...
Docker 渡渡鸟镜像同步站 使用教程
Docker 渡渡鸟镜像同步站 使用教程 🚀 介绍 Docker.aityp.com(渡渡鸟镜像同步站)是一个专注于为国内开发者提供 Docker 镜像加速和同步服务的平台。它通过同步官方镜像源(如 Docker Hub、GCR、GHCR 等),为…...
位图的实现和拓展
一:位图的介绍 ①:需要位图的场景 给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中? 要判断一个数是否在某一堆数中,我们可能会想到如下方法: A…...
字符串问题c++
题目描述 小 Z 有一个字符串 s,他对这个字符串会进行如下两个操作: 1 p c 把字符串的第 p 个字符改成 c2 l r 把 s 的第 l 个字符到第 r 个字符按顺序输出。 这里『第 i 个字符』的下标计数从 1 开始,例如,a 是字符串 abc 的第…...
Redis事务
Redis中的事务是指提供一种将多个命令打包到一起,一次性按照顺序执行的机制。Redis在执行事务期间,不会接收处理其他操作命令。 Redis事务有以下局限性 无回滚机制:如果某个命令执行失败,不会影响其他命令的执行,因此…...
【PostgreSQL数据分析实战:从数据清洗到可视化全流程】3.3 异常值识别(Z-score法/IQR法/业务规则法)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 PostgreSQL数据分析实战:数据质量分析之异常值识别(Z-score法 / IQR法 / 业务规则法)3.3 异常值识别3.3.1 Z-score法3.3.2 IQR法3.3.3 业…...
MCP底层协议完整通信过程
2025 年是智能体的元年, 也注定是智能体集中爆发的一年! 两个互联领域的重大挑战: 第一、 Agent 与 Tools (工具)的交互 Agent 需要调用外部工具和 API...
C语言 指针(5)
目录 1.冒泡排序 2.二级指针 3.指针数组 4.指针数组模拟二级数组 1.冒泡排序 1.1 基本概念 冒泡排序(Bubble Sort) 是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元 素,如果它们的顺序错误就把它…...
MYSQL-联合查询
经过上节课,我们学会了如何设计表以及了解到各种范式,这节课就请大家和小L一起来学习设计表之后如何使用 1.为什么要使用联合查询 在数据设计时由于范式的要求,数据被拆分到多个表中,那么要查询⼀个条数据的完整信息,…...
一篇撸清 Http,SSE 与 WebSocket
HTTP,SSE 和WebSocket都是网络传输的协议,本篇快速介绍三者的概念和比较。 SSE(Server-Sent Events) 是什么? SSE(Server-Sent Events),服务器发送事件, 是一种基于 HTTP 的轻量级协议,允许服务器主动向客户端(如浏览器)推送实时数据。它设计用于单向通信(服务器到…...
系统架构设计师:设计模式——行为设计模式
一、行为设计模式 行为模式涉及算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的、复杂的控制流。它们将用户的注意力从控制流转移到对象间的联系方式上来。 行为类模式使用继承机制在类间分…...
OpenCV入门指南:从环境搭建到第一个图像处理程序
引言 你是否想让计算机"看懂"世界?OpenCV(Open Source Computer Vision Library)正是打开计算机视觉大门的钥匙。本文将带你从零开始搭建开发环境,理解图像处理核心概念,并完成第一个OpenCV程序。无论你是想…...
基于STM32的温湿度光照强度仿真设计(Proteus仿真+程序设计+设计报告+讲解视频)
这里写目录标题 **1.****主要功能****2.仿真设计****3.程序设计****4.设计报告****5.下载链接** 基于STM32的温湿度光照强度仿真设计(Proteus仿真程序设计设计报告讲解视频) 仿真图Proteus 8.9 程序编译器:keil 5 编程语言:C语言 设计编号…...
4个纯CSS自定义的简单而优雅的滚动条样式
今天发现 uni-app 项目的滚动条不显示,查了下原来是设置了 ::-webkit-scrollbar {display: none; } 那么怎么用 css 设置滚动条样式呢? 定义滚动条整体样式 ::-webkit-scrollbar 定义滚动条滑块样式 ::-webkit-scrollbar-thumb 定义滚动条轨道样式…...
修复笔记:SkyReels-V2项目中的 from_config 警告
#工作记录 Windows避坑部署SkyworkAI/SkyReels-V2昆仑万维电影生成模型_skyreels-v2本地部署-CSDN博客 一、项目背景 项目名称:SkyReels-V2 项目简介:由昆仑万维开源的全球首个无限时长电影生成模型,支持文本到视频、图像到视频等多种生成方…...
[硬件电路-11]:模拟电路常见元器件 - 什么是阻抗、什么是输入阻抗、什么是输出阻抗?阻抗、输入阻抗与输出阻抗的全面解析
1. 阻抗(Impedance) 定义:阻抗是电路或元件对交流信号(AC)流动的阻碍能力,用符号Z表示,单位为欧姆(Ω)。它综合了电阻(R)、电感(L&am…...
MCP协议与Dify集成教程
一、MCP协议概述 MCP(Model Control Protocol)是一种新兴的开放协议,为大型语言模型(LLM)与外部应用之间构建了双向通信通道。它就像是AI的"USB-C"接口,帮助模型发现、理解并安全调用各种外部工…...