Spring扩展点系列-InstantiationAwareBeanPostProcessor

文章目录

    • 简介
    • 测试一
      • 1、配置文件Bean注册
      • 2、单元测试方法
      • 3、测试类
      • 4、输出结果
      • 结论
    • 测试二
      • 1、测试类
      • 2、输出结果
      • 结论
    • 源码解析
      • postProcessProperties
        • CommonAnnotationBeanPostProcessor
        • AnnotationInjectedBeanPostProcessor
    • 总结

简介

spring容器中Bean的生命周期内所有可扩展的点的调用顺序
扩展接口 实现接口
ApplicationContextlnitializer initialize
AbstractApplicationContext refreshe
BeanDefinitionRegistryPostProcessor postProcessBeanDefinitionRegistry
BeanDefinitionRegistryPostProcessor postProcessBeanFactory
BeanFactoryPostProcessor postProcessBeanFactory
instantiationAwareBeanPostProcessor postProcessBeforelnstantiation
SmartlnstantiationAwareBeanPostProcessor determineCandidateConstructors
MergedBeanDefinitionPostProcessor postProcessMergedBeanDefinition
InstantiationAwareBeanPostProcessor postProcessAfterlnstantiation
SmartInstantiationAwareBeanPostProcessor getEarlyBeanReference
BeanFactoryAware postProcessPropertyValues
ApplicationContextAwareProcessor invokeAwarelnterfaces
BeanNameAware setBeanName
InstantiationAwareBeanPostProcessor postProcessBeforelnstantiation
@PostConstruct
InitializingBean afterPropertiesSet
FactoryBean getobject
SmartlnitializingSingleton afterSingletonslnstantiated
CommandLineRunner run
DisposableBeandestroy

从源码中我们可以获知InstantiationAwareBeanPostProcessor接口除了具有父接口中的两个方法以外还自己额外定义了三个方法。所以该接口一共定义了5个方法,这5个方法的作用分别是

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {@Nullabledefault Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {return null;}default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {return true;}@Nullabledefault PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)throws BeansException {return null;}
public interface BeanPostProcessor {@Nullabledefault Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Nullabledefault Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;}
}
方法描述
postProcessBeforeInitialization方法将在Bean实例的afterPropertiesSet方法或者自定义的init方法被调用前调用,此时Bean属性已经被赋值。方法返回原始Bean实例或者包装后的Bean实例,如果返回null,则后续的后置处理方法不再被调用。
postProcessAfterInitialization方法将在Bean实例的afterPropertiesSet方法或者自定义的init方法被调用后调用,此时Bean属性已经被赋值。方法返回原始Bean实例或者包装后的Bean实例,如果返回null,则后续的后置处理方法不再被调用。
postProcessBeforeInstantiation方法作用为:在Bean实例化前调用该方法,返回值可以为代理后的Bean,以此代替Bean默认的实例化过程。返回值不为null时,后续只会调用BeanPostProcessor的 postProcessAfterInitialization方法,而不会调用别的后续后置处理方法(如postProcessAfterInitialization、postProcessBeforeInstantiation等方法);返回值也可以为null,这时候Bean将按默认方式初始化。
postProcessAfterInstantiation方法作用为:当Bean通过构造器或者工厂方法被实例化后,当属性还未被赋值前,该方法会被调用,一般用于自定义属性赋值。方法返回值为布尔类型,返回true时,表示Bean属性需要被赋值;返回false表示跳过Bean属性赋值,并且InstantiationAwareBeanPostProcessor的postProcessProperties方法不会被调用。

Instantiation为实例化的意思,Initialization为初始化的意思。在Spring Bean生命周期中,实例化指的是创建Bean的过程,初始化指的是Bean创建后,对其属性进行赋值(populate bean)、后置处理等操作的过程,所以Instantiation执行时机先于Initialization。

测试一

简单测试一下,看看每个方法的执行顺序

1、配置文件Bean注册

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean class="cn.mycode.chen.myunit.config.springextend.TestAutowired" id="testAutowired" init-method="init"><property name="testName" value="若曼底登陆"></property></bean><!-- 注册InstantiationAwareBeanPostProcessor对象 --><bean class="cn.mycode.chen.myunit.config.springextend.ExtendInstantiationAwareBeanPostProcessor"></bean>
</beans>

2、单元测试方法

@Test
public void test2() {ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");TestAutowired testAutowired = ac.getBean(TestAutowired.class);System.out.println(testAutowired);// 关闭销毁ac.registerShutdownHook();
}

3、测试类

一个业务类TestAutowired

@Slf4j
@Component
public class TestAutowired {private String testName ;public TestAutowired(){log.info("TestAutowired构造方法实例化------run");}public void setTestName(String testName) {log.info("设置属性:"+testName);this.testName = testName;}@PostConstructpublic void init() {log.info("init bean执行 ");this.testName = "若曼底登陆";}public void start() {log.info("TestAutowired init-method run" );}
}

InstantiationAwareBeanPostProcessor的实现测试类

@Slf4j
@Configuration
public class ExtendInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {/*** BeanPostProcessor接口中的方法* 在Bean的自定义初始化方法之前执行* Bean对象已经存在了*/@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// TODO Auto-generated method stublog.info(">>postProcessBeforeInitialization");return bean;}/*** BeanPostProcessor接口中的方法* 在Bean的自定义初始化方法执行完成之后执行* Bean对象已经存在了*/@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {log.info("<<postProcessAfterInitialization");return bean;}/*** InstantiationAwareBeanPostProcessor中自定义的方法* 在方法实例化之前执行  Bean对象还没有*/@Overridepublic Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {log.info("--->postProcessBeforeInstantiation");return null;}/*** InstantiationAwareBeanPostProcessor中自定义的方法* 在方法实例化之后执行  Bean对象已经创建出来了*/@Overridepublic boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {log.info("<---postProcessAfterInstantiation");return true;}/*** InstantiationAwareBeanPostProcessor中自定义的方法* 可以用来修改Bean中属性的内容*/@Overridepublic PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean,String beanName) throws BeansException {log.info("<---postProcessPropertyValues--->");return pvs;}
}

4、输出结果

执行测试方法,输出如下结果

springextend.ExtendInstantiationAwareBeanPostProcessor - --->postProcessBeforeInstantiation
springextend.TestAutowired - TestAutowired构造方法实例化------run
springextend.ExtendInstantiationAwareBeanPostProcessor - <---postProcessAfterInstantiation
springextend.ExtendInstantiationAwareBeanPostProcessor - <---postProcessPropertyValues--->
springextend.TestAutowired - 设置属性:若曼底登陆
ExtendInstantiationAwareBeanPostProcessor - >>postProcessBeforeInitialization
springextend.TestAutowired - init bean执行 
springextend.ExtendInstantiationAwareBeanPostProcessor - <<postProcessAfterInitialization

结论

通过打印结果可以看到五个方法全部都执行,并且清楚它们的执行顺序

测试二

基于测试一的代码,更改postProcessBeforeInstantiation方法内部返回

1、测试类

postProcessBeforeInstantiation方法代码

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {log.info("--->postProcessBeforeInstantiation");return new TestAutowired();
}

2、输出结果

从打印结果看,该方法返回的结果如果为null,后面的方法都正常执行了,但是如果postProcessBeforeInstantiation方法返回实例对象后跳过了对象的初始化操作,直接执行了postProcessAfterInitialization(该方法在自定义初始化方法执行完成之后执行),跳过了postProcessAfterInstantiation,postProcessPropertyValues以及自定义的初始化方法

springextend.ExtendInstantiationAwareBeanPostProcessor - --->postProcessBeforeInstantiation
springextend.TestAutowired - TestAutowired构造方法实例化------run
springextend.ExtendInstantiationAwareBeanPostProcessor - <<postProcessAfterInitialization

结论

postProcessBeforeInstantiation方法是最先执行的方法,它在目标对象实例化之前调用,该方法的返回值类型是Object,我们可以返回任何类型的值。由于这个时候目标对象还未实例化,所以这个返回值可以用来代替原本该生成的目标对象的实例(比如代理对象)。如果该方法的返回值代替原本该生成的目标对象,后续只有postProcessAfterInitialization方法会调用,其它方法不再调用;否则按照正常的流程走

源码解析

postProcessProperties

  • CommonAnnotationBeanPostProcessor : 注册带有 @Resource 注解的属性
  • AutowiredAnnotationBeanPostProcessor : 处理带有 @Value、@Autowired、@Inject、@Lookup 注解的属性
CommonAnnotationBeanPostProcessor
static {//将需要解析的注解 都先加载进来  @Resource必须加载,@WebServiceRef和@EJB按条件加载resourceAnnotationTypes.add(Resource.class);webServiceRefClass = loadAnnotationType("javax.xml.ws.WebServiceRef");if (webServiceRefClass != null) {resourceAnnotationTypes.add(webServiceRefClass);}ejbClass = loadAnnotationType("javax.ejb.EJB");if (ejbClass != null) {resourceAnnotationTypes.add(ejbClass);}}

postProcessProperties方法的逻辑就是找到对应的带有注解的元数据Metadata ,然后注入进去

@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs);try {metadata.inject(bean, beanName, pvs);}catch (Throwable ex) {throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);}return pvs;
}

findResourceMetadata方法主要就是查询元数据信息

private InjectionMetadata findResourceMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {// 首先优先用beanName 作为缓存的Key,没有beanName回退到类名作为缓存键String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());// 首先快速检查并发映射(从缓存里面获取,是否已经存在)InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);// needsRefresh判断是否为null ,为null就重新创建, 这里是一个双重检查if (InjectionMetadata.needsRefresh(metadata, clazz)) {synchronized (this.injectionMetadataCache) {metadata = this.injectionMetadataCache.get(cacheKey);if (InjectionMetadata.needsRefresh(metadata, clazz)) {// 如果不为null ,先清除一下if (metadata != null) {metadata.clear(pvs);}// 这里开始创建,并放入缓存,详细在下面分析metadata = buildResourceMetadata(clazz);this.injectionMetadataCache.put(cacheKey, metadata);}}}return metadata;}

buildResourceMetadata类的大概逻辑主要是对字段,方法遍历,看是否带有 @Resource,@WebServiceRef,@EJB 注解 ,如果有就对 其进行解析, 然后再将结果放入一个list

private InjectionMetadata buildResourceMetadata(Class<?> clazz) {if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {return InjectionMetadata.EMPTY;}List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();Class<?> targetClass = clazz;do {final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();ReflectionUtils.doWithLocalFields(targetClass, field -> {if (webServiceRefClass != null && field.isAnnotationPresent(webServiceRefClass)) {if (Modifier.isStatic(field.getModifiers())) {throw new IllegalStateException("@WebServiceRef annotation is not supported on static fields");}currElements.add(new WebServiceRefElement(field, field, null));}else if (ejbClass != null && field.isAnnotationPresent(ejbClass)) {if (Modifier.isStatic(field.getModifiers())) {throw new IllegalStateException("@EJB annotation is not supported on static fields");}currElements.add(new EjbRefElement(field, field, null));}else if (field.isAnnotationPresent(Resource.class)) {if (Modifier.isStatic(field.getModifiers())) {throw new IllegalStateException("@Resource annotation is not supported on static fields");}if (!this.ignoredResourceTypes.contains(field.getType().getName())) {currElements.add(new ResourceElement(field, field, null));}}});//如果方法上面有 @Resource,@WebServiceRef,@EJB 注解 ,就放入listReflectionUtils.doWithLocalMethods(targetClass, method -> {Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {return;}if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {if (webServiceRefClass != null && bridgedMethod.isAnnotationPresent(webServiceRefClass)) {if (Modifier.isStatic(method.getModifiers())) {throw new IllegalStateException("@WebServiceRef annotation is not supported on static methods");}if (method.getParameterCount() != 1) {throw new IllegalStateException("@WebServiceRef annotation requires a single-arg method: " + method);}PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);currElements.add(new WebServiceRefElement(method, bridgedMethod, pd));}else if (ejbClass != null && bridgedMethod.isAnnotationPresent(ejbClass)) {if (Modifier.isStatic(method.getModifiers())) {throw new IllegalStateException("@EJB annotation is not supported on static methods");}if (method.getParameterCount() != 1) {throw new IllegalStateException("@EJB annotation requires a single-arg method: " + method);}PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);currElements.add(new EjbRefElement(method, bridgedMethod, pd));}else if (bridgedMethod.isAnnotationPresent(Resource.class)) {if (Modifier.isStatic(method.getModifiers())) {throw new IllegalStateException("@Resource annotation is not supported on static methods");}Class<?>[] paramTypes = method.getParameterTypes();if (paramTypes.length != 1) {throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method);}if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);currElements.add(new ResourceElement(method, bridgedMethod, pd));}}}});elements.addAll(0, currElements);targetClass = targetClass.getSuperclass();}while (targetClass != null && targetClass != Object.class);return InjectionMetadata.forElements(elements, clazz);}

inject方法获取里面的 InjectedElement 列表 ,然后开始遍历注入

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Collection<InjectedElement> checkedElements = this.checkedElements;Collection<InjectedElement> elementsToIterate =(checkedElements != null ? checkedElements : this.injectedElements);if (!elementsToIterate.isEmpty()) {for (InjectedElement element : elementsToIterate) {element.inject(target, beanName, pvs);}}
}
AnnotationInjectedBeanPostProcessor

AnnotationInjectedBeanPostProcessor大体的逻辑也是一样,但是针对的注解主要是@Value,@Autowired, @Inject

@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {InjectionMetadata metadata = findInjectionMetadata(beanName, bean.getClass(), pvs);try {metadata.inject(bean, beanName, pvs);} catch (BeanCreationException ex) {throw ex;} catch (Throwable ex) {throw new BeanCreationException(beanName, "Injection of @" + getAnnotationType().getName()+ " dependencies is failed", ex);}return pvs;
}

总结

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/418284.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【重构获得模式 Refactoring to Patterns】

重构获得模式 Refactoring to Patterns 面向对象设计模式是“好的面向对象设计”&#xff0c;所谓“好的面向对象设计”指的是那些可以满足“应对变化&#xff0c;提高复用”的设计。 现代软件设计的特征是“需求的频繁变化”。设计模式的要点是“寻找变化点&#xff0c;然后…

Kafka 实战演练:创建、配置与测试 Kafka全面教程

文章目录 1.配置文件2.消费者1.注解方式2.KafkaConsumer 3.依赖1.注解依赖2.KafkaConsumer依赖 本文档只是为了留档方便以后工作运维&#xff0c;或者给同事分享文档内容比较简陋命令也不是特别全&#xff0c;不适合小白观看&#xff0c;如有不懂可以私信&#xff0c;上班期间都…

5G前传-介绍

1. 引用 知识分享系列一&#xff1a;5G基础知识-CSDN博客 5G前传的最新进展-CSDN博客 灰光和彩光_通信行业5G招标系列点评之二&#xff1a;一文读懂5G前传-光纤、灰光、彩光、CWDM、LWDM、MWDM...-CSDN博客 术语&#xff1a; 英文缩写描述‌BBU&#xff1a;Building Baseba…

review——C++中的右值引用

目录 前言 一、什么是左值、什么是右值 二、右值引用 1.右值引用与右值引用的一些性质 2.解释一下左值引用与右值应用于程序员之间的关系 3.右值引用与移动语义 4.右值引用右值后变成左值的必要性与完美转发 1.右值引用引用右值后变为左值属性的必要性 2.完美转发 Ⅰ…

【docker】docker 镜像仓库的管理

Docker 仓库&#xff08; Docker Registry &#xff09; 是用于存储和分发 Docker 镜像的集中式存储库。 它就像是一个大型的镜像仓库&#xff0c;开发者可以将自己创建的 Docker 镜像推送到仓库中&#xff0c;也可以从仓库中拉取所需的镜像。 Docker 仓库可以分为公共仓…

Java获取小程序码示例(三种小程序码)

首先我们可以看到官方文档上是有三种码的 获取小程序码 这里特别要注意的是第一种和第三种是有数量限制的&#xff0c;所以大家生成的时候记得保存&#xff0c;也不要一直瞎生成 还有一点要注意的是第一种和第二种是太阳码 第三种是方形码 好了直接上代码 这里要注意&#xff…

Golang | Leetcode Golang题解之第391题完美矩形

题目&#xff1a; 题解&#xff1a; func isRectangleCover(rectangles [][]int) bool {type point struct{ x, y int }area, minX, minY, maxX, maxY : 0, rectangles[0][0], rectangles[0][1], rectangles[0][2], rectangles[0][3]cnt : map[point]int{}for _, rect : range…

HTML生日蛋糕

目录 写在前面 完整代码 代码分析 系列文章 写在最后 写在前面 HTML实现的生日蛋糕来喽&#xff0c;小编亲测&#xff0c;发给好友可以直接打开哦。在代码的第183行可以写下对朋友的祝福&#xff0c;快拿去送给你的好朋友吧&#xff01; 完整代码 <!DOCTYPE html>…

C++ 上位软件通过Snap7开源库访问西门子S7-1200/S7-1500数据块的方法

C 上位软件通过Snap7开源库访问西门子S7-1200/S7-1500数据块的方法

【LeetCode】15.三数之和

题目要求 解题思路 这道题我们可以使用暴力解法来解决&#xff0c;时间复杂度为O&#xff08;N^3&#xff09;。但是会超时&#xff0c;因此我们需要对暴力解法进行优化&#xff0c;而这道题我们使用双指针来进行优化&#xff0c;即依次固定一个数&#xff0c;在接下来的区间中…

vue3整合antv x6实现图编辑器快速入门

安装&#xff1a; npm install antv/x6 --save如果使用 umd 包&#xff0c;可以使用下面三个 CDN 中的任何一个&#xff0c;默认使用 X6 的最新版&#xff1a; https://unpkg.com/antv/x6/dist/index.jshttps://cdn.jsdelivr.net/npm/antv/x6/dist/index.jshttps://cdnjs.clo…

mysql树形结构返回是否叶子节点

我们界面上展示树形结构的时候往往会用到懒加载&#xff0c;做懒加载需要知道哪个节点是叶子节点&#xff0c;这样叶子节点就不需要继续往下加载了&#xff0c;这种需求可以通过sql实现 先来看下表结构 方式一,通过sql语句直接获取leaf 什么是叶子节点&#xff1f;就是没有哪…

Alternative account/备选科目代码配置说明 【1:1和国家科目配置运营科目】

业务场景&#xff1a; 有个关于mapping的问题&#xff1a; 1. 如果在GL account的master data里面&#xff0c;之前已经做好了alternative account的mapping, 那我要改成另外的local account,这样 A 会不会影响本地子公司的报表&#xff1f; 2. 如果GL account和alternativ…

Redis 缓存深度解析:穿透、击穿、雪崩与预热的全面解读

Redis 缓存深度解析&#xff1a;穿透、击穿、雪崩与预热的全面解读 一 . 什么是缓存 ?二 . 使用 Redis 作为缓存三 . 缓存的更新策略3.1 定期生成3.2 实时生成 四 . 缓存预热、缓存穿透、缓存雪崩、缓存击穿4.1 缓存预热4.2 缓存穿透4.3 缓存雪崩4.4 缓存击穿 Hello , 大家好 …

WebAPI (一)DOM树、DOM对象,操作元素样式(style className,classList)。表单元素属性。自定义属性。间歇函数定时器

文章目录 Web API基本认知一、 变量声明二、 DOM1. DOM 树2. DOM对象3. 获取DOM对象(1)、选择匹配的第一个元素(2)、选择匹配多个元素 三、 操作元素1. 操作元素内容2. 操作元素属性(1)、常用属性&#xff08;href之类的&#xff09;(2)、通过style属性操作CSS(3)、通过类名(cl…

wireshark安装及抓包新手使用教程

Wireshark是非常流行的网络封包分析软件&#xff0c;可以截取各种网络数据包&#xff0c;并显示数据包详细信息。常用于开发测试过程各种问题定位。本文主要内容包括&#xff1a; 1、Wireshark软件下载和安装以及Wireshark主界面介绍。 2、WireShark简单抓包示例。通过该例子学…

Ollama—87.4k star 的开源大模型服务框架!!

这一年来&#xff0c;AI 发展的越来越快&#xff0c;大模型使用的门槛也越来越低&#xff0c;每个人都可以在自己的本地运行大模型。今天再给大家介绍一个最厉害的开源大模型服务框架——ollama。 项目介绍 Ollama 是一个开源的大语言模型&#xff08;LLM&#xff09;服务工具…

IDM 工具下载 地图高程数据

巧用IDM工具 快捷下载ASTER GDEM v3高程数据 ASTER GDEM v3是NASA推出的30米高清DEM,覆盖了几乎全部的地球陆地。那么,在NASA官网怎么下载ASTER GDEM v3的地形高程数据呢? 首先,你需要注册一个nasa的账号 注册网址: https://urs.earthdata.nasa.gov/users/new 注册方式和国…

基于人工智能的聊天情感分析系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 情感分析是一种自然语言处理任务&#xff0c;旨在识别文本中的情感&#xff0c;如“积极”、“消极”或“中立”。在聊天应用中&#…

iPhone手机清理软件:照片清理功能全解析

在数字化生活中&#xff0c;智能手机成为我们记录生活点滴的主要工具&#xff0c;尤其是iPhone&#xff0c;以其卓越的相机功能备受用户青睐。然而&#xff0c;成千上万的照片迅速堆积&#xff0c;不仅占用了大量存储空间&#xff0c;还使得设备运行缓慢。在众多解决方案中&…