iOS--通知、代理、单例模式总结

news2024/11/18 23:31:13

通知

概要

  • 观察者和被观察者都无需知晓对方,只需要通过标记在NSNotificationCenter中找到监听该通知所对应的类,从而调用该类的方法。
  • 并且在NSNotificationCenter中,观察者可以只订阅某一特定的通知,并对齐做出相应操作,而不用对某一个类发的所有通知都进行更新操作。
  • NSNotificationCenter对观察者的调用不是随机的,而是遵循注册顺序一一执行的,并且在该线程内是同步的

使用步骤

在要传递参数的地方,发送通知给通知中心

[[NSNotificationCenter defaultCenter] postNotificationName:@"temp" object:nil userInfo:@{@"content": self.myTextField.text}];

在接收参数的地方注册通知,并实现定义方法

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(Notificate:) name:@"temp" object:nil];

在不需要通知的时候,移除通知

[[NSNotificationCenter defaultCenter] removeObserver:self];

自定义实现通知功能

首先创建一个自定义文件NotificationCenter,继承自NSObject,用作自定义通知的类,因为通知是可以实现多对多关系的,所以我们在这个类中还需要定义一个可变的字典属性,用来存储注册的通知。又因为注册的通知数据需要一直保存下来,所以我们使用单例来完成这一操作,保证我们在想要访问已经注册的通知的时候,其数据是存在的。

@interface NotificationCenter ()

// 因为通知是多对多的关系,所以这里定义一个可变字典用来存储对应关系
@property (nonatomic, strong) NSMutableDictionary *classDictionary;

@end
@implementation NotificationCenter

// 实现默认的通知中心,是个单例,防止其自动销毁
+ (instancetype)defaultCenter {
    // 定义一个锁
    static dispatch_once_t onceToken;
    // 创建通知中心的单例,同时初始化其中数据
    static NotificationCenter *notificationCenter = nil;
    dispatch_once(&onceToken, ^{
        notificationCenter = [NotificationCenter new];
        notificationCenter.classDictionary = [NSMutableDictionary dictionary];
    });
    return notificationCenter;
}
@end

因为我们使用通知的目的就是为了传递参数,供别的类来使用,所以我们这里再定义一个专门保存通知的类ZJQNotification,其中包含通知的必要信息:通知的名称、通知传递的参数信息、以及一个id类型的object。因为这是需要给外界透露的接口,外界不能对其进行写操作,所以为只读(readOnly)属性,除了这三个参数,当然还得需要一个快速的初始化类的方法:

// 通知类,用来保存通知及其参数
@interface ZJQNotification : NSObject

// 对外有三个只读属性
@property (readonly, copy) NSNotificationName name; // 通知名
@property (nullable, readonly, copy) NSDictionary *userInfo; // 参数信息
@property (nullable, readonly, retain) id object; // 接收通知的对象

// 快速初始化类参数的方法
- (instancetype)initWithName:(NSString *)name object:(nullable id)object userInfo:(nullable NSDictionary *)userInfo;
@end

该通知类中的三个参数是对外只读的,内部可以进行修改,所以我们在内部重写属性,实现可读可写,同时实现该类快速初始化方法:

@interface ZJQNotification()
// 在内部可修改这三个属性
@property (nonatomic, copy) NSString *name; // 通知名
@property (nonatomic, copy) NSDictionary *userInfo; // 参数信息
@property (nonatomic, retain) id object; // 接收通知的对象

@end
 
// 通知类,用来保存通知及其参数
@implementation ZJQNotification

// 快速初始化类参数的方法
- (instancetype)initWithName:(NSString *)name object:(id)object userInfo:(NSDictionary *)userInfo {
    ZJQNotification *notification = [ZJQNotification new];
    notification.name = name;
    notification.object = object;
    notification.userInfo = userInfo;
    return notification;
}

@end

接着我们在之前定义的通知中心ZJQNotificationCenter中再定义通知常用的对外开放的接口,注册通知、发送通知、删除通知:

// 通知中心
@interface ZJQNotificationCenter : NSObject

// 默认的通知中心
+ (instancetype)defaultCenter;

// 添加通知中心
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject;

// 发送通知
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;

// 移除通知
- (void)removeObserver:(id)observer;
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject;

@end

接着就一个一个实现,首先是添加通知的逻辑,我们在添加、调用通知的时候,需要知道三个必要信息,一个是创建该通知的实例类,一个是通知要调用的自定义方法,以及一个object。所以我们需要将这三个信息保存起来,一起保存到刚刚我们创建的通知中心单例的classDictionary属性中,为了方便我们后续的查找,所以该属性的key我们使用注册通知时通知的名称标识,因为一个通知可能对应多个类,所以这里我们value使用一个数组,该数组中的每个变量又是一个字典,其中的内容就是上边所说的三个参数:

// 添加通知中心
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSString *)aName object:(nullable id)anObject {
    // 以通知名为key来设置value并保存在通知中心
    // 从通知中心获取该通知名的所有注册信息
    NSMutableArray *array = self.classDictionary[aName];
    // 如果通知中心没有储存过该通知名的信息,就新建
    if (!array) {
        array = [NSMutableArray array];
    }
    // 向数组中添加传递过来的信息
    [array addObject:@{@"class": observer, @"selector": NSStringFromSelector(aSelector), @"object": anObject ? : [NSNull null]}];
    // 将更新过的数组重新添加到通知中心
    [self.classDictionary setObject:array forKey:aName];
}

发送通知的逻辑,其实就是通过发送过来的通知名标识aNameclassDictionary属性中查找对应的通知信息,然后依次使用objc_msgSend发送消息,从而达到传值的目的,这里我们用系统封装好的NSInvocation类进行调用:

// 发送通知
- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject {
    [self postNotificationName:aName object:anObject userInfo:nil];
}

- (void)postNotificationName:(NSString *)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo {
    // 通过发送通知的通知名,找到在通知中心保存的该通知名的所有注册的类的信息
    NSMutableArray *array = self.classDictionary[aName];
    // 通过获取到的信息中的方法名和类信息,来逐一使用msgSend发送消息给目标类
    for (NSDictionary *mapDictionary in array) {
        // 当mapDictionary中的object与anObject一致或者接收者为null时,才调用方法,确保信息无误不会发错
        if ([mapDictionary[@"object"] isEqual:anObject] || [mapDictionary[@"object"] isKindOfClass:[NSNull class]]) {
            // NSInvocation;用来包装方法和对应的对象,它可以存储方法的名称,对应的对象,对应的参数,
            /*
             NSMethodSignature:签名:再创建NSMethodSignature的时候,必须传递一个签名对象,签名对象的作用:用于获取参数的个数和方法的返回值
             */
            // 创建签名对象的时候不是使用NSMethodSignature这个类创建,而是方法属于谁就用谁来创建,创建当前访问信息的class类中的selector方法的签名
            NSMethodSignature *signature = [[mapDictionary[@"class"] class] instanceMethodSignatureForSelector:NSSelectorFromString(mapDictionary[@"selector"])];
            
            // 1、通过创建的方法签名,创建NSInvocation对象
            NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
            // 给NSInvocation对象设置实现该selector方法的类信息
            invocation.target = mapDictionary[@"class"];
            // 给NSInvocation对象设置调用的方法的信息
            invocation.selector = NSSelectorFromString(mapDictionary[@"selector"]);
            // 包装要使用通知传递的信息,通过消息转发机制来实现跨界面传值
            // 注意:设置参数的索引时不能从0开始,因为0已经被self占用,1已经被_cmd占用
            ZJQNotification *notification = [[ZJQNotification alloc] initWithName:aName object:anObject userInfo:aUserInfo];
            [invocation setArgument:&notification atIndex:2];
            /* 第一个参数:需要给指定方法传递的值
                   第一个参数需要接收一个指针,也就是传递值的时候需要传递地址 */
            // 第二个参数:需要给指定方法的第几个参数传值
            
            // 2、调用NSInvocation对象的invoke方法
            // 只要调用invocation的invoke方法,就代表需要执行NSInvocation对象中制定对象的指定方法,并且传递指定的参数
            [invocation invoke];
        }
    }
}

移除通知时,通过给定的通知的信息,在classDictionary中删除对应的数据即可:

// 移除通知
// 找到对应的类,在classDictionary中删除即可
// 全部移除
- (void)removeObserver:(id)observer {
    // 创建一个临时字典,存放删除完了的数据,最后用这个字典更新classDictionary数据
    NSMutableDictionary *tempDictionary = [NSMutableDictionary dictionary];
    // 枚举遍历
    [self.classDictionary enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSMutableArray *tempArray = [obj mutableCopy];
        for (NSDictionary * mapDictionary in obj) {
            // 判断该类是不是observer的类,是就删除
            if ([mapDictionary[@"class"] isKindOfClass:[observer class]]) {
                [tempArray removeObject:mapDictionary];
            }
        }
        // 删除完了,添加到tempDictionary中
        [tempDictionary setObject:tempArray forKey:key];
    }];
    // 更新classDictionary数据
    self.classDictionary = tempDictionary;
}

// 根据通知名移除
- (void)removeObserver:(id)observer name:(nullable NSNotificationName)aName object:(nullable id)anObject {
    // 获取该通知名注册的所有信息
    NSMutableArray *array = self.classDictionary[aName];
    NSMutableArray *tempArray = [array mutableCopy];
    for (NSDictionary *mapDictionary in array) {
        // 判断该类是不是observer的类
        if ([mapDictionary[@"class"] isKindOfClass:[observer class]]) {
            // 如果该通知的接收方相等或者接收方不存在,再删除,确保不会误删
            if ([mapDictionary[@"object"] isEqual:anObject] || !anObject) {
                [tempArray removeObject:mapDictionary];
            }
        }
    }
    // 更新classDictionary数据
    [self.classDictionary setValue:tempArray forKey:aName];
}

通知原理

通知机制的核心是一个与线程关联的单例对象叫通知中心(NSNotificationCenter)。通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。
因为苹果的通知源码没有开源,所以我们看看GNUStep的源码。

数据结构

单例类
从我们之前使用通知的流程和代码来看,通知其实就是一个单例,方便随时访问。

static NSNotificationCenter *default_center = nil;

+ (NSNotificationCenter*) defaultCenter
{
  return default_center;
}

NSNotificationCenter:消息中心
这个单例类中主要定义了两个表,一个存储所有注册通知信息的表的结构体,一个保存单个注册信息的节点结构体。

typedef struct NCTbl {
  Observation       *wildcard;  // 添加观察者时既没有传入 NotificationName ,又没有传入object,就会加在这个链表上,它里边的观察者可以接收所有的系统通知
  GSIMapTable       nameless;   // 添加观察者时没有传入 NotificationName 的表
  GSIMapTable       named;      // 添加观察者时传入了 NotificationName 的表
} NCTable

保存了观察者的信息:

typedef struct  Obs {
  id        observer;   // 观察者对象
  SEL       selector;   // 方法信息
  struct Obs    *next;      // 指向下一个节点
  int       retained;   /* Retain count for structure.  */
  struct NCTbl  *link;      /* Pointer back to chunk table  */
} Observation;

以及一些宏定义:

#define        TABLE                ((NCTable*)_table)
#define        WILDCARD             (TABLE->wildcard)
#define        NAMELESS             (TABLE->nameless)
#define        NAMED                (TABLE->named)
#define        LOCKCOUNT            (TABLE->lockCount)

named表
在 named 表中,NotifcationName 作为表的 key,因为我们在注册观察者的时候是可以传入一个参数 object 用于只监听指定该对象发出的通知,并且一个通知可以添加多个观察者,所以还需要一张表来保存 object 和 Observer 的对应关系。这张表的是 key、Value 分别是以 object 为 Key,Observer 为 value。用了链表这种数据结构实现保存多个观察者的情况。
请添加图片描述
在实际开发过程中 object 参数我们经常传 nil,这时候系统会根据 nil 自动生成一个 key,相当于这个 key 对应的 value(链表)保存的就是当前通知传入了 NotificationName 没有传入 object 的所有观察者。
nameless表
上边说了 named 表,那么 nameless 表就不难想象了,他注册时没有 NotificationName ,即没有了最外边一层键值对的约束了,其中就只有 object 和 Observation 所对应的键值对结构了:请添加图片描述
wildcard表
这个表既没有 NotificationName 也没有 object 了,所以他就会在 nameless基础上在脱去一层键值对,那么它就只剩下一个链表了,该练表存储了可以接收所有通知的类的信息请添加图片描述

添加观察者

使用方法addObserver:selector:name:object添加观察者,根据 GNUStep 的源码分析:

- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name
              object: (id)object
{
  Observation        *list;
  Observation        *o;
  GSIMapTable        m;
  GSIMapNode        n;
// observer为空时的报错
  if (observer == nil)
    [NSException raise: NSInvalidArgumentException
                format: @"Nil observer passed to addObserver ..."];
// selector为空时的报错
  if (selector == 0)
    [NSException raise: NSInvalidArgumentException
                format: @"Null selector passed to addObserver ..."];
// observer不能响应selector时的报错
  if ([observer respondsToSelector: selector] == NO)
    {
      [NSException raise: NSInvalidArgumentException
        format: @"[%@-%@] Observer '%@' does not respond to selector '%@'",
        NSStringFromClass([self class]), NSStringFromSelector(_cmd),
        observer, NSStringFromSelector(selector)];
    }
// 给表上锁
  lockNCTable(TABLE);
// 建立一个新Observation,存储这次注册的信息
  o = obsNew(TABLE, selector, observer);
  // 如果有name
  if (name) {
      // 在named表中 以name为key寻找value
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
      // named表中没有找到对应的value
      if (n == 0) {
          // 新建一个表
          m = mapNew(TABLE);
          // 由于这是对给定名称的首次观察,因此我们对该名称进行了复制,以便在map中无法对其进行更改(来自GNUStep的注释)
          name = [name copyWithZone: NSDefaultMallocZone()];
          // 新建表作为name的value添加在named表中
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          GS_CONSUMED(name)
      } else { //named表中有对应的value
      	  // 取出对应的value
          m = (GSIMapTable)n->value.ptr;
      }
      // 将observation添加到正确object的列表中
      // 获取添加完后name对应的value的object对应的链表
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
      // n是object的value
      if (n == 0) { // 如果object对应value没有数据
          o->next = ENDOBS;
          // 将o作为object的value链表的头结点插入
          GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
      } else { // 如果有object对应的value那么就直接添加到原练表的尾部
          // 在链表尾部加入o
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
      // 这个else if 就是没有name有object的Observation,对object进行的操作相同,
  } else if (object) {
  	  // 直接获取object对应的value链表
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n == 0) { // 这个对应链表如果没有数据
          o->next = ENDOBS;
          // 将该observation作为头节点插入
          GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
      } else { // 有数据,将obsevation直接插在原链表的后面
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
 } else {
 	  // 既没有name又没有object,就加在WILDCARD链表中
      o->next = WILDCARD;
      WILDCARD = o;
 }
  // 解锁
  unlockNCTable(TABLE);
}

流程总结:

  • 1.首先会根据传入的参数实例化一个 Observation,Observation 对象保存了观察者对象,接收到通知观察者所执行的方法,以及下一个 Observation 对象的地址。
  • 2.根据是否传入 NotificationName 选择操作 Named Table 还是 Nameless Table。
  • 3.若传入了 NotificationName,则会以 NotificationName 为 key 去查找对应的 Value,若找到 value,则取出对应的 value;若未找到对应的 value,则新建一个 table,然后将这个 table 以 NotificationName 为 key 添加到 Named Table 中。
  • 4.若在保存 Observation 的 table 中,以 object 为 key 取对应的链表。若找到了则直接在链接末尾插入之前实例化好的 Observation;若未找到则以之前实例化好的 Observation 对象作为头节点插入进去。
  • 5.若既没有 NotificationName 也没有 object,那么就加在 WILDCARD 链表中

发送通知

使用方法postNotification:, postNotificationName:object:userInfo或者postNotificationName:object:发送通知,后者默认userInfo为nil,同样使用GNUStep源码进行分析:

- (void) postNotification: (NSNotification*)notification {
  if (notification == nil) {
      [NSException raise: NSInvalidArgumentException
                  format: @"Tried to post a nil notification."];
    }
  [self _postAndRelease: RETAIN(notification)];
}

- (void) postNotificationName: (NSString*)name
                       object: (id)object {
  [self postNotificationName: name object: object userInfo: nil];
}


- (void) postNotificationName: (NSString*)name
                       object: (id)object
                     userInfo: (NSDictionary*)info {
  GSNotification        *notification;

  notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
  notification->_name = [name copyWithZone: [self zone]];
  notification->_object = [object retain];
  notification->_info = [info retain];
  [self _postAndRelease: notification];
}

我们发现,经典的源码书写,最终都只会调用 _postAndRelease:方法。不同的是,postNotification:方法外部直接传了一个NSNotification对象,其他两个方法都是内部进行了处理包装 成为了一个NSNotification对象,我们再看看_postAndRelease:方法做了什么:

- (void) _postAndRelease: (NSNotification*)notification {
  Observation        *o;
  unsigned        count;
  NSString        *name = [notification name];
  id                object;
  GSIMapNode        n;
  GSIMapTable        m;
  GSIArrayItem        i[64];
  GSIArray_t        b;
  GSIArray        a = &b;
   // name为空的报错,注册时可以注册无名,注册无名就等于说是所有的通知都能接收,但是发送通知时不可以
  if (name == nil) {
      RELEASE(notification);
      [NSException raise: NSInvalidArgumentException
                  format: @"Tried to post a notification with no name."];
    }
  object = [notification object];

  GSIArrayInitWithZoneAndStaticCapacity(a, _zone, 64, i);
  lockNCTable(TABLE);
  // 查找所有未指定name或object的观察者,加在a数组中,即将wildcard表中的数据都加在新建链表中
  for (o = WILDCARD = purgeCollected(WILDCARD); o != ENDOBS; o = o->next)
    {
      GSIArrayAddItem(a, (GSIArrayItem)o);
    }
  // 查找与通知的object相同但是没有name的观察者,加在a数组中
  if (object) {
  	  // 在nameless中找object对应的数据节点
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      if (n != 0) { // 将其加入到新建链表中
          o = purgeCollectedFromMapNode(NAMELESS, n);
          while (o != ENDOBS) {
              GSIArrayAddItem(a, (GSIArrayItem)o);
              o = o->next;
            }
        }
    }

  // 查找name的观察者,但观察者的非零对象与通知的object不匹配时除外,加在a数组中
  if (name) {
  	  // 先匹配name
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
      if (n) { // m指向name匹配到的数据
          m = (GSIMapTable)n->value.ptr;
      } else {
          m = 0;
      }
      if (m != 0) { // 如果上述name查找到了数据
          // 首先,查找与通知的object相同的观察者
          n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
          if (n != 0) { // 找到了与通知的object相同的观察者,就加入到新建链表中
              o = purgeCollectedFromMapNode(m, n);
              while (o != ENDOBS) {
                  GSIArrayAddItem(a, (GSIArrayItem)o);
                  o = o->next;
                }
            }

          if (object != nil) {
          // 接着是没有object的观察者,都加在新建链表中
              n = GSIMapNodeForSimpleKey(m, (GSIMapKey)nil);
              if (n != 0) { // 如果没有object并且有数据,就把其中的数据加到新建链表中
                  o = purgeCollectedFromMapNode(m, n);
                  while (o != ENDOBS) {
                      GSIArrayAddItem(a, (GSIArrayItem)o);
                      o = o->next;
                    }
                }
            }
        }
    }
  unlockNCTable(TABLE);

  // 发送通知,给之前新建链表中的所有数据
  count = GSIArrayCount(a);
  while (count-- > 0) {
      o = GSIArrayItemAtIndex(a, count).ext;
      if (o->next != 0) {
          NS_DURING {
              // 给observer发送selector,让其处理
              [o->observer performSelector: o->selector
                                withObject: notification];
            }
          NS_HANDLER {
              BOOL        logged;
              // 尝试将通知与异常一起报告,但是如果通知本身有问题,我们只记录异常。
              NS_DURING
                NSLog(@"Problem posting %@: %@", notification, localException);
                logged = YES;
              NS_HANDLER
                logged = NO;
              NS_ENDHANDLER
                if (NO == logged)
                { 
                  NSLog(@"Problem posting notification: %@", localException);
                }  
            }
          NS_ENDHANDLER
        }
    }
  lockNCTable(TABLE);
  GSIArrayEmpty(a);
  unlockNCTable(TABLE);

  RELEASE(notification);
}

流程总结:

  • 1.首先会创建一个数组 observerArray 用来保存需要通知的 observer。
  • 2.遍历 wildcard 链表,将 observer 添加到 observerArray 数组中。
  • 3.若存在 object,在 nameless table 中找到以 object 为 key 的链表,然后遍历找到的链表,将 observer 添加到 observerArray 数组中。
  • 4.若存在 NotificationName,在 named table 中以 NotificationName 为 key 找到对应的 table,然后再在找到的 table 中以 object 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。如果 object 不 为nil,则以 nil 为 key 找到对应的链表,遍历链表,将 observer 添加到 observerArray 数组中。
  • 5.至此所有关于当前通知的 observer(wildcard + nameless + named)都已经加入到了数组 observerArray 中。遍历 observerArray 数组,取出其中 的observer 节点(包含了观察者对象和 selector)其中调用观察者的方法,调用形式如下
[o->observer performSelector: o->selector withObject: notification];

移除通知

GNUStep源码:

- (void) removeObserver: (id)observer {
  if (observer == nil)
    return;

  [self removeObserver: observer name: nil object: nil];
}

- (void) removeObserver: (id)observer
                   name: (NSString*)name
                 object: (id)object {
  // 当其要移除的信息都为空时,直接返回
  if (name == nil && object == nil && observer == nil)
      return;

  lockNCTable(TABLE);
  // name和object都为nil,就在wildcard链表里删除对应observer的注册信息
  if (name == nil && object == nil) {
      WILDCARD = listPurge(WILDCARD, observer);
    }
  // name为空时
  if (name == nil) {
      GSIMapEnumerator_t        e0;
      GSIMapNode                n0;
      // 首先尝试删除为此object对应的所有命名项目
      // 在named表中
      e0 = GSIMapEnumeratorForMap(NAMED);
      n0 = GSIMapEnumeratorNextNode(&e0);
      while (n0 != 0) {
          GSIMapTable                m = (GSIMapTable)n0->value.ptr;
          NSString                *thisName = (NSString*)n0->key.obj;

          n0 = GSIMapEnumeratorNextNode(&e0);
          if (object == nil) { // 如果object为空,直接清除named表
          	  // 清空named表
              GSIMapEnumerator_t        e1 = GSIMapEnumeratorForMap(m);
              GSIMapNode                n1 = GSIMapEnumeratorNextNode(&e1);

              while (n1 != 0) {
                  GSIMapNode        next = GSIMapEnumeratorNextNode(&e1);

                  purgeMapNode(m, n1, observer);
                  n1 = next;
                }
          } else {
          // 以object为key找到对应链表,清空该链表
              GSIMapNode        n1;
              n1 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
              if (n1 != 0) {
                  purgeMapNode(m, n1, observer);
                }
            }
          if (m->nodeCount == 0) {
              mapFree(TABLE, m);
              GSIMapRemoveKey(NAMED, (GSIMapKey)(id)thisName);
            }
        }
      // 开始操作nameless表
      if (object == nil) { // object为空时
      	  // 清空nameless表
          e0 = GSIMapEnumeratorForMap(NAMELESS);
          n0 = GSIMapEnumeratorNextNode(&e0);
          while (n0 != 0) {
              GSIMapNode        next = GSIMapEnumeratorNextNode(&e0);

              purgeMapNode(NAMELESS, n0, observer);
              n0 = next;
            }
        } else { // object不为空
          // 找到对应的observer链表,清空该链表
          n0 = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
          if (n0 != 0) {
              purgeMapNode(NAMELESS, n0, observer);
            }
        }
   } else { // name不为空
      GSIMapTable                m;
      GSIMapEnumerator_t        e0;
      GSIMapNode                n0;

      n0 = GSIMapNodeForKey(NAMED, (GSIMapKey)((id)name));
      // 如果没有和这个name相同的key,直接返回
      if (n0 == 0) {
          unlockNCTable(TABLE);
          return;                /* Nothing to do.        */
      }
      m = (GSIMapTable)n0->value.ptr; // 找到name作为key对应的数据信息

      if (object == nil) {
      // 如果object为nil,就清空刚才找到的name对应的数据信息
          e0 = GSIMapEnumeratorForMap(m);
          n0 = GSIMapEnumeratorNextNode(&e0);

          while (n0 != 0) {
              GSIMapNode        next = GSIMapEnumeratorNextNode(&e0);

              purgeMapNode(m, n0, observer);
              n0 = next;
            }
      } else {
      // 如果object不为空,清空object对应的链表
          n0 = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
          if (n0 != 0) {
              purgeMapNode(m, n0, observer);
            }
        }
      // 因为其中的数据清除完了,所以记得清除named表中的作为key的name
      if (m->nodeCount == 0) {
          mapFree(TABLE, m);
          GSIMapRemoveKey(NAMED, (GSIMapKey)((id)name));
        }
    }
  unlockNCTable(TABLE);
}

流程总结:

  • 1.若 NotificationName 和 object 都为 nil,则清空 wildcard 链表。
  • 2.若 NotificationName 为 nil,遍历 named table,若 object 为 nil,则清空 named table,若 object 不为 nil,则以 object 为 key 找到对应的链表,然后清空链表。在 nameless table 中以 object 为 key 找到对应的 observer 链表,然后清空,若 object 也为 nil,则清空 nameless table。
  • 3.若 NotificationName 不为nil,在 named table 中以 NotificationName 为 key 找到对应的 table,若 object 为 nil,则清空找到的 table,若 object 不为 nil,则以 object 为 key 在找到的 table 中取出对应的链表,然后清空链表。

一些问题

通知的发送时同步的,还是异步的?发送消息与接收消息的线程是同一个线程么?
通知中心发送通知给观察者是同步的,也可以用通知队列(NSNotificationQueue)异步发送通知。

在抛出通知以后,观察者在通知事件处理完成以后(可以通过休眠3秒来测试),抛出者才会往下继续执行,也就是说这个过程默认是同步的;当发送通知时,通知中心会一直等待所有的 observer 都收到并且处理了通知才会返回到 poster。

接收通知的线程,和发送通知所处的线程是同一个线程。也就是说如果要在接收通知的时候更新 UI,需要注意发送通知的线程是否为主线程。

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{    // 异步执行 + 串行队列
        NSLog(@"--current thread: %@", [NSThread currentThread]);
        NSLog(@"Begin post notification");
        [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
        NSLog(@"End");
    });
}

- (void)test {
    NSLog(@"--current thread: %@", [NSThread currentThread]);
    NSLog(@"Handle notification and sleep 3s");
    sleep(3);
}

如何使用异步发送通知?
1.让通知事件处理方法在子线程中执行:

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"--current thread: %@", [NSThread currentThread]);
    NSLog(@"Begin post notification");
    [[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
    NSLog(@"End");
}

- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{    // 异步执行 + 串行队列
        NSLog(@"--current thread: %@", [NSThread currentThread]);
        NSLog(@"Handle notification and sleep 3s");
        sleep(3);
    });
}

2.可以通过 NSNotificationQueueenqueueNotification: postingStyle: enqueueNotification: postingStyle: coalesceMask: forModes: 方法将通告放入队列,实现异步发送,在把通告放入队列之后,这些方法会立即将控制权返回给调用对象。

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:@"NotificationName" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"--current thread: %@", [NSThread currentThread]);
    NSLog(@"Begin post notification");
    NSNotification *notification = [NSNotification notificationWithName:@"NotificationName" object:nil];
    [[NSNotificationQueue defaultQueue] enqueueNotification:notification postingStyle:NSPostASAP];
    NSLog(@"End");
}

- (void)test {
    NSLog(@"--current thread: %@", [NSThread currentThread]);
    NSLog(@"Handle notification and sleep 3s");
    sleep(3);
}

NSNotificationQueue 和 runloop 的关系?

postringStyle 参数就是定义通知调用和 runloop 状态之间关系。
该参数的三个可选参数:

  • 1.NSPostWhenIdle:通知回调方法是等待到当下线程 runloop 进入等待状态才会调用。
  • 2.NSPostASAP:通知回调方法是等待到当下线程 runloop 开始接收事件源的时候就会调用。
  • 3.NSPostNow:其实和直接用默认的通知中心添加通知是一样的,通知马上调用回调方法

页面销毁时不移除通知会崩溃吗?

  • 在观察者对象释放之前,需要调用removeOberver方法将观察者从通知中心移除,否则程序可能会出现崩溃。但从 iOS9 开始,即使不移除观察者对象,程序也不会出现异常。
  • 这是因为在 iOS9 以后,通知中心持有的观察者由 unsafe_unretained 引用变为weak引用。即使不对观察者手动移除,持有的观察者的引用也会在观察者被回收后自动置空。但是通过 addObserverForName:object: queue:usingBlock: 方法注册的观察者需要手动释放,因为通知中心持有的是它们的强引用。
    多次添加同一个通知会是什么结果?多次移除通知呢?
  • 多次添加同一个通知,观察者方法会调用多次
  • 多次移除,没关系。应该和源码有关系,我的理解是,它在表中找不到,同时源码也没写这样会报错,那就自然没有关系。

下面的方法不会接收到通知?

// 添加观察
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 通知发送
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];

不会,上文介绍 NSNotificationCenter 时介绍了 center 的结构。

  • 注册通知在添加observer时,路径为 TestNotification -> @1 -> self
  • 发送通知在查找observer时,路径为 TestNotification -> nil -> observer list

object是干嘛的?是不是可以用来传值?
object是用来过滤Notification的,只接收指定的sender所发的Notification,传值请用userInfo,而不是object。

单例模式

什么是单例模式

单例模式在整个工程中,相当于一个全局变量,就是不论在哪里需要用到这个类的实例变量,都可以通过单例方法来取得,而且一旦你创建了一个单例类,不论你在多少个界面中初始化调用了这个单例方法取得对象,它们所有的对象都是指向的同一块内存的存储空间(即单例类保证了该类的实例对象是唯一存在的一个)。

系统为我们提供的单例类有:

UIApplication(应用程序实例类)
NSNotificationCenter(消息中心类)
NSFileManager(文件管理类)
NSUserDefaults(应用程序设置)
NSURLCache(请求缓存类)
NSHTTPCookieStorage(应用程序cookies池)

单例模式的优缺点

优点:

  • 一个类只被实例化一次,提供了对唯一实例的受控访问。
  • 节省系统资源。
  • 允许可变数目的实例

缺点:

  • 一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

单例的实现

单例的实现分为两种:懒汉式和饿汉式。

  • 懒汉式:顾名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。
  • 饿汉式:饿了肯定会饥不择食,所以在单例类加载的时候就进行实例化。

特点和选择:

  • 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能,这是以空间换时间。
  • 在访问量较小时,采用懒汉实现,这是以时间换空间。

3.1 懒汉式

单线程下实现单例:

static id manager = nil;
+ (instancetype)shareInstance {
    if (!manager) {
        manager = [[super allocWithZone:NULL] init];
    }
    return manager;
}

以上代码在单线程下不会出问题,但是如果在多线程下,可能会出现多个线程共同进入 if 条件中,创建出多个对象,所以为了防止此类问题的发生,我们选择给他上锁,保证每次访问该代码都只能一个线程进行访问:

static id manager = nil;
+ (instancetype)shareInstance {
    @synchronized (self) {
        if (!manager) {
            manager = [[super allocWithZone:NULL] init];
        }
    }
    return manager;
}

添加完@synchronized锁之后确实可以实现单线程访问该代码了,但是这样的代码无论我们有没有创建该类的单例对象,他都会进来执行一次锁,就很没有必要,我们创建过了这个对象之后就不用进来了,那么我们在对其进行改造:

static id manager = nil;
+ (instancetype)shareInstance {
	// 防止多次加锁
    if (!manager) {
        @synchronized (self) {
            if (!manager) {
                manager = [[super allocWithZone:NULL] init];
            }
        }
    }
    return manager;
}

这样写也可以达到目的,但是相比GCD给出的轻量级的dispatch_once来说,还是很繁琐,dispatch_once它没有使用重量级的同步机制,性能也优于前者,并且更加高效,所以,单例推荐写法如下:

static id manager = nil;

+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[super allocWithZone:NULL] init];
    });
    return manager;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return [LazyType shareInstance];
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    return [LazyType shareInstance];
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return [LazyType shareInstance];
}

dispatch_once无论使用多线程还是单线程,都只执行一次,在安全的前提下也保证了性能。
dispatch_once主要是根据onceToken的值来决定怎么执行代码:

  • onceToken为0时,线程执行dispatch_onceblock中的代码
  • onceToken为-1时,线程跳过dispatch_onceblock中的代码
  • onceToken为其他值时,线程被阻塞,等待onceToken值改变

dispatch_once的执行流程:

  • 当线程调用shareInstance,此时onceToken为0,执行dispatch_onceblock中的代码,此时onceToken中的值为其他值
  • 这时如果有其他线程再调用shareInstance方法时,onceToken值为其他值,线程阻塞
  • block线程执行完block后,onceToken变为-1。其他线程不再阻塞,跳过block
  • 下次再调用shareInstance时,onceToken为-1,直接跳过block

饿汉式

当类被加载的时候就创建,因为一个类在整个生命周期中只会被加载一次,所以它肯定只有一个线程对其进行访问,此时再创建他就是线程安全的,就不需要使用线程锁来保证其不会被多次创建。

static id manager = nil;

+ (void)load {
    [super load];
    manager = [[super allocWithZone:NULL] init];
}

+ (instancetype)shareInstance {
    return manager;
}

- (instancetype)copyWithZone:(NSZone *)zone {
    return manager;
}

- (instancetype)mutableCopyWithZone:(NSZone *)zone {
    return manager;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    return manager;
}

关于复写

我们在写单例的时候一定要记着复写该类的其他方法,例如alloc、copy、mutablecopy、new等,但一般不复写alloc方法,就像上面我书写的代码一样。

因为这些方法都可以创建一个全新的该单例对象,但是根据单例的定义,我们只能允许存在一个该单例对象,所以我们要扼制其创建出新的该单例类的对象,保证对象的唯一性。

关于XXXWithZone:

我们重写这些XXXWithZone:都是为了使我们的单例返回的是同一个对象,但是为甚么非要重写这些方法,重写alloc、copy、mutableCopy方法不行吗?

答案肯定是不行的,为了保持单例类实例的唯一性,需要覆盖所有会生成新的实例的方法,如果我们只重写了alloc、copy、mutableCopy方法,那要是有人初始化这个单例类的时候不走[[Class alloc] init],而是直接allocWithZone:方法,那么这个单例就不再是单例了,其他的copy、mutableCopy方法原因也相同,所以我们必须把这个方法也堵上。

代理

代理我们一般用于相邻两个界面之间的回传值,用起来很顺手,下面我就来说说:

代理传值的使用步骤:

我们的使用场景如下,现在有两个视图控制器A和B,我们现在想要在视图B中回传一个值给视图A,同时改变A视图中UILabel显示的字符串,B中我们使用UITextField来实现数据的输入,传回的就是这个UITextField中的值:
第一步:在B视图控制器声明一份协议

@protocol MyViewControllerDelegate <NSObject>
- (void)changeUILabelText:(NSString *)string;
@end

第二步:在B视图控制器中声明一个代理属性

@property (nonatomic, weak) id<MyViewControllerDelegate> delegate;

第三步:在B视图控制器中想要回传值的地方写代理执行的方法

[self.delegate changeUILabelText:self.myTextField.text];

第四步:在A视图控制器里签订代理协议

<MyViewControllerDelegate>

第五步:A中签订代理人

self.myView = [[MyViewController alloc] init];
self.myView.delegate = self;

第六步:A中实现代理方法

- (void)changeUILabelText:(NSString *)string {
    self.myLabel.text = [string copy];
}

代理传值的原理

代理传值其实就是利用了可以在遵守代理协议的视图中实现代理方法的原理,上述例子中,我们自己先定义了一个代理协议,然后在B中定义一个代理对象delegate,同时在需要传值的地方实现了代理对象delegate对代理方法的调用,好了,关键来了,这个代理对象delegate其实就是代理传值的核心,因为我们在A视图中创建B视图的时候有行这样的代码:

self.myView.delegate = self;

在此之前,B视图已经初始化创建出来了,同时你将A视图自身的地址赋值给了B视图中的delegate属性,那么此时B视图中的delegate属性是不是就等同于是A视图了,并且该delegate还遵循了代理协议<MyViewControllerDelegate>,那不就是等同于遵循该代理协议的A视图嘛,那不就有了self.delegate == 遵循<MyViewControllerDelegate>协议的A视图控制器这不就是完全成立的吗,现在我们在看在B视图中写的回传值的方法:

[self.delegate changeUILabelText:self.myTextField.text];

这不就纯纯是A视图在进行该方法的调用吗?到此想必大家都已经想明白了代理传值的原理了。

综上所述,代理传值就是将一个视图的地址传递给另一个视图的delegate属性,使用该属性进行操作其实就是使用那个视图在进行操作

代理的循环引用

上述我们说了代理传值的原理,我们不难发现其中的delegate属性是weak进行修饰的,为什么使用weak进行修饰,下面我们就来讲讲这其中的道理:

上面说了,发送方的delegate等同于接收方,其实就是它引用了接收方视图,并且我们在接收方中定义发送方视图的时候,接收方这不又引用了发送方吗?两个视图之间互相引用,这不就是典型的循环引用案例,所以我们为了打破这个循环引用的问题,所以才在发送方的delegate使用weak进行修饰,即发送方delegate属性弱引用接收方,接收方强引用发送方,这不就打破了循环引用。

因为 weak 不会使对象的引用计数增加,如果使用它来修饰发送方,我们不知道它会在什么时候将发送方对象置 nil 了,程序说不定还会 crash,但是使用其修饰 delegate 就不一样了,所以我们使用 weak 来修饰 delegate。

KVO\KVC\单例模式\通知\代理\Block

代理和通知的区别

  • 效率:代理比通知高;
  • 关联:代理是强关联,委托和代理双方互相知道。通知是弱关联,不需要知道是谁发,也不需要知道是谁接收;
  • 代理是一对一的关系,通知是一对多的关系;代理要实现对多个类发出消息可以通过将代理者添加入集合类后遍历,或通过消息转发来实现。
  • 代理一般行为需要别人来完成,通知是全局通知;

KVO和通知的区别

  • 相同:都是一对多的关系;
  • 不同:通知是需要被观察者先主动发出通知,观察者注册监听再响应,比KVO多了发送通知这一步;
  • 监听范围:KVO是监听一个值的变化,通知不局限于监听属性的变化,还可以对多种多样的状态变化进行监听,通知的监听范围广,使用更灵活;
  • 使用场景:KVO的一般使用场景是监听数据变化,通知是全局通知;

block和代理的区别

相同点:

  • block和代理都是回调的方式。使用场景相同。

不同点:

  • block集中代码块,而代理分散代码块,所以 block 更适用于轻便、简单的回调,如网络传输,代理适用于公共接口较多的情况,这样做也更易于解耦代码架构;
  • block运行成本高,block出栈时,需要将使用的数据从栈内存拷贝到堆内存。当然如果是对象就是加计数,使用完或block置为 nil 后才消除,而代理只是保存了一个对象指针,直接回调,并没有额外消耗,相对C的函数指针,只是多做了一个查表动作;

在这里插入图片描述

设计模式总结

KVO/通知 -------> 观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
优势:解耦合
接口隔离原则、开放-封闭原则

KVC --------> KVC模式
单例模式

利用应用程序只有一个该类的实例对象这一特殊性来实现资源共享。
优势:使用简单,延时求值,易于跨模块
劣势:这块内存知道程序退出时才能释放
单一职责原则
举例:[UIApplication sharedApplication]。

代理模式

委托方将不想完成的任务交给代理方处理,并且需要委托方通知代理方才能处理。
优势: 解耦合
开放-封闭原则
举例:tableview的数据源和代理

策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
优势:使算法的变化独立于使用算法的用户
接口隔离原则、多用组合,少用继承、针对接口编程,而非实现
举例:账号密码输入格式的判断、NSArray的sortedArrayUsingSelector等等

MVC模式

将程序书写分为三层,分别为模型、视图、控制器,每层都有各自的职责完成各自的工作。
优势: MVC模式使系统,层次清晰,职责分明,易于维护
对扩展开放-对修改封闭

MVVM模式

用于解决MVC模式下C层代码冗杂的情况(过多的网络请求以及业务逻辑处理)而出现的MVVM模式,其相比于MVC多了一层ViweModel(业务处理和数据转化)层,专门用于处理数据。
当功能简单时,MVVM反而会增加很多代码,所以对于简单的功能,MVC更加的方便。

MVP模式

MVP是MVC模式派生出来的,常用于创建用户界面,在MVP中所有页面显示逻辑都会被推送到presenter(中间人)中,它主要实现程序逻辑控制、数据的检索,并格式化数据以便在视图中显示,把Model和View完全的进行分离,MVP模式的V是View + ViewController。

三种工厂模式

通过给定参数来返回对应的实例,完全对用户隐藏其实现的原理。
优势:易于替换,面向抽象编程
依赖倒置原则

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

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

相关文章

管理类联考——数学——记忆篇——不同角度解读——四、数据分析——1.计数原理/排列组合

计数原理 1.计数原理 1.1 加法原理、乘法原理 1.2 排列与排列数 1.3 组合与组合数 PS&#xff1a;图标说明 ⛲️&#xff1a;陈jian &#x1f475;&#xff1a;鑫quan &#x1f469;&#xff1a;张紫chao &#x1f482;&#xff1a;MBA大shi &#x1f63d;&#xff1a;海mian…

小程序UV:衡量用户规模与活跃度的重要指标

什么是UV UV是Unique Visitor&#xff08;独立访客&#xff09;的缩写&#xff0c;指的是在特定时间段内访问某个网站、应用或平台的独立用户数量。UV是根据设备、IP地址、Cookie等来识别不同的用户&#xff0c;对于相同的用户多次访问&#xff0c;只计算为一个UV。UV是衡量网…

linux判断端口是否占用(好用)

netstat 一般的话使用 netstat -tunlp | grep xxx参数作用-t指明显示TCP端口-u指明显示UDP端口-l仅显示监听套接字(所谓套接字就是使应用程序能够读写与收发通讯协议(protocol)与资料的程序)-p显示进程标识符和程序名称&#xff0c;每一个套接字/端口都属于一个程序。-n不进行…

随机数检测(五)

随机数检测&#xff08;五&#xff09;- 检测工具 1 检测规范2 检测量3 检测项目4 检测工具 1 检测规范 随机数检测应遵循GM/T 0005-2021和GM/T 0062-2018两个标准。 首先根据产品实际情况确定产品类型。 随机数检测量和检测项目可参考GM/T 0062-2018对不同类型产品的要求。不…

C++类与对象 - 2(构造函数和析构函数)(超详细)

构造函数和析构函数 - 超详细讲解 1. 构造函数1.1 概念1.2特性 2. 析构函数2.1 概念2.2特性 1. 构造函数 1.1 概念 对于以下Date类&#xff1a; class Date { public:void Init(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout <&l…

7p透明屏的制造过程复杂琐屑吗?

7p透明屏是一种新型的显示技术&#xff0c;它可以使屏幕变得透明&#xff0c;让用户可以透过屏幕看到背后的物体。这种技术在科幻电影中经常出现&#xff0c;但现在已经成为现实。 7p透明屏的工作原理是利用液晶显示技术和透明材料。液晶显示技术是一种通过控制液晶分子的排列…

Android 之 动画合集之帧动画

本节引言&#xff1a; 从本节开始我们来探究Android中的动画&#xff0c;毕竟在APP中添加上一些动画&#xff0c;会让我们的应用变得 很炫&#xff0c;比如最简单的关开Activity&#xff0c;当然自定义控件动画肯定必不可少啦~而Android中的动画 分为三大类&#xff0c;逐帧动画…

“xAI正式成立,GPT大战重燃,AI大模型的现状与发展怎么看?“

文章目录 每日一句正能量前言“反AI斗士”马斯克进军AI&#xff0c;你怎么看&#xff1f;回顾上半年的“百模大战”&#xff0c;中国的AI产业怎么样了&#xff1f;AI大模型这把火&#xff0c;还能怎么烧&#xff1f;后记 每日一句正能量 世界上最有力的一句话&#xff0c;不是给…

探索UCI心脏病数据:利用R语言和h2o深度学习构建预测模型

一、引言 随着机器学习模型在实际应用中的广泛应用&#xff0c;人们对于模型的解释性和可理解性日益关注。可解释性机器学习是指能够清晰、透明地解释机器学习模型决策过程的一种方法和技术。在许多领域中&#xff0c;如医疗诊断、金融风险评估和自动驾驶等&#xff0c;解释模型…

【沐风老师】3DMAX自动材质插件使用方法教程

3DMAX自动材质插件使用方法教程 3DMAX自动材质工具用于在将纹理加载到3dsax中时快速创建简单的材质&#xff0c;并具有一些很酷的材质功能。 这个插件可以根据真正制造商的纹理&#xff08;通常比例为2:1&#xff09;快速创建简单的木材材质&#xff0c;并根据板材的长度自动对…

<MySQL>建表SQ和CRUD SQ脚本案例二

1. MySQL 建表SQ脚本案例&#xff1a; 地域表 CREATE TABLE xxx_region_list_dic (seqId INT(11) NOT NULL AUTO_INCREMENT,sortId INT(11) DEFAULT NULL,name VARCHAR(255) NOT NULL COMMENT 地域,code VARCHAR(25) NOT NULL COMMENT 编码,isEnable VARCHAR(25) DEFAULT NULL…

图文教程:使用 Photoshop、3ds Max 和 After Effects 创建被风暴摧毁的小屋

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 1. 在 Photoshop 中设置图像 步骤 1 打开 Photoshop。 打开 Photoshop 步骤 2 我已经将小屋的图像导入到Photoshop中以演示 影响。如果您愿意&#xff0c;可以使用其他图像。 图片导入 步骤 3 由于小…

filfox 通过接口获取

通过filfox接口获取24小时出块奖励 ╰─➤ curl https://filfox.info/api/v1/address/f01889512/mining-stats\?duration\24h | jq .% Total % Received % Xferd Average Speed Time Time Time CurrentDload Upload Total Spent Left Speed 100 346…

Java面向对象的学习

由于类和对象这一部分的知识点&#xff0c;比较难&#xff0c;而且涉及的知识点很广、也很杂&#xff0c;在这里&#xff0c;单独作为一个知识点来学习。我会通过不同的视频&#xff0c;不同的资料&#xff0c;根据不同的特点来学这个方面的知识点。 一、理解什么是类和对象。 …

let definitions are not supported by current javascript——前端笔记

idea中报的错&#xff0c;解决办法就是改一下javascript在idea中的配置。 本文来自&#xff1a;https://blog.csdn.net/fgx_123456/article/details/77825499

Simulink仿真模块 - Demux

Demux:提取并输出虚拟向量信号的元素 在仿真库中的位置为:Simulink / Commonly Used Blocks Simulink / Signal Routing HDL Coder / Commonly Used Blocks HDL Coder / Signal Routing 模型为: 说明 Demux 模块提取输入向量信号的分量,再将它们输出为单独的信号。输出信号…

Java开发工具MyEclipse发布v2023.1.2,今年第二个修复版!

MyEclipse一次性提供了巨量的Eclipse插件库&#xff0c;无需学习任何新的开发语言和工具&#xff0c;便可在一体化的IDE下进行Java EE、Web和PhoneGap移动应用的开发&#xff1b;强大的智能代码补齐功能&#xff0c;让企业开发化繁为简。 MyEclipse v2023.1.2官方正式版下载 …

linux下用docker安装mysql

1.mysql Docker镜像 docker pull mysql:[版本号 或 latest]例&#xff1a;docker pull mysql:5.7 2.查看拉取的docker镜像 docker images3.设置 Docker 卷 docker volume create mysql-data列出 Docker 已知的所有卷 docker volume ls4.运行一个 MySQL Docker 容器 docke…

windows 安装 mongodb 数据库

软件下载 访问官方的下载地址&#xff1a; https://www.mongodb.com/try/download/community &#xff0c;然后选择对应的版本进行下载 下载好了之后双击进行安装 软件安装 1、点击 next 点击下一步 2、勾选接受协议&#xff0c;点击 next 3、第三页有两个选项&#x…

vue 项目中使用阿里巴巴矢量图标库

1.网址&#xff1a;https://www.iconfont.cn/ 2.手动创建自己的项目图标库 选中图标 → 添加入库&#xff08;点击购物车&#xff09;→ 完成后点击上方菜单栏的购物车 → 添加至项目&#xff08;没有则新建项目&#xff09;→ 自动打开项目图标库 → 点击下载至本地 → 点击de…