【开源框架】Glide的图片加载流程
本篇文章从Glide 4.11源码入手,简单的分析整个图片请求的流程,本着 ”只见树林,不见树木“ 的原则,宏观请求流程,不细究实现细节(细节留坑埋点,之后慢慢写)
引入依赖
以下的所有分析都是基于此版本的Glide分析
//引入第三方库glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
分析
Glide的使用就是短短的一行代码
Glide.with(this).load("xxx").into(imageView);//拆分成三步
RequestManager with = Glide.with(this);RequestBuilder<Drawable> load = with.load("");load.into(iv);
- 首先通过构造Glide的单例对象
- 调用with给每一个RequestManager绑定一个空白的Fragment来管理图片加载的生命周期
- 构建Request对象(真正的实现类SingleRequest)
- 请求之前先检测缓存
- 先检测活动缓存
- 在检测内存缓存
- 没有缓存就构建一个新的异步任务
- 检测有没有本地磁盘缓存
- 没有磁盘缓存,就通过网络请求,返回输入流InputStream
- 解析输入流InputStream进行采样压缩,最终拿到Bitmap对象
- 对Bitmap进行转换成Drawble
- 构建磁盘缓存DiskCache
- 构建内存缓存
- 最终回到ImageViewTarget显示图片
with(如何实现生命周期的管控)
调用with方法
public static RequestManager with(@NonNull FragmentActivity activity) {return getRetriever(activity).get(activity);
}
会来到RequestManagerRetriever
类的get方法中,在这里区分主线程还是在子线程中使用with。
如果在子线程中,绑定的是app的生命周期,在主线程中会将图片的加载与当前activity的生命周期绑定。
这也是为什么不能在子线程中使用Glide的原因,生命周期的管理会失效。
public RequestManager get(@NonNull FragmentActivity activity) {if (Util.isOnBackgroundThread()) {return get(activity.getApplicationContext());} else {assertNotDestroyed(activity);FragmentManager fm = activity.getSupportFragmentManager();return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));}}
最终会返回一个RequestManager
对象。
load
调用load最终会返回一个RequestBuilder
对象
load负责做什么?
into
into创建请求
RequestBuilder
在RequestBuilder
中的into方法,首先会创建一个请求,它是一个接口,真正的实现类是SingleRequest
。
private <Y extends Target<TranscodeType>> Y into(@NonNull Y target,@Nullable RequestListener<TranscodeType> targetListener,BaseRequestOptions<?> options,Executor callbackExecutor) {······//1.创建一个请求//这里的Request是一个接口,实际构造的对象是SingleRequest实现类Request request = buildRequest(target, targetListener, options, callbackExecutor);······//2.requestManager.clear(target);target.setRequest(request);//3.继续跟踪tarck()requestManager.track(target, request);return target;}
RequestTracker
一个用于跟踪、取消和重新启动正在进行的、已完成的和失败的请求的类
跟踪track方法最终调用的是runRequest方法。
其中有两个列表:
- requests:运行中的队列
- pendingRequests:等待中的队列
public void runRequest(@NonNull Request request) {//1.将请求加入到运行队列中requests.add(request);//2.请求没有暂停就调用SingleRequest的begin方法if (!isPaused) {request.begin();//3.请求暂停了就加入到等待运行的队列中} else {request.clear();pendingRequests.add(request);}}
SingleRequest
在SingleRequest
类中begin函数是添加了synchronized
,保证在多线程下的线程安全
onSizeReady
方法,最终返回调用的engine.load()
@Overridepublic void begin() {synchronized (requestLock) {······status = Status.WAITING_FOR_SIZE;if (Util.isValidDimensions(overrideWidth, overrideHeight)) {//1.继续深入此方法onSizeReady(overrideWidth, overrideHeight);} else {target.getSize(this);}······}}
Engine(活动和内存缓存)
这时已经来到了Engine类的load方法中
public <R> LoadStatus load(GlideContext glideContext,Object model,Key signature,int width,int height,······) {long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;//1.得到一个唯一的key,用于唯一标识图片,用于缓存读取EngineKey key =keyFactory.buildKey(model,signature,width,height,······);//2.将key传入,从缓存中获取图片EngineResource<?> memoryResource;synchronized (this) {memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);if (memoryResource == null) {//4.如果缓存中没有则加载return waitForExistingOrStartNewJob(glideContext,model,signature,width,height,······key,startTime);}}//3.存在图片缓存,则直接将它回调出去cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);return null;}
详看loadFromMemory
方法
在这里存在活动缓存和内存缓存两级缓存,这两级缓存都是运行时缓存,当APP进程被杀,这这两级缓存是不再存在的。
活动缓存是:直接面向用户正在被展示的图片,是一个弱引用对象,当弱引用对象被回收之后,会把它持有的图片资源放到内存缓存中
@Nullableprivate EngineResource<?> loadFromMemory(EngineKey key, boolean isMemoryCacheable, long startTime) {······//1.从活动缓存中获取EngineResource<?> active = loadFromActiveResources(key);if (active != null) {return active;}//2.活动缓存没有,才从内存缓存中获取EngineResource<?> cached = loadFromCache(key);if (cached != null) {return cached;}//3.都没有则直接退出return null;}
回过头再看waitForExistingOrStartNewJob
方法,这个方法是没有检测到缓存时才会被调用。
首先会再次检测磁盘缓存中是否存在,不存在则创建异步任务
private <R> LoadStatus waitForExistingOrStartNewJob(GlideContext glideContext,Object model,Key signature,int width,int height,······EngineKey key,long startTime) {//1.get磁盘缓存EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);if (current != null) {current.addCallback(cb, callbackExecutor);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Added to existing load", startTime, key);}return new LoadStatus(cb, current);}//2.执行图片各类操作的jobEngineJob<R> engineJob =engineJobFactory.build(key,isMemoryCacheable,useUnlimitedSourceExecutorPool,useAnimationPool,onlyRetrieveFromCache);//3.真正需要执行的任务,最终放入到engineJob中执行DecodeJob<R> decodeJob =decodeJobFactory.build(glideContext,model,key,······);jobs.put(key, engineJob);engineJob.addCallback(cb, callbackExecutor);//4.将decodeJob放入engineJob开始执行engineJob.start(decodeJob);if (VERBOSE_IS_LOGGABLE) {logWithTimeAndKey("Started new load", startTime, key);}return new LoadStatus(cb, engineJob);}
由engineJob
启动decodeJob
的执行。最终通过线程池去执行decodeJob
操作,可见decodeJob
肯定是Runnable
的实现类。
public synchronized void start(DecodeJob<R> decodeJob) {this.decodeJob = decodeJob;GlideExecutor executor =decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();executor.execute(decodeJob);}
DecedeJob
执行的是decodeJob
中的run方法
@Overridepublic void run() {·······try {//1.重点关注此方法runWrapped();} catch (CallbackException e) {throw e;} catch (Throwable t) {·····} finally {}
runWrapped
主要是对不同任务的区分。
具体执行哪一个看此篇文章:https://www.jianshu.com/p/faeeb7bb39a3
private void runWrapped() {switch (runReason) {//1.首次初始化并获取资源case INITIALIZE:stage = getNextStage(Stage.INITIALIZE);currentGenerator = getNextGenerator();runGenerators();break;//2.从磁盘缓存获取不到数据,重新获取case SWITCH_TO_SOURCE_SERVICE:runGenerators();break;//3.获取资源成功,解码数据case DECODE_DATA:decodeFromRetrievedData();break;default:throw new IllegalStateException("Unrecognized run reason: " + runReason);}}
不管是 INITIALIZE
初始化还是 SWITCH_TO_SOURCE_SERVICE
从磁盘缓存获取不到数据进行重试,都是要调用 runGenerators
来获取数据。我们先来看getNextGenerator
,在首次初始化时会调用这个方法。可以看到这里由很多的XXXGenerator
DataFetcherGenerator的三个实现子类:
- ResourceCacheGenerator:从磁盘缓存中获取原始资源处理后的Resource资源
- DataCacheGenerator:从磁盘缓存中获取原始资源
- SourceGenerator:从数据源(例如网络)中获取资源
具体从哪一个生成器中获取资源跟用户的使用配置有关,没有配置时默认是Source
因此上面的currentGenerator
是SourceGenerator
private DataFetcherGenerator getNextGenerator() {switch (stage) {case RESOURCE_CACHE:return new ResourceCacheGenerator(decodeHelper, this);case DATA_CACHE:return new DataCacheGenerator(decodeHelper, this);case SOURCE:return new SourceGenerator(decodeHelper, this);case FINISHED:return null;default:throw new IllegalStateException("Unrecognized stage: " + stage);}}
继续深入会发现在runGenerators方法中会调用currentGenerator
的startNext
方法,很明显是SourceGenenrator的startNext方法。
@Overridepublic boolean startNext() {······loadData = null;boolean started = false;while (!started && hasNextModelLoader()) {//1.注意getLoadDataloadData = helper.getLoadData().get(loadDataListIndex++);if (loadData != null&& (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())|| helper.hasLoadPath(loadData.fetcher.getDataClass()))) {started = true;startNextLoad(loadData);}}return started;}
重点关注getLoadData方法,这个方法中调用了modelLoader.buildLoadData,返回一个LoadData,它是工厂接口ModelLoader中的一个内部类。不必深究这个接口为什么设计,主要知道它是返回的它的实现子类HttpGlideUrlLoader
,这时在初始化Glide是动态注册的
@Overridepublic LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {// GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time// spent parsing urls.GlideUrl url = model;if (modelCache != null) {url = modelCache.get(model, 0, 0);if (url == null) {modelCache.put(model, 0, 0, model);url = model;}}int timeout = options.get(TIMEOUT);return new LoadData<>(url, new HttpUrlFetcher(url, timeout));}
以下截图来自Glide类中
HttpGlideUrlLoader
既然如此自然而然调用的就是HttpGlideUrlLoader
类中的buildLoadData
方法
@Overridepublic LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {······int timeout = options.get(TIMEOUT);return new LoadData<>(url, new HttpUrlFetcher(url, timeout));}
可以看到最终又创建了一个HttpUrlFetcher,往这个类中深入
HttpUrlFetcher
来到HttpUrlFetcher中,可算是柳暗花明,终于看到了网络请求,最终返回的是一个InputStream对象。
而网络请求的实现原理是使用Android中的HttpURLConnection,但是从Android 4.4开始,HttpURLConnection的实现确实是通过调用okhttp完成的,而具体的方法则是通过HttpHandler这个桥梁,以及在OkHttpClient, HttpEngine中增加相应的方法来实现,当然,其实还涉及一些类的增加或删除。所以想研究网络请求的实现不妨再看以下OkHttp这个网络请求框架。
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {······//1.网络请求urlConnection = connectionFactory.build(url);for (Map.Entry<String, String> headerEntry : headers.entrySet()) {urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());}urlConnection.setConnectTimeout(timeout);urlConnection.setReadTimeout(timeout);urlConnection.setUseCaches(false);urlConnection.setDoInput(true);urlConnection.setInstanceFollowRedirects(false);urlConnection.connect();stream = urlConnection.getInputStream();if (isCancelled) {return null;}final int statusCode = urlConnection.getResponseCode();if (isHttpOk(statusCode)) {return getStreamForSuccessfulRequest(urlConnection);} else if (isHttpRedirect(statusCode)) {······}
DecedeJob
拿到了请求的结果,我们并不能直接显示这张图片,还需要进行采样压缩,否则很容易出现大图OOM。
多级回调最终回到了DecedeJob类中。
在runLoadPath方法中,又继续深入到了LoadPath类中执行采样压缩(这里就不再深入分析是怎么采样压缩的了,目的很明显,最终返回的就是一张被压缩优化的Bitmap图片)
private <Data, ResourceType> Resource<R> runLoadPath(Data data, DataSource dataSource, LoadPath<Data, ResourceType, R> path)throws GlideException {Options options = getOptionsWithHardwareConfig(dataSource);DataRewinder<Data> rewinder = glideContext.getRegistry().getRewinder(data);try {// ResourceType in DecodeCallback below is required for compilation to work with gradle.return path.load(rewinder, options, width, height, new DecodeCallback<ResourceType>(dataSource));} finally {rewinder.cleanup();}}
PathLoad
采样压缩的细节不再深入,最终返回的是一张优化处理后的Bitmap位图
最终将图片一系列的回调,回到了Engine类中
Engine
回调到EngineJob中,再回调到Engine中的onEngineJobComplete,顾名思义就是到是异步请求任务已经完成
@Override
public synchronized void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource) {if (resource != null && resource.isMemoryCacheable()) {activeResources.activate(key, resource);}jobs.removeIfCurrent(key, engineJob);
}
调用activate方法将它放入到活动缓存中,注意这是一个弱引用
synchronized void activate(Key key, EngineResource<?> resource) {ResourceWeakReference toPut =new ResourceWeakReference(key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);ResourceWeakReference removed = activeEngineResources.put(key, toPut);if (removed != null) {removed.reset();}}
SingleRequest
将图片保存到活动缓存中后,再由EngineJob回调到SingeRequest中的onResourceReady
,意为资源已经准备好了
@GuardedBy("requestLock")private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {······try {······if (!anyListenerHandledUpdatingTarget) {//1.加载动画Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource);//2.返回图片和动画target.onResourceReady(result, animation);}} finally {isCallingCallbacks = false;}//3.通知加载成功notifyLoadSuccess();}
ImageViewTarget
最终回到起点,设置图片,调用的是ImageViewTarget中的setResourceInternal方法,最终调用其中的抽象方法setResource
@Overridepublic void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {//1.设置图片资源if (transition == null || !transition.transition(resource, this)) {setResourceInternal(resource);} else {maybeUpdateAnimatable(resource);}}
有3个子类实现了这个方法,最终由它们去显示图片
DrawableImageViewTarget
这里的view就是一个ImageView对象,至此一张图片就显示在了屏幕上
@Overrideprotected void setResource(@Nullable Drawable resource) {view.setImageDrawable(resource);}
相关文章:

【开源框架】Glide的图片加载流程
本篇文章从Glide 4.11源码入手,简单的分析整个图片请求的流程,本着 ”只见树林,不见树木“ 的原则,宏观请求流程,不细究实现细节(细节留坑埋点,之后慢慢写) 引入依赖 以下的所有分…...

win10下Mariadb绿色版安装步骤
使用绿色版的mariadb数据库管理软件,免费开源,可以用来替换MySQL。首先从mariadb官网下载绿色版本的压缩包。解压后、配置好即可以使用。 把他解压缩到C:\mariadb\之下。打开powershell: Cd c:\mariadb\bin .\mysql_install_db.exe 这一…...

wiresharak捕获DNS
DNS解析: 过滤项输入dns: dns查询报文 应答报文: 事务id相同,flag里 QR字段1,表示响应,answers rrs变成了2. 并且响应报文多了Answers 再具体一点,得到解析出的ip地址(最底下的add…...

vue源码分析(一)——源码目录说明
文章目录 一、如何下载源码(可忽略)(1)打开地址(2)复制链接(3)git clone 链接 二、源码目录说明1.可以根据你下载的源码通过package.json文件查看vue版本2.源码目录说明 一、如何下载…...

【深度学习】吴恩达课程笔记(二)——浅层神经网络、深层神经网络
笔记为自我总结整理的学习笔记,若有错误欢迎指出哟~ 笔记链接 【深度学习】吴恩达课程笔记(一)——深度学习概论、神经网络基础 吴恩达课程笔记——浅层神经网络、深层神经网络 四、浅层神经网络1.双层神经网络表示2.双层神经网络的前向传播第一层前向传播第二层前…...

UI自动化概念 + Web自动化测试框架介绍
1.UI自动化测试概念:我们先明确什么是UI UI,即(User Interface简称UI用户界面)是系统和用户之间进行交互和信息交换的媒介 UI自动化测试: Web自动化测试和移动自动化测试都属于UI自动化测试,UI自动化测试就是借助自动化工具对程序UI层进行自动化的测试 …...

在 macOS 上的多个 PHP 版本之间切换
文章目录 前言一、前提条件1.引入库需要安装 Xcode 2.安装多个PHP版本2.PHP版本切换 开源替代品 前言 不同项目使用php版本可能不同,需要安装不同版本php 一、前提条件 1.引入库 需要安装 Xcode 命令行工具和Homebrew xcode-select --install检查brew是否已安…...

地址解析协议ARP
地址解析协议(Address Resolution Protocol,ARP),用于根据本网内目的主机或默认网关的IP地址获取其MAC地址。 ARP的基本思想:在每一台主机中设置专用内存区域,称为ARP高速缓存(也称为ARP表&…...

Go学习第十三章——Gin入门与路由
Go web框架——Gin入门与路由 1 Gin框架介绍1.1 基础介绍1.2 安装Gin1.3 快速使用 2 路由2.1 基本路由GET请求POST请求 2.2 路由参数2.3 路由分组基本分组带中间件的分组 2.4 重定向 1 Gin框架介绍 github链接:https://github.com/gin-gonic/gin 中文文档…...

[减脂期食谱] 自制千岛酱
[减脂期食谱] 自制千岛酱 成品如下: 最中间的那个,算比较居中的颜色吧,其实自己家做原版的千岛酱还是比较简单的,它的底就是蛋黄酱(蛋黄油乳化的酱),随后里面的材料比较自由,维基百科是这么介绍的…...

Android 系统架构
目录 Android 系统架构 1. Android 应用层 2. Android应用框架层 2.1 Activity Manager (活动管理器) 2.2 Window Manager (窗口管理器) 2.3 Content Provider (内容提供器) 2.4 View System(视图系统&a…...

【Docker】Python Flask + Redis 练习
一、构建flask镜像 1.准备文件 创建app.py,内容如下 from flask import Flask from redis import Redis app Flask(__name__) redis Redis(hostos.environ.get(REDIS_HOST,127.0.0.1),port6379)app.route(/) def hello():redis.incr(hits)return f"Hello Container W…...

shell_52.Linux测试与其他网络主机的连通性脚本
实战演练 本节将展示一个实用脚本,该脚本在处理用户输入的同时,使用 ping 命令或 ping6 命令来测试与其他网络主机的连通性。 ping 命令或 ping6 命令可以快速测试网络主机是否可用。这个命令很有用,经常作为首选工具。如果只是检查单个主机&…...

OpenCV C++ 图像处理实战 ——《缺陷检测》
OpenCV C++ 图像处理实战 ——《缺陷检测》 一、结果演示二、缺陷检测算法2.1、多元模板图像2.2、训练差异模型三、图像配准3.1 功能源码3.1 功能效果四、多元模板图像4.1 功能源码五、缺陷检测5.1 功能源码六、源码测试图像下载总结一、结果演示...

Python操作MySQL基础使用
Python操作MySQL基础使用 链接数据库并查询数据 import pymysql# 链接数据库 conn pymysql.connect(host10.5.6.250,port3306,userroot,password******** )# 查看MySQL版本信息 print(conn.get_server_info()) # 5.5.27# 获取到游标对象 cursor conn.cursor()# 选择数据库…...

【pytorch】pytorch中的高级索引
这里只介绍pytorch的高级索引,是一些奇怪的切片索引 基本版 a[[0, 2], [1, 2]] 等价 a[0, 1] 和 a[2, 2],相当于索引张量的第一行的第二列和第三行的第三列元素; a[[1, 0, 2], [0]] 等价 a[1, 0] 和 a[0, 0] 和 a[2, 0],相当于索…...

基于图像识别的自动驾驶汽车障碍物检测与避障算法研究
基于图像识别的自动驾驶汽车障碍物检测与避障算法研究是一个涉及计算机视觉、机器学习、人工智能和自动控制等多个领域的复杂问题。以下是对这个问题的研究内容和方向的一些概述。 障碍物检测 障碍物检测是自动驾驶汽车避障算法的核心部分,它需要从车辆的感知数据…...

Spring boot定时任务
目录 前言一、使用 Scheduled 注解二、使用 ScheduledExecutorService三、使用 Spring 的 TaskScheduler四、使用第三方调度框架 前言 在 Spring Boot 中,有多种方法来编写定时任务,以执行周期性或延迟执行的任务。下面是几种常见的方式 一、使用 Sche…...

Glide原理
本文基于Carson整理 1.简介 相比其他几种图片加载框架,Glide性能最好。这得益于其高效的图片缓存策略 其还有多样化的媒体格式加载:如GIF、Video,对于商城首页需展示丰富样式、信息的页面需求来说,也是必不可少的。 2.加载原理…...

wps表格按分隔符拆分单元格
有数据如下;看选中区域,一个单元格中有一个v,空格,然后有三个数值,以空格分开;点击菜单中的数据-分列; 弹出分列向导;选择 分隔符号; 选择分隔符为空格;出现预…...

【SEC 学习】Vim 的基本使用
一、Vim 编辑器安装 yum install -y vim二、Vim 三种模式 命令模式 编辑模式 末行模式 三、三种模式之间的转换 1. 命令模式 -> 编辑模式 快捷键含义i从光标处插入I从光标所在行首插入a从光标后插入A从光标所在行末插入o从光标下一行插入O从光标上一行插入 2. 命令模式 …...

Linux中shell脚本练习
目录 1.猜数字 2.批量创建用户 3.监控网卡Receive Transmit 数据的变化 4.部署Linux 5.系统性能检测脚本 6.分区脚本 7.数据库脚本 1.猜数字 随机数的生成 使用环境变量RANDOM,范围是0~32767 编写guest.sh,实现以下功能࿱…...

AS/400简介
AS400 AS400 简介AS/400操作系统演示 AS400 简介 在 AS400 中,AS代表“应用系统”。它是多用户、多任务和非常安全的系统,因此用于需要同时存储和处理敏感数据的行业。它最适合中级行业,因此用于制药行业、银行、商场、医院管理、制造业、分销…...

FreeRTOS 中断管理介绍和实操
目录 中断定义 中断优先级 相关注意 中断相关函数 1.队列 2.信号量 3.事件标志组 4.任务通知 5.软件定时器 中断管理实操 中断定义 中断是指在程序执行的过程中,突然发生了某种事件,需要立即停止当前正在执行的程序,并转而处理这个…...

性能测试 —— Jmeter 常用三种定时器!
1、同步定时器 位置:HTTP请求->定时器->Synchronizing Timer 当需要进行大量用户的并发测试时,为了让用户能真正的同时执行,添加同步定时器,用户阻塞线程,知道线程数达到预先配置的数值,才开始执行…...

ROS自学笔记十七:Arbotix
ArbotiX 是一个基于 ROS(Robot Operating System)的机器人控制系统,它旨在为小型机器人提供硬件控制和传感器接口,以便于机器人的运动和感知。以下是有关 ROS 中 ArbotiX 的简介和安装步骤: ArbotiX 简介 ArbotiX 主…...

Mac电脑窗口管理Magnet中文 for mac
Magnet是一款Mac窗口管理工具,它可以帮助用户轻松管理打开的窗口,提高多任务处理效率。以下是Magnet的一些主要特点和功能: 分屏模式支持:Magnet支持多种分屏模式,包括左/右/顶部/底部 1/2 分屏、左/中/右 1/3 分屏、…...

Centos7 部署 Stable Diffusion
参考:https://www.jianshu.com/p/ff81bb76158a 遇到的问题: 1、git clone 比较慢 解决办法:设置代理 https://blog.csdn.net/dszgf5717/article/details/130735389 2、pip install 比较慢 解决办法:更换源或设置代理 https:/…...

【Python】一个句子中也许有多个连续空格,过滤掉多余的空格,只留下一个空格
题目要求:一个句子中也许有多个连续空格,过滤掉多余的空格,只留下一个空格 例:(为了方便观看,以 ▢ 代替空格) 输入:123▢▢abc▢▢▢python 输出:123▢abc▢python 参考…...

嵌入式项目电灯
1、原理,电灯有个正负极,当正确接入电源正负极就能点亮(如正极5v,负极0v),单两边同时接入正极,就不会亮(两端都是5v),所以通过控制电平,来实现控制led等的亮暗 cpu通过给…...