【iOS】KVO

KVO初识

KVO全称为Key-Value observing,翻译为键值观察,这是一种机制,其允许其他对象的指定属性的更改通知给对象。

这里我们首先要去理解KVC,再来理解KVO。这是由于键值观察是建立在键值编码的基础上的,下面附上笔者学习KVC的笔记:【iOS】KVC

在iOS日常开发过程中,我们会使用KVO来监听对象属性的变化,作出相应;也就是说当指定的被观察的对象的属性被修改之后,KVO会自动通知相应的观察者。在之前的学习中,还学习过NSNotificatioCenter,那么这两者又有什么区别呢?

  • 相同点
    • 二者实现的原理都是观察者模式,都是用于监听
    • 都可以实现一对多的操作
  • 不同点
    • KVO只可以用于监听对象属性的变化,并且属性名都通过NSString来查找,编译器并不会检查对错与补全,纯手敲会比较容易出错
    • NSNotificatioCenter的发送监听的操作我们可以控制,KVO由系统控制
    • KVO可以记录新旧值的变化

KVO的使用

KVO的基本使用

下面我们先来看看KVO使用的三部曲:

  • 注册观察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];//这里observer:添加的监听者的对象,当监听的属性发生改变时会通知这个对象。 keyPath:监听的属性,不能传nil。 options:指明通知发出的时机以及change中的键值。 context:是一个可选的参数,可以传任何数据。

options:

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,//更改前的值
    NSKeyValueObservingOptionOld = 0x02,//更改后的值
    NSKeyValueObservingOptionInitial = 0x04,//观察最初的值(在注册观察服务时会调用一次触发方法)
    NSKeyValueObservingOptionPrior  = 0x08 //分别在值修改前后触发方法(即一次修改有两次触发)
};
  • 实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) 
      NSLog(@"%@", change);
}
  • 移除观察者
[self.person removeObserver:self forKeypath:@"name"];

context的使用

在我们平时的使用中,我们一般都将 其设置为NULL,那么其究竟由什么用呢?这里我们来探究一下。

context参数主要作用其实是为KVO回调提供一个标识符或标记,有助于区分同一属性的不同观察者或在多个地方注册的同一个观察者

在这里插入图片描述

这是在Apple官方文档中对于context的说明,从其解释来看,使用context是一种更安全、可扩展的方法来确保收到的通知是发送给我们观察者而不是superClass

如果说当我们的父类中已经有了一个name属性,而子类中也有一个name属性,这两者都注册对name的观察,这个时候如果仅仅通过keyPath已经无法区分是哪一个name发生了变化,这个时候就可以使用context

//定义context
static void *PersonNameContext = &PersonNameContext;
static void *StudentNameContext = &StudentNameContext;
//注册观察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext];
//KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (context == PersonNameContext) {
        NSLog(@"%@", change);
    } else if (context == StudentNameContext) {
        NSLog(@"%@", change);
    }
}

移除通知的必要性

或许在我们日常的开发中,我们有时候觉得是否移除了通知都无关痛痒,但是不移除通知会带来潜在的风险

这里我们将Person类以单例的形式创建,这个时候如果没有移除通知我们来看看会发生什么呢?

#import "LGDetailsViewController.h"

@interface LGDetailsViewController ()

@property (nonatomic, strong) Person* person;

@end

@implementation LGDetailsViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    self.person = [Person sharedInstance];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
    
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.person.name = @"wahaha";
    //[self.navigationController popViewControllerAnimated:YES];
}

- (void)dealloc
{
    NSLog(@"dealloc");
    [self.person removeObserver:self forKeyPath:@"name"];
}

这里我们pop出这个界面再push进这个界面,这个时候就会报错:

在这里插入图片描述

这是由于没有移除观察,单例对象依旧存在,再次进来点击屏幕就会出现野指针报错。

KVO观察可变数组

这里我创建一个Student类,其中有一个可变数组muArr,现在观察他:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.detaiView = [[LGDetailsViewController alloc] init];
    self.stu = [[Student alloc] init];
    [self.stu addObserver:self forKeyPath:@"muArr" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.stu.muArr addObject:@"1"];
    NSLog(@"点击屏幕");
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"muArr"]) {
        NSLog(@"%@", change);
    }
}

- (void)dealloc
{
    [self.stu removeObserver:self forKeyPath:@"muArr"];
}

这时候点击屏幕是不会打印的,这是为什么呢?

  • 首先我们要知道KVO是建立在KVC的基础上的,而可变数组直接添加方法是不会调用Setter方法的
  • 在KVC官方文档中,针对可变数组的集合类型来说,访问集合对象需要通过调用mutableArrayValueForKey以达到将元素添加到可变数组中的目的

这里我们应该使用mutableArrayValueForKey方法:

[[self.stu mutableArrayValueForKey:@"muArr"] addObject:@"1"];

KVO原理

官方解释

这里我们先来看看Apple官方对于KVO的解释:

在这里插入图片描述

  • KVO使用的是isa-swizzling技术来实现的
  • isa指针指向维护分配表的对象的类,该分派表实质上包含指向该类实现的方法的指针以及其他数据
  • 在为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。isa指针的值不一定反映实例的实际类
  • 您永远不应依靠isa指针来确定类成员身份。相反,您应该使用class方法来确定对象实例的类

代码实现

这里我们来看看注册观察者前后的isa的指向:

在这里插入图片描述

通过这两张图片我们可以看出,在观察者注册前后Student类并没有发生变化,但是实例对象的isa指向发生了变化。

那么动态生成的中间类NSKVONotifying_StudentStudent是一个什么关系呢?

我们在注册观察者前后来分别调用打印一下子类的方法,看看有什么结果:

- (void)printClasses:(Class)cls{
     // 注册类的总数
     int count = objc_getClassList(NULL, 0);
     // 创建一个数组, 其中包含给定对象
     NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
     // 获取所有已注册的类
     Class* classes = (Class*)malloc(sizeof(Class)*count);
     objc_getClassList(classes, count);
     for (int i = 0; i<count; i++) {
          if (cls == class_getSuperclass(classes[i])) {
                [mArray addObject:classes[i]];
          }
     }
     free(classes);
     NSLog(@"classes = %@", mArray);
}

当我们在注册观察者前后分别调用这个方法的时候,我们来看看打印结果:

在这里插入图片描述

这里我们可以发现NSKVONotifying_StudentStudent的子类。

在这里插入图片描述

动态子类的方法

下面我们来看看动态子类中有哪些方法,我们通过rutime-API来打印一下动态子类和观察类的方法:

-(void) printClassAllMethod:(Class) cls {
    NSLog(@"*********************");
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

这里我直接给出打印StudentNSKVONotifying_Student的结果:

在这里插入图片描述

这里我们可以看到,中间类重写了setmuArrclassdealloc这几个方法,并且添加了一个叫_isKVOA的方法,来区分是不是系统通过KVO自动生成的

观察者被移除isa指向

上述我们看了当注册观察者之后会生成一个中间类,那么当我们移除观察者之后,这里又会指回原本的类

如何预防KVO崩溃

导致KVO崩溃的原因

通常我们在KVO的日常使用中会遇到以下几个原因造成KVO崩溃:

  1. KVO添加次数和移除次数并不匹配:
    • 移除未注册的观察者,造成崩溃
    • 重复移除了多次,移除次数多于添加的次数,从而导致崩溃
    • 重复添加了多次,这种情况并不会导致KVO崩溃,但当观察对象发生改变的时候,会被观察多次
    • 添加和删除的顺序不一致会导致崩溃
  2. 添加了观察者,但是并没有实现observerValueForKeyPath:ofObject:context方法,这会导致崩溃
  3. 添加或者移除时keypath==nil,导致崩溃
  4. 被观察者提前释放,被观察者在dealloc的时候还注册着KVO,导致崩溃的产生

当我们面对后两种情况的时候:

  • SwizzleobserveValueForKeyPath:ofObject:change:context:方法,在调用原方法外包一层try catch就可以解决第二个的崩溃
  • swizzleaddObserver:forKeyPath:options:context:removeObserver:forKeyPath:removeObserver:forKeyPath:context方法,在里面对keypath判空或者直接在原方法外包装一次try catch就可以解决第三点

try catch:一种常见的防护崩溃而被广泛使用的错误处理机制

防止崩溃的方法

有很多中方法去预防KVO崩溃,这里记录我学习的预防崩溃的方法:

  1. NSObject创建一个分类,使用方法交换,实现自定义的BMP_addObserver:forKeyPath:options:context:BMP_removeObserver:forKeyPath:BMP_removeObserver:forKeyPath:context:BMPKVO_dealloc 方法,用来替换系统原生的添加移除观察者方法的实现。
  2. 使用一个自定义的对象,在该对象中使用{keypath : [observer1, observer2 , ...](NSHashTable)}结构的关系哈希表对keyPathobserver之间进行一个维护
  3. 在分类中自定义dealloc的实现,移除了多余的观察者

参考博客:iOS 开发:『Crash 防护系统』(二)KVO 防护

KVO遇到的问题

直接修改成员变量的值,会不会触发KVO?

不会触发KVO,首先,本质上*KVO是替换掉setter*方法的实现,所以只有通过set方法修改才会触发KVO

KVC修改属性是否会触发KVO呢

是会触发的,虽然我们知道KVC使用setValue:firKey不一定触发instance实例对象的setter方法,但是该方法在修改成员变量的值的时候会调用willchangevalueforkeydidchangevalueforKey两个方法,这会触发监听器的回调方法

[!IMPORTANT]

willchangevalueforKeydidchangevalueforKey是KVO的两个核心方法

KVO的具体实现流程

KVO的实现是依赖于OC运行时的动态特性,通过使用isa-swizzling技术动态的创建一个当前类的派生类NSKVONotifying_XXX,动态的修改当前实例对象的isa指针,使其指向派生类,并重写了setXXXclassdealloc这几个方法,并且添加了一个叫_isKVOA的方法,来区分是不是系统通过KVO自动生成的。在OC发送消息的时候,通过isa指针找到当前对象所属的类对象。类对象中保存着当前对象的实例方法,在向该对象发送消息的时候,实际上是发送到了派生类对象的方法。

这里要注意一点,当移除观察者之后,isa指针会指向原来的类,但是并不销毁中间类

在这里插入图片描述

子类继承父类的属性,属性改变时KVO是否可以监听?

由于继承关系Father <- Son <- KVOSOn,当监听一个父类属性keyPath的时候,Son实例同样也可以通过继承链找到父类的Setter方法,将其加入到KVOSon中去。

<think>我们正在讨论如何实现类似Dependency-Check的依赖项识别功能,特别是通过解析JAR文件的MANIFEST.MF文件来获取依赖项的名称和版本。根据用户提供的引用,我们可以从以下几个方面入手: 1. **MANIFEST.MF文件的作用**:引用[1]和引用[3]提到,MANIFEST.MF文件是JAR包中的清单文件,记录了元数据信息,包括Implementation-Title(实现标题)和Implementation-Version(实现版本)等。这些信息可以用来识别依赖项。 2. **解析JAR文件**:我们需要读取JAR文件,并提取其中的META-INF/MANIFEST.MF文件内容。 3. **提取关键属性**:从MANIFEST.MF中提取Implementation-Title和Implementation-Version作为依赖项的名称和版本。 4. **其他可能的来源**:除了MANIFEST.MF,还可以考虑其他方式(如pom.properties)来获取更准确的信息。 ### 技术实现方案 #### 1. 读取JAR文件并解析MANIFEST.MF 我们可以使用Java的标准库(java.util.jar)来读取JAR文件并解析清单文件。 ```java import java.io.IOException; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; public class JarManifestParser { public static void main(String[] args) { String jarPath = "path/to/your.jar"; try (JarFile jarFile = new JarFile(jarPath)) { Manifest manifest = jarFile.getManifest(); if (manifest != null) { Attributes mainAttributes = manifest.getMainAttributes(); String implementationTitle = mainAttributes.getValue("Implementation-Title"); String implementationVersion = mainAttributes.getValue("Implementation-Version"); System.out.println("Implementation-Title: " + implementationTitle); System.out.println("Implementation-Version: " + implementationVersion); } } catch (IOException e) { e.printStackTrace(); } } } ``` #### 2. 处理多行属性 MANIFEST.MF文件中的属性可能跨越多行(每行不超过72个字符,续行以空格开头)。Java的`Manifest`类已经处理了这种情况,因此我们无需额外处理。 #### 3. 备用方案:pom.properties 如果MANIFEST.MF中没有提供所需信息,我们可以尝试在JAR文件中查找`pom.properties`文件(通常位于`META-INF/maven/<groupId>/<artifactId>/pom.properties`)。该文件包含了更详细的项目信息,包括groupId、artifactId和version。 ```java import java.io.InputStream; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class PomPropertiesParser { public static void parsePomProperties(JarFile jarFile) throws IOException { // 遍历JAR文件中的条目,查找pom.properties for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements(); ) { JarEntry entry = entries.nextElement(); if (entry.getName().endsWith("pom.properties") && !entry.getName().contains("SNAPSHOT")) { Properties properties = new Properties(); try (InputStream is = jarFile.getInputStream(entry)) { properties.load(is); String groupId = properties.getProperty("groupId"); String artifactId = properties.getProperty("artifactId"); String version = properties.getProperty("version"); System.out.println("GroupId: " + groupId + ", ArtifactId: " + artifactId + ", Version: " + version); } } } } } ``` #### 4. 综合策略 在实际的依赖项识别中,我们可以采用以下策略: 1. 首先尝试从MANIFEST.MF中获取Implementation-Title和Implementation-Version。 2. 如果无法获取,则尝试解析pom.properties文件。 3. 如果上述方法都失败,则使用JAR文件名作为依赖项名称(例如:`spring-core-5.2.0.RELEASE.jar`可以解析为artifactId: spring-core, version: 5.2.0.RELEASE)。 #### 5. 注意事项 - **Implementation-Title的格式**:Implementation-Title可能不是标准的Maven坐标(groupId:artifactId)。它可能只包含artifactId,甚至可能不是唯一的。因此,尽可能使用pom.properties中的信息。 - **版本格式**:版本号可能包含特殊字符串(如“SNAPSHOT”、“RELEASE”等),需要保留原样。 ### 与依赖树解析的整合 在构建工具(如Maven、Gradle)中,依赖项通常通过坐标(groupId, artifactId, version)来管理。因此,我们解析出的依赖项信息需要映射到坐标上。如果从JAR中解析出的信息只有artifactId和version,我们可以通过以下方式推断groupId: - 使用pom.properties中的groupId(如果存在)。 - 使用已知的groupId模式(例如,常见的开源项目通常使用反向域名)。 - 建立本地仓库索引,通过artifactId查找可能的groupId(需要额外的数据库)。 ### 性能优化 - **缓存机制**:对已经解析过的JAR文件,可以缓存其元数据,避免重复解析。 - **并行解析**:在扫描多个JAR文件时,使用多线程并行处理。 ### 示例:综合解析器 ```java public class DependencyIdentifier { public static DependencyInfo identifyDependency(File jarFile) { DependencyInfo info = new DependencyInfo(); try (JarFile jar = new JarFile(jarFile)) { // 尝试从MANIFEST.MF获取 Manifest manifest = jar.getManifest(); if (manifest != null) { Attributes attributes = manifest.getMainAttributes(); String title = attributes.getValue("Implementation-Title"); String version = attributes.getValue("Implementation-Version"); if (title != null && version != null) { info.setArtifactId(title); info.setVersion(version); } } // 如果MANIFEST.MF中没有,尝试从pom.properties获取 if (info.getArtifactId() == null) { PomProperties pomProps = findPomProperties(jar); if (pomProps != null) { info.setGroupId(pomProps.getGroupId()); info.setArtifactId(pomProps.getArtifactId()); info.setVersion(pomProps.getVersion()); } } // 如果都没有,则使用文件名 if (info.getArtifactId() == null) { String fileName = jarFile.getName(); // 移除.jar后缀 if (fileName.toLowerCase().endsWith(".jar")) { fileName = fileName.substring(0, fileName.length() - 4); } // 假设文件名格式为artifactId-version int lastDash = fileName.lastIndexOf('-'); if (lastDash > 0) { info.setArtifactId(fileName.substring(0, lastDash)); info.setVersion(fileName.substring(lastDash + 1)); } else { info.setArtifactId(fileName); } } } catch (IOException e) { e.printStackTrace(); } return info; } private static PomProperties findPomProperties(JarFile jar) throws IOException { // 遍历JAR条目查找pom.properties Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); if (name.startsWith("META-INF/maven/") && name.endsWith("/pom.properties")) { Properties props = new Properties(); try (InputStream is = jar.getInputStream(entry)) { props.load(is); PomProperties pomProps = new PomProperties(); pomProps.setGroupId(props.getProperty("groupId")); pomProps.setArtifactId(props.getProperty("artifactId")); pomProps.setVersion(props.getProperty("version")); return pomProps; } } } return null; } } ``` ### 总结 通过解析JAR文件的MANIFEST.MF和pom.properties文件,我们可以获取依赖项的名称和版本。然而,由于MANIFEST.MF中的信息可能不完整或不规范,建议优先使用pom.properties文件。同时,需要结合文件名解析作为后备方案。 ### §§相关问题§§ 1. 如何在没有MANIFEST.MF和pom.properties的情况下识别JAR文件的依赖信息? 2. 如何解析OSGI bundle的MANIFEST.MF文件(包含Bundle-SymbolicName和Bundle-Version)? 3. 如何处理同一个JAR文件包含多个pom.properties文件的情况(例如多模块项目)? 4. 如何将识别出的依赖信息与漏洞数据库进行匹配? 5. 如何优化大规模JAR文件扫描的性能?</think># Dependency-Check Java 依赖项识别技术实现方案 ## 核心识别机制 Dependency-Check 主要通过解析 JAR 文件的元数据来识别依赖项,核心实现基于以下技术: ### 1. JAR 文件结构解析 ```java public void analyzeJar(File jarFile) throws IOException { try (JarFile jar = new JarFile(jarFile)) { // 解析MANIFEST.MF Manifest manifest = jar.getManifest(); if (manifest != null) { processManifest(manifest); } // 检查pom.properties文件 processPomProperties(jar); // 分析文件名 analyzeFileName(jarFile.getName()); } } ``` ### 2. MANIFEST.MF 解析技术细节 引用[1]和引用[3]提到,MANIFEST.MF 文件包含关键元数据: ```java private void processManifest(Manifest manifest) { Attributes mainAttrs = manifest.getMainAttributes(); // 获取Implementation-Title和Implementation-Version String title = mainAttrs.getValue("Implementation-Title"); String version = mainAttrs.getValue("Implementation-Version"); // 如果未找到,尝试使用Bundle-SymbolicName和Bundle-Version (OSGi) if (title == null) title = mainAttrs.getValue("Bundle-SymbolicName"); if (version == null) version = mainAttrs.getValue("Bundle-Version"); // 清理数据 if (title != null) title = sanitizeTitle(title); // 添加到依赖项列表 if (title != null && version != null) { addDependency(title, version); } } ``` ### 3. pom.properties 解析 当 MANIFEST.MF 信息不足时,解析 META-INF/maven 中的 pom.properties: ```java private void processPomProperties(JarFile jar) throws IOException { Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); if (name.startsWith("META-INF/maven/") && name.endsWith("/pom.properties")) { Properties props = new Properties(); try (InputStream is = jar.getInputStream(entry)) { props.load(is); String groupId = props.getProperty("groupId"); String artifactId = props.getProperty("artifactId"); String version = props.getProperty("version"); if (artifactId != null && version != null) { addDependency(groupId + ":" + artifactId, version); } } } } } ``` ### 4. 文件名分析 当元数据缺失时,通过文件名解析: ```java private void analyzeFileName(String fileName) { // 移除.jar扩展名 if (fileName.toLowerCase().endsWith(".jar")) { fileName = fileName.substring(0, fileName.length() - 4); } // 版本号通常是最后一个连字符后的部分 int lastDash = fileName.lastIndexOf('-'); if (lastDash > 0) { String artifact = fileName.substring(0, lastDash); String version = fileName.substring(lastDash + 1); // 验证版本号格式 if (isValidVersion(version)) { addDependency(artifact, version); } } } ``` ## 依赖项识别优先级 Dependency-Check 使用多层识别策略: 1. **首选**: pom.properties 中的完整坐标 (groupId:artifactId:version) 2. **次选**: MANIFEST.MF 中的 Implementation-Title/Version 3. **备选**: 文件名解析 (artifactId-version) 4. **补充**: 类路径扫描和字节码分析 ## 技术挑战与解决方案 ### 1. 版本号规范化 ```java private String normalizeVersion(String version) { // 处理特殊版本标记 version = version.replace(".RELEASE", "") .replace(".Final", "") .replace("-SNAPSHOT", ""); // 标准化版本格式 return version.replaceAll("[^0-9.]", ""); } ``` ### 2. 依赖项别名处理 ```java private Map<String, String> ALIAS_MAP = Map.of( "springframework", "org.springframework", "jackson-core", "com.fasterxml.jackson.core" ); private String resolveAlias(String identifier) { return ALIAS_MAP.getOrDefault(identifier, identifier); } ``` ### 3. 冲突解决策略 当多个来源提供不同信息时: 1. 优先使用 pom.properties 信息 2. 次选 MANIFEST.MF 信息 3. 最后考虑文件名解析 4. 记录冲突并生成警告 ## 完整工作流程 ```mermaid graph TD A[扫描JAR文件] --> B[提取MANIFEST.MF] B --> C{是否包含有效信息?} C -->|是| D[记录Implementation-Title/Version] C -->|否| E[查找pom.properties] E --> F{是否找到?} F -->|是| G[记录groupId/artifactId/version] F -->|否| H[解析文件名] H --> I[提取artifactId和version] D --> J[标准化依赖标识] G --> J I --> J J --> K[添加到依赖清单] ``` ## 性能优化技术 1. **并行处理**: ```java List<File> jarFiles = // 获取所有JAR文件 jarFiles.parallelStream().forEach(this::analyzeJar); ``` 2. **缓存机制**: ```java private Map<String, DependencyInfo> dependencyCache = new ConcurrentHashMap<>(); public DependencyInfo analyzeJar(File jarFile) { String key = jarFile.getAbsolutePath() + "_" + jarFile.lastModified(); return dependencyCache.computeIfAbsent(key, k -> doAnalysis(jarFile)); } ``` 3. **增量扫描**: - 记录文件修改时间戳 - 只扫描新文件或修改过的文件 ## 实际应用示例 对于引用[1]中的 MANIFEST.MF: ``` Manifest-Version: 1.0 Implementation-Title: spring-demo Implementation-Version: 0.0.1-SNAPSHOT ... ``` 识别结果: - 依赖名称: spring-demo - 版本: 0.0.1-SNAPSHOT - 来源: MANIFEST.MF ## 相关工具集成 引用[2]和引用[4]提到,Gradle/Maven插件实现类似: ```groovy // Gradle插件示例 task dependencyCheck(type: DependencyCheckTask) { outputDirectory = "${buildDir}/reports/dependency-check" scanConfigurations = ['runtimeClasspath'] } ``` ##
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值