iOS——KVO底层学习

news2024/10/6 20:32:16

前情回顾

什么是KVO?在之前的博客里我们已经学过:

KVO全称Key Value Observing。KVO传值允许对象监听另一个对象的特定属性,当该属性改变的时候,会触发事件。
KVO不仅可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象的时候,通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVO监听的方法。集合对象包含NSArray和NSSet。

  • KVO只是监听setter方法,例如像可变数组添加元素的方法(addObject)它不属于setter方法,所以即使你向数组中add多少个元素也不会有监听反应。
  • 在不使用时一定要移除KVO。

KVO具体的例子看:
IOS——多界面传值

KVO的实现

在同样一个类中,set方法都在该类里,为什么只有被添加为观察者的实例的属性变化会触发观察者的方法,而未被添加为观察者的实例的属性变化则不会?
这里就要涉及到KVO的底层实现了,让我们来了解它是如何监听的:
这里我们先写一个KVO的例子,通过点击button监听nV的str1属性:

@interface otherViewController : UIViewController

@property (nonatomic, strong) NSString *str1;
@property (nonatomic, strong) NSString *str2;

@end
#import "ViewController.h"
#import "otherViewController.h"

@interface ViewController ()

@property (nonatomic, strong)otherViewController *nV;
@property (nonatomic, strong)otherViewController *nV2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.nV = [[otherViewController alloc] init];
    self.nV2 = [[otherViewController alloc] init];

    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    button.frame = CGRectMake(100, 300, 200, 100);
    [button setTitle:@"touch" forState:UIControlStateNormal];
    [self.view addSubview:button];
    [button addTarget:self action:@selector(touchPress) forControlEvents:UIControlEventTouchUpInside];
    
    NSKeyValueObservingOptions optips = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    
    [self.nV addObserver:self forKeyPath:@"str1" options:optips context:nil];
}

- (void)touchPress {
    self.nV.str1 = @"str1";
    self.nV2.str1 = @"str2";
}

在ViewController中写监听方法,这里顺便再详细解释一下这个方法和它的参数:

/*
  1. keyPath 是一个 NSString 类型的参数,表示被观察的属性或属性路径。例如,如果你正在观察一个 Person 对象的 name 属性,那么 keyPath 就是 @"name"。
  2. object 是一个 id 类型的参数,表示被观察的对象。
  3. change 是一个字典,键是 NSKeyValueChangeKey 枚举类型中的值,值是 id 类型。这个字典包含了关于属性变化的信息。
常见的 NSKeyValueChangeKey 枚举值有:NSKeyValueChangeKindKey: 表示变化的类型(例如,是否是设置、插入、删除等)。NSKeyValueChangeNewKey: 表示新的值。NSKeyValueChangeOldKey: 表示旧的值。
  4. context 是一个 void * 类型的参数,它是一个通用的指针,允许你传递任何类型的数据。我们可以检查这个 context 值来确定是哪个观察触发了这个方法。
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"监听到%@的%@属性发生了变化-%@ -%@",object, keyPath, change, context);
}

运行结果:
在这里插入图片描述

然后,我们使用lldb,观察nV在被监听的时候的动态变化:
在这里插入图片描述

我们可以发现,nV和nV2明明都是同一个类创建出来的实例对象,为什么打印出来它们的类不同呢,而且这个NSKVONotifying_otherViewController明明我们没有创建啊?
这里是因为内部被修改了,那么实际的实现过程是什么呢。我们先来看NSKVONotifying_otherViewController
当一个对象被KVO监听时,其isa指针会被动态地修改,指向一个由runtime创建的新类,这个新类的名称通常以“NSKVONotifying_”作为前缀,后面跟着被监听对象的原始类名。
这个新类是原始类的子类,并且系统会为这个新类重写被观察属性的setter方法。setter方法会负责在调用原始setter方法之前和之后,通知所有注册的观察者属性值的更改情况。
当一个对象被KVO监听时,其isa指针会被修改,指向上述提到的动态创建的新类。这样做是为了在不修改原始类的情况下,能够拦截到被观察属性的setter方法调用,从而通知观察者。
因此这就是为什么我们在之前学习的博客中提到:修改被观察对象的属性时,应该使用属性的setter方法(如self.property = newValue;),而不是直接访问实例变量(如_property = newValue;)。

形象的实现逻辑我们可以看下面两张图:
在这里插入图片描述
在这里插入图片描述

所以现在我们可以得知,加了kvo和没有加kvo走的并不是一套,加了kvo的走的是子类NSKVONotifying_otherViewController的set方法,并达到监听的目的。

KVO源码

在分析KVO的内部实现之前,先来分析一下KVO的存储结构,主要用到了以下几个类:

  • GSKVOInfo
  • GSKVOPathInfo
  • GSKVOObservation

GSKVOInfo

GSKVOInfo 类用于保存和管理监控特定对象的观察者及其观察路径的信息。这个类的设计目的是为了支持键值观察(KVO)机制的实现:

/*
 * 这个类的实例用于保存监控特定对象的观察者的信息。
 */
@interface GSKVOInfo : NSObject
{
  NSObject *instance;          // 被观察的对象实例(不被保留)。
  NSRecursiveLock *iLock;      // 递归锁,用于线程安全的访问。
  NSMapTable *paths;           // 存储观察路径的映射表。
}
@end
  • 它保存了一个对象的实例,但是它没有持有,也不是weak,所以当释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者
  • paths 用于保存keyPath 到 GSKVOPathInfo 的映射:

GSKVOPathInfo

/*
 * 这个类的实例记录了某个键路径的观察者以及发送通知过程中的递归状态。
 */
@interface GSKVOPathInfo : NSObject
{
@public
  unsigned recursion;               // 递归计数器,跟踪发送通知的递归深度。
  unsigned allOptions;              // 所有观察选项的位掩码,用于存储观察选项。
  NSMutableArray *observations;     // 观察者数组,存储所有与键路径相关的观察者。
  NSMutableDictionary *change;      // 变化字典,用于存储属性变化的信息。
}
@end
  • 它保存了一个keypath对应的所有观察者
  • observations保存了所有的观察者(GSKVOObservation 类型)
  • allOptions保存了观察者的options集合
  • change 保存了KVO触发要传递的内容

GSKVOObservation

/*
 * 这个类的实例记录了单个观察的所有信息。
 */
@interface GSKVOObservation : NSObject
{
@public
  NSObject *observer;  // 观察者对象(不被保留,使用零弱引用指针)
  void *context;       // 上下文信息,用户在添加观察者时传递的上下文
  int options;         // 观察选项,指定观察的行为
}
@end

它保存了单个观察的所有信息

  • observer保存观察者 注意这里也是 Not retained
  • context options 都是添加观察者时传入的参数

这三个类的调用流程

  1. 添加观察者
    当向一个对象添加观察者时,会创建或获取该对象对应的 GSKVOInfo 实例。
    GSKVOInfo 实例中,为特定的属性路径创建或获取一个 GSKVOPathInfo 实例。
    创建一个新的 GSKVOObservation 实例,存储观察者的信息,并将其添加到 GSKVOPathInfoobservations 数组中。

  2. 发送通知
    当属性值发生变化时,GSKVOPathInfo 实例会遍历其 observations 数组,向所有观察者发送通知。
    在通知发送过程中,会更新 recursion 属性以跟踪递归状态,确保线程安全。

  3. 移除观察者
    当移除观察者时,会从 GSKVOPathInfo 实例的 observations 数组中删除相应的 GSKVOObservation 实例。
    如果某个属性路径不再有观察者,则从 GSKVOInfo 实例的 paths 表中移除对应的 GSKVOPathInfo 实例。

KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。==在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。==所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

为什么要重写class方法呢?

如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_类名,就会将该类暴露出来。
由于 KVO 使用了 isa-swizzling 技术,苹果建议在开发中不应该直接依赖 isa 指针来判断对象的类型,而是应该通过 class 实例方法来获取对象的类型。这样可以避免因为 isa-swizzling 而导致的类型判断错误。
在这里插入图片描述

isa-swizzling

isa-swizzling技术主要通过以下几个类实现:

  • GSKVOReplacement
  • GSKVOBase
  • GSKVOSetter

GSKVOReplacement

/*
 * 这个类保存了关于一个类被观察时其替代子类的信息。
 */
@interface GSKVOReplacement : NSObject
{
  Class original;          /* 原始类 */
  Class replacement;       /* 替代类 */
  NSMutableSet *keys;      /* 被观察的属性键集合 */
}

- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
  • original:保存被观察对象的原始类。
  • replacement:保存替代原始类的中间类。在 KVO 机制中,KVO 会动态创建一个中间类来替代原始类,以便拦截和处理属性变化通知。
  • keys:保存所有被观察的属性键集合。这些键表示哪些属性被观察者监听。
// 创建
- (id) initWithClass: (Class)aClass
{
  NSValue       *template;
  NSString      *superName;
  NSString      *name;

  original = aClass;

  /*
   * Create subclass of the original, and override some methods
   * with implementations from our abstract base class.
   */
  superName = NSStringFromClass(original);      // original == Temp
  name = [@"GSKVO" stringByAppendingString: superName];    // name = GSKVOTemp
  template = GSObjCMakeClass(name, superName, nil);   // template = GSKVOTemp
  GSObjCAddClasses([NSArray arrayWithObject: template]);
  replacement = NSClassFromString(name);
  GSObjCAddClassBehavior(replacement, baseClass);

  /* Create the set of setter methods overridden.
   */
  keys = [NSMutableSet new];

  return self;
}
  • 该方法是 GSKVOReplacement 类的初始化方法,接受一个类作为参数。
  • 使用 NSStringFromClass 函数获取原始类的类名,并生成一个新的类名(例如在原类名之前加上 GSKVO 前缀)。
  • 使用 GSObjCMakeClass 函数创建一个新的类模板,该类继承自原始类。
  • 注册新的类(替代类)并获取其 Class 对象。使用 GSObjCAddClassBehavior 函数为替代类添加行为(通常是方法实现)。
  • 初始化 keys 集合,用于存储被重写的 setter 方法。并返回初始化后的实例。

GSKVOBase

这个类默认提供了几个方法,都是对NSObject方法的重写,而从上面得知,这些方法都要拷贝到新创建的替换类中。也就是被观察者会拥有这几个方法的实现

@implementation	GSKVOBase

- (void) dealloc
{
  // Turn off KVO for self ... then call the real dealloc implementation.
  //对象释放后,移除KVO数据,将对象重新指向原始类
  [self setObservationInfo: nil];
  object_setClass(self, [self class]);
  [self dealloc];
  GSNOSUPERDEALLOC;
}

//此方法用来隐藏替换类信息,应用层获取类的信息,仍然是原始类的信息. 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
- (Class) class
{
  return class_getSuperclass(object_getClass(self));
}

/*这个方法是属于KVC中的,重写这个方法,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey]和[self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口。*/
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
  Class		c = [self class];
  void		(*imp)(id,SEL,id,id);

  imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

  if ([[self class] automaticallyNotifiesObserversForKey: aKey])
    {
      [self willChangeValueForKey: aKey];
      imp(self,_cmd,anObject,aKey);
      [self didChangeValueForKey: aKey];
    }
  else
    {
      imp(self,_cmd,anObject,aKey);
    }
}

//此方法和class方法原理相同
- (Class) superclass
{
  return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
@end

GSKVOSetter

@interface	GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef  _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end

这个类和上面重写KVC方法原理相同,将来会替换被观察者keypath的setter方法实现。会在原始setter方法前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey]

KVO流程总结

添加观察者

我们使用其源码说明:

@implementation	 GSKVOInfo

/* 添加观察者方法 */
- (void) addObserver: (NSObject*)anObserver
      forKeyPath: (NSString*)aPath
         options: (NSKeyValueObservingOptions)options
         context: (void*)aContext
{
  GSKVOPathInfo         *pathInfo;
  GSKVOObservation      *observation;
  unsigned              count;

  // 确认观察者实现了 observeValueForKeyPath:ofObject:change:context: 方法
  if ([anObserver respondsToSelector:
    @selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
    {
      return; // 如果没有实现该方法,直接返回
    }
  
  [iLock lock]; // 加锁
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath); // 获取路径信息
  if (pathInfo == nil) // 如果路径信息为空
    {
      pathInfo = [GSKVOPathInfo new]; // 创建新的路径信息
      aPath = [aPath copy]; // 复制路径字符串,使用不可变对象作为键
      NSMapInsert(paths, (void*)aPath, (void*)pathInfo); // 插入路径信息到路径映射表
      [pathInfo release]; // 释放路径信息
      [aPath release]; // 释放路径字符串
    }

  observation = nil;
  pathInfo->allOptions = 0;
  count = [pathInfo->observations count]; // 获取观察者数量
  while (count-- > 0) // 遍历所有观察者
    {
      GSKVOObservation      *o;

      o = [pathInfo->observations objectAtIndex: count]; // 获取观察者
      if (o->observer == anObserver) // 如果观察者匹配
        {
          o->context = aContext; // 更新上下文
          o->options = options; // 更新选项
          observation = o; // 记录当前观察者
        }
      pathInfo->allOptions |= o->options; // 更新路径信息的所有选项
    }
  
  if (observation == nil) // 如果当前观察者不存在
    {
      observation = [GSKVOObservation new]; // 创建新的观察者
      GSAssignZeroingWeakPointer((void**)&observation->observer, (void*)anObserver); // 分配零弱指针
      observation->context = aContext; // 设置上下文
      observation->options = options; // 设置选项
      [pathInfo->observations addObject: observation]; // 添加观察者到路径信息
      [observation release]; // 释放观察者
      pathInfo->allOptions |= options; // 更新路径信息的所有选项
    }

  if (options & NSKeyValueObservingOptionInitial) // 如果选项包含 NSKeyValueObservingOptionInitial
    {
      /* 如果设置了 NSKeyValueObservingOptionInitial 选项,
       * 必须立即发送包含现有值的通知。
       */
      [pathInfo->change setObject: [NSNumber numberWithInt: 1]
                           forKey: NSKeyValueChangeKindKey]; // 设置更改类型为插入
      if (options & NSKeyValueObservingOptionNew) // 如果选项包含 NSKeyValueObservingOptionNew
        {
          id value;

          value = [instance valueForKeyPath: aPath]; // 获取当前值
          if (value == nil) // 如果值为空
            {
              value = null; // 设置为 null
            }
          [pathInfo->change setObject: value
                               forKey: NSKeyValueChangeNewKey]; // 设置新值
        }
      [anObserver observeValueForKeyPath: aPath
                                ofObject: instance
                                  change: pathInfo->change
                                 context: aContext]; // 发送通知给观察者
    }
  [iLock unlock]; // 解锁
}
  • 检查观察者是否实现了 observeValueForKeyPath:ofObject:change:context: 方法。如果没有实现,直接返回。
  • 调用 [iLock lock]进行加锁,确保线程安全。
  • 从 paths 映射表中获取指定键路径的 GSKVOPathInfo 对象。如果不存在,则创建新的 GSKVOPathInfo 对象,并插入到映射表中。
  • 遍历 GSKVOPathInfo 对象中的所有观察者,检查是否已经存在相同的观察者。如果存在,则更新其上下文和选项,并记录下当前观察者;否则,创建新的观察者对象 GSKVOObservation 并添加到 GSKVOPathInfo 中。
  • 如果选项包含 NSKeyValueObservingOptionInitial,则立即发送包含当前值的通知给观察者。首先设置通知类型为 NSKeyValueChangeKindKey,然后根据选项设置新值(NSKeyValueChangeNewKey),并调用观察者的 observeValueForKeyPath:ofObject:change:context: 方法发送通知。
  • 调用 [iLock unlock] 进行解锁。

移除观察者

/*
 * 移除观察者
 */
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
  GSKVOPathInfo *pathInfo;

  [iLock lock]; // 加锁,确保线程安全
  pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath); // 获取路径信息
  if (pathInfo != nil) // 如果路径信息不为空
    {
      unsigned  count = [pathInfo->observations count]; // 获取观察者数量

      pathInfo->allOptions = 0; // 重置路径信息的所有选项
      while (count-- > 0) // 反向遍历观察者列表
        {
          GSKVOObservation      *o;

          o = [pathInfo->observations objectAtIndex: count]; // 获取当前观察者
          if (o->observer == anObserver || o->observer == nil) // 如果观察者匹配或已释放
            {
              [pathInfo->observations removeObjectAtIndex: count]; // 移除观察者
              if ([pathInfo->observations count] == 0) // 如果观察者列表为空
                {
                  NSMapRemove(paths, (void*)aPath); // 从路径映射表中删除该键路径
                }
            }
          else // 如果观察者不匹配
            {
              pathInfo->allOptions |= o->options; // 更新路径信息的所有选项
            }
        }
    }
  [iLock unlock]; // 解锁
}
  • removeObserver:forKeyPath: 方法用于从指定键路径的观察者列表中移除指定的观察者。
  • 如果观察者列表为空,则从路径映射表中删除该键路径。
  • 该方法通过加锁来确保线程安全,防止在多线程环境中出现数据竞争问题。

面试题

  1. ios用什么方式实现对一个对象的kvo?(kvo的本质是什么?)
    利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的类,当修改instance对象的属性时,会调用Foundation的——NSSetXXXValueAndNotify函数:(willChangeValueForKey; 父类原来的setter方法;didChangeValueForKey(内部会触发监听器(observer)的监听方法(observerValueForKeyPath:)))

  2. 如何手动触发kvo?
    (不调用setage方法)
    手动调用willChangeValueForKey 和didChangeValueForKey方法,即可直接触发kvo。

  3. 直接修改成员变量会触发KVO么?
    不会触发KVO,(添加kvo的person实例,其实是NSKVONotyfing_person类,再调用setter方法,不是调用person的setter方法,而是NSKVONotyfing_person的setter方法,因为修改成员变量不是setter方法赋值self.person->age=@“12”, 所以就无所谓调用NSKVONotyfing_person类的setter方法,也就不会实现kvo。)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

进口电动温度调节阀的原理-美国品牌

进口电动温度调节阀的原理可以清晰地分为以下几个部分进行描述&#xff1a; 1. 温度感应与信号转换 温度传感器&#xff1a;负责感应介质&#xff08;如液体、气体等&#xff09;的温度&#xff0c;并将其转化为相应的电信号。这个电信号反映了介质当前的实际温度。 2. 信号…

数据结构——bitset(位图)模拟实现

从一个题目引出位图 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在这40亿个数中&#xff1f; 这里有两种大家容易想到的解法&#xff1a; 1.遍历搜索&#xff0c;时间复杂度为O(N) 2.先排序&#xff08;O(NlogN)&a…

【DBA早下班系列】—— 并行SQL/慢SQL 问题该如何高效收集诊断信息

1. 前言 OceanBase论坛问答区或者提交工单支持的时候大部分时间都浪费在了诊断信息的获取交互上&#xff0c;今天我就其中大家比较头疼的SQL问题&#xff0c;给大家讲解一下如何一键收集并行SQL/慢SQL所需要的诊断信息&#xff0c;减少沟通成本&#xff0c;让大家早下班。 2. …

Linux安装——初入linux

前言&#xff1a;linux是雷纳斯托瓦斯在1991年接触到学校机房的装配unix操作系统的电脑后&#xff0c; 立志并且力行大学四年创建出来的操作系统。 在大佬创建linux后就将它的源代码公开——开源。 同时&#xff0c; 大佬给linux取得全名是&#xff1a; Linux is not unix。lin…

使用 python 将 Markdown 文件转换为 ppt演示文稿

在这篇博客中&#xff0c;我们将展示如何使用 wxPython 创建一个简单的图形用户界面 (GUI)&#xff0c;以将 Markdown 文件转换为 PowerPoint 演示文稿。我们将利用 markdown2 模块将 Markdown 转换为 HTML&#xff0c;并使用 python-pptx 模块将 HTML 内容转换为 PowerPoint 幻…

笨蛋学算法之LeetCodeHot100_2_字母异位词分组(Java)

package com.lsy.leetcodehot100;import java.util.*;public class _Hot2_字母异位词分组 {public static List<List<String>> groupAnagrams(String[] strs){//首先&#xff0c;定义一个map类型&#xff0c;其中key为String&#xff0c;value为List<String>…

C# 中文字符串转GBK字节的示例

一、编写思路 在 C# 中&#xff0c;将中文字符串转换为 GBK 编码的字节数组需要使用 Encoding 类。然而&#xff0c;Encoding 类虽然默认并不直接支持 GBK 编码&#xff0c;但是可以通过以下方式来实现这一转换&#xff1a; 1.使用系统已安装的编码提供者&#xff08;如果系统…

axure使用中继器画柱状图

源文件在顶部。 在axure通过读取中继器中的数据来画柱状图&#xff0c;如下图&#xff1a; 1&#xff09;创建一个中继器&#xff0c;在里面创建两列&#xff1a;1列是柱状图底部的名称、2列是柱的高度&#xff0c;如下图&#xff1a; 2&#xff09;双击中继器&#xff0c;画一…

springmvc 全局异常处理器配置的三种方式深入底层源码分析原理

文章目录 springmvc 全局异常处理器配置的三种方式&深入底层源码分析原理配置全局异常处理器的三种方式实现接口HandlerExceptionResolver并配置到WebMvcConfigurer注解式配置ExceptionHandlercontroller里方法上定义ExceptionHandler 深入源码分析进入DispatcherServlet执…

防泄密的方法?用迅软DSE加密软件可靠吗?

防止泄密常用的方法则是使用加密软件&#xff0c;把文件转换为密文&#xff0c;这样一来&#xff0c;未授权的人员打不开&#xff0c;查看不到内容&#xff0c;也就防止了泄密行为的出现。只需用到加密软件&#xff0c;即可保护数据。 用迅软DSE加密软件可靠吗&#xff1f; 特…

【算法与数据结构】【数组篇】【题6-题10】

系列文章 本人系列文章-CSDN博客https://blog.csdn.net/handsomethefirst/article/details/138226266?spm1001.2014.3001.5502 1.数组基本知识点 1.1概念 数组就是一个集合。数组会用一些名为索引的数字来标识每项数据在数组中的位置&#xff0c;且在大多数编程语言中&…

java float 无处不是陷阱

小数位随意&#xff0c;不确定。 public float payerAmt; 数据库是 decimal&#xff08;10,2&#xff09; 页面输入后&#xff0c;保存到数据库竟然是随意的一个小数点&#xff0c; 中间没有经过任何运算&#xff0c;输入 - 保存 <result column"payer_amt" …

计算机图形学入门08:反走样、滤波与卷积

1.抗锯齿(反走样) 在上一章中&#xff0c;在光栅化三角形的时候出现了明显的锯齿效果。如下图所示是三角形采样过程&#xff1a; 从图上可知像素点不是纯红色就是纯白色。如果在采样前先进行模糊操作(滤波)&#xff0c;如下图所示&#xff1a; 经过模糊操作后三角形的边缘颜色变…

了解多线程

1.线程与并发 1.1 理解进程和线程的区别 进程&#xff1a;是指一个内存中运行的应用程序&#xff08;程序的一次运行就产生一个进程&#xff09;&#xff0c;每个进程都有自己独立的一块内存空间&#xff0c;比如在Windows的任务管理器中&#xff0c;一个运行的xx.exe就是一个进…

一个热门的源码整站数据打包完整代码(开箱即用),集成了最新有效数据和完美wordpress主题。

分享一个资源价值几千元的好代码资源网整站打包代码&#xff0c;这个wordpress网站基于集成了ripro9.1完全明文无加密后门版本定制开发&#xff0c;无需独立服务器&#xff0c;虚拟主机也可以完美运营&#xff0c;只要主机支持php和mysql即可。整合了微信登录和几款第三方的主题…

AGP7+ 适配 plugin 动态引入第三方插件

AGP4 适配前 def hwPlugin com.huawei.agconnectmProject.getPlugins().apply(hwPlugin)AGP7 适配后 在 AGP4 如果仍然使用上述代码&#xff0c;那么编译期会报错&#xff0c;升级版本之后使用下面的pluginManager 即可。 def hwPlugin com.huawei.agconnectmProject.plug…

Studio One安装教程+软件安装包下载

Studio One6全新版本上线 记录、生产、混合、掌握和执行所有操作。从工作室到舞台&#xff0c;Studio One6以易用为核心&#xff0c;是您的创意合作伙伴。 当你准备好登上舞台时&#xff0c;Studio One就在那里。只有Studio One从最初的灵感到完整的制作&#xff0c;最终混音…

3D感知视觉表示与模型分析:深入探究视觉基础模型的三维意识

在深度学习与大规模预训练的推动下&#xff0c;视觉基础模型展现出了令人印象深刻的泛化能力。这些模型不仅能够对任意图像进行分类、分割和生成&#xff0c;而且它们的中间表示对于其他视觉任务&#xff0c;如检测和分割&#xff0c;同样具有强大的零样本能力。然而&#xff0…

(三十八)Vue之插槽Slots

文章目录 插槽介绍插槽分类默认插槽具名插槽条件插槽动态插槽名 作用域插槽默认作用域插槽具名作用域插槽 上一篇&#xff1a;&#xff08;三十七&#xff09;vue 项目中常用的2个Ajax库 插槽介绍 在之前的文章中&#xff0c;我们已经了解到组件能够接收任意类型的值作为 prop…

【品质】如何培养幽默感,如何幽默的沟通与应对生活(自卑vs自信,悲观vs乐观)

【品质】如何培养幽默感&#xff0c;如何幽默和正能量的沟通与应对生活&#xff08;自卑vs自信&#xff0c;悲观vs乐观&#xff09; 文章目录 一、性格底色&#xff08;自我认知&#xff0c;世界观&#xff09;1、从悲观的底色开始2、用摆烂、自嘲的方式与世界和解 二、沟通方法…