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

通知

概要

  • 观察者和被观察者都无需知晓对方,只需要通过标记在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)observerselector: (SEL)selectorname: (NSString*)nameobject: (id)object
{Observation        *list;Observation        *o;GSIMapTable        m;GSIMapNode        n;
// observer为空时的报错if (observer == nil)[NSException raise: NSInvalidArgumentExceptionformat: @"Nil observer passed to addObserver ..."];
// selector为空时的报错if (selector == 0)[NSException raise: NSInvalidArgumentExceptionformat: @"Null selector passed to addObserver ..."];
// observer不能响应selector时的报错if ([observer respondsToSelector: selector] == NO){[NSException raise: NSInvalidArgumentExceptionformat: @"[%@-%@] Observer '%@' does not respond to selector '%@'",NSStringFromClass([self class]), NSStringFromSelector(_cmd),observer, NSStringFromSelector(selector)];}
// 给表上锁lockNCTable(TABLE);
// 建立一个新Observation,存储这次注册的信息o = obsNew(TABLE, selector, observer);// 如果有nameif (name) {// 在named表中 以name为key寻找valuen = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);// named表中没有找到对应的valueif (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// 取出对应的valuem = (GSIMapTable)n->value.ptr;}// 将observation添加到正确object的列表中// 获取添加完后name对应的value的object对应的链表n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);// n是object的valueif (n == 0) { // 如果object对应value没有数据o->next = ENDOBS;// 将o作为object的value链表的头结点插入GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);} else { // 如果有object对应的value那么就直接添加到原练表的尾部// 在链表尾部加入olist = (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: NSInvalidArgumentExceptionformat: @"Tried to post a nil notification."];}[self _postAndRelease: RETAIN(notification)];
}- (void) postNotificationName: (NSString*)nameobject: (id)object {[self postNotificationName: name object: object userInfo: nil];
}- (void) postNotificationName: (NSString*)nameobject: (id)objectuserInfo: (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: NSInvalidArgumentExceptionformat: @"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) {// 先匹配namen = 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->selectorwithObject: notification];}NS_HANDLER {BOOL        logged;// 尝试将通知与异常一起报告,但是如果通知本身有问题,我们只记录异常。NS_DURINGNSLog(@"Problem posting %@: %@", notification, localException);logged = YES;NS_HANDLERlogged = NO;NS_ENDHANDLERif (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)observername: (NSString*)nameobject: (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的nameif (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.mzph.cn/news/13986.shtml

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

相关文章

最后的组合:K8s 1.24 基于 Hekiti 实现 GlusterFS 动态存储管理实践

前言 知识点 定级&#xff1a;入门级GlusterFS 和 Heketi 简介GlusterFS 安装部署Heketi 安装部署Kubernetes 命令行对接 GlusterFS 实战服务器配置(架构 1:1 复刻小规模生产环境&#xff0c;配置略有不同) 主机名IPCPU内存系统盘数据盘用途ks-master-0192.168.9.912450100…

【Java面试丨消息中间件】Kafka

一、kafka是如何保证消息不丢失 1. 介绍 使用kafka在消息的收发过程都有可能会出现消息丢失 &#xff08;1&#xff09;生产者发送消息到broker丢失 &#xff08;2&#xff09;消息在broker中存储丢失 &#xff08;3&#xff09;消费者从broker接收消息丢失 2. 生产者发送消…

小程序picker 在苹果手机不兼容 bug,按month时在iPhone 显示不正确及自动定位时间问题

如下图&#xff1a;点击弹出时间列表&#xff1a;日历控件点击选择显示1年1月 解决: 加上起始时间字段 <picker mode"date" value"{{date}}" start"1970-09-01" end"2030-09-01"></picker> 问题二&#xff1a; 还是&a…

【MySQL】事务与隔离级别详解

【MySQL】事务 事务的概念为什么要有事务引擎对事务的支持事务的提交方式事务的操作准备正常事务操作&#xff1a;启动、回滚、提交非正常事务总结 事务的隔离级别隔离性隔离级别隔离级别的查看设置隔离级别会话级别全局级别 隔离级别的具体体现读未提交 Read Uncommitted读提交…

数学建模学习(6):数学建模数据预处理专题

1 数据预处理是什么&#xff1f; 在数学建模赛题中&#xff0c;官方给所有参赛选手的数据可能受到主 观或客观条件的影响有一定的问题&#xff0c;如果不进行数据的处理而直 接使用的话可能对最终的结果造成一定的影响&#xff0c;因此为了保证数 据的真实性和建模结果的可靠…

回归预测 | MATLAB实现PSO-GPR粒子群优化高斯过程回归多输入单输出回归预测

回归预测 | MATLAB实现PSO-GPR粒子群优化高斯过程回归多输入单输出回归预测 目录 回归预测 | MATLAB实现PSO-GPR粒子群优化高斯过程回归多输入单输出回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab基于PSO-GPR基于粒子群算法优化高斯过程回归的数据回归预…

建设银行秋招指南,备考技巧和考试内容详解

建设银行秋招简介 银行作为非常吃香的岗位&#xff0c;每年都有不少同学通过投递简历&#xff0c;进入笔试&#xff0c;再到面试成功&#xff0c;成功到银行就职&#xff0c;也有相当一部分同学因为信息差&#xff0c;符合条件却没有报名。无法进入银行工作。 建设银行的秋招…

QT【day4】

chat_QT服务器端&#xff1a; //.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> //服务器类 #include<QTcpSocket> //客户端类 #include<QMessageBox> //对话框类 #include<QList> //链表容器 #inc…

Java面试准备篇:全面了解面试流程与常见问题

文章目录 1.1 Java面试概述1.2 面试流程和注意事项1.3 自我介绍及项目介绍1.4 常见面试问题 在现代职场中&#xff0c;面试是求职过程中至关重要的一环&#xff0c;特别是对于Java开发者而言。为了帮助广大Java开发者更好地应对面试&#xff0c;本文将提供一份全面的Java面试准…

【如何训练一个中英翻译模型】LSTM机器翻译模型部署之ncnn(python)(五)

系列文章 【如何训练一个中英翻译模型】LSTM机器翻译seq2seq字符编码&#xff08;一&#xff09; 【如何训练一个中英翻译模型】LSTM机器翻译模型训练与保存&#xff08;二&#xff09; 【如何训练一个中英翻译模型】LSTM机器翻译模型部署&#xff08;三&#xff09; 【如何训练…

flask的配置项

flask的配置项 为了使 Flask 应用程序正常运行&#xff0c;有多种配置选项需要考虑。下面是一些基本的 Flask 配置选项&#xff1a; DEBUG: 这个配置项决定 Flask 是否应该在调试模式下运行。如果这个值被设为 True&#xff0c;Flask 将会提供更详细的错误信息&#xff0c;并…

go 查询采购单设备事项V3

一、版本说明 本版本在整合上两次的功能基础上&#xff0c;引进ini配置文件的读取事项&#xff0c;快速读取本地配置文件&#xff0c;完成读取设置 第一版&#xff1a;实现了严格匹配模式的查找 https://blog.csdn.net/wtt234/article/details/131979385 第二版&#xff1a;实…

作为程序员,你很有必要了解一下IVX

一、IVX是什么 iVX 是一个“零代码”的可视化编程平台&#xff0c;拥有方便的在线集成开发环境&#xff0c;不需要下载开发环境&#xff0c;打开浏览器即可随时随地进行项目编辑。iVX 还拥有“一站式”的云资源&#xff0c;通过这一套一站式服务&#xff0c;iVX 可以实现一站式…

详解STM32的GPIO八种输入输出模式,GPIO各种输入输出的区别、初始化的步骤详解,看这文章就行了(超详细)

在STM32微控制器中&#xff0c;常见的输入输出(GPIO)模式有八种&#xff0c;分别是推挽输出、开漏输出、复用推挽输出、复用开漏输出、浮空输入、上拉输入、下拉输入和模拟输入。下面我将为你解释每种模式的特点和区别&#xff0c;并提供相应的示例代码。 文章目录 介绍区别初…

MySql002——关系型数据库基础知识

前言&#xff1a;因为本专栏学习的是MySQL&#xff0c;而MySQL是关系型数据库&#xff0c;所以这篇文章就来介绍一些关系型数据库基础知识&#xff0c;至于其他知识小伙伴们可以自行学习&#xff0c;同时不足之处也欢迎批评指正&#xff0c;谢谢~ 一、MySQL关系型数据库(RDBMS)…

从实践彻底掌握MySQL的主从复制

目录 一、本次所用结构如图---一主多从级联&#xff1a; 二、IP。 三、配置M1&#xff1a; 四、从库M1S1&#xff1a; 五、从库M2配置&#xff1a; 六、 从库M2S1&#xff1a; 一、本次所用结构如图--- 一主多从级联&#xff1a; 二、IP。这里M1S1和M1S2一样的&#xff0…

图技术在 LLM 下的应用:知识图谱驱动的大语言模型 Llama Index

LLM 如火如荼地发展了大半年&#xff0c;各类大模型和相关框架也逐步成型&#xff0c;可被大家应用到业务实际中。在这个过程中&#xff0c;我们可能会遇到一类问题是&#xff1a;现有的哪些数据&#xff0c;如何更好地与 LLM 对接上。像是大家都在用的知识图谱&#xff0c;现在…

查看maven发布时间的方法

查看maven发布时间的方法如下【 打开maven官网 选中Release Notes 即可查看对应版本的发布时间 】

【计算机网络】第 4 课 - 物理层

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、物理层的基本概念 2、物理层协议的主要任务 3、物理层任务 4、总结 1、物理层的基本概念 在计算机网络中&#xff0c;用来…

基于多场景的考虑虑热网网损的太阳能消纳能力评估研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…