iOS——KVO底层学习

2024-06-12 04:52
文章标签 学习 底层 ios kvo

本文主要是介绍iOS——KVO底层学习,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前情回顾

什么是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
{
@publicunsigned recursion;               // 递归计数器,跟踪发送通知的递归深度。unsigned allOptions;              // 所有观察选项的位掩码,用于存储观察选项。NSMutableArray *observations;     // 观察者数组,存储所有与键路径相关的观察者。NSMutableDictionary *change;      // 变化字典,用于存储属性变化的信息。
}
@end
  • 它保存了一个keypath对应的所有观察者
  • observations保存了所有的观察者(GSKVOObservation 类型)
  • allOptions保存了观察者的options集合
  • change 保存了KVO触发要传递的内容

GSKVOObservation

/** 这个类的实例记录了单个观察的所有信息。*/
@interface GSKVOObservation : NSObject
{
@publicNSObject *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 == Tempname = [@"GSKVO" stringByAppendingString: superName];    // name = GSKVOTemptemplate = GSObjCMakeClass(name, superName, nil);   // template = GSKVOTempGSObjCAddClasses([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*)anObserverforKeyPath: (NSString*)aPathoptions: (NSKeyValueObservingOptions)optionscontext: (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: valueforKey: NSKeyValueChangeNewKey]; // 设置新值}[anObserver observeValueForKeyPath: aPathofObject: instancechange: pathInfo->changecontext: 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。)

这篇关于iOS——KVO底层学习的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1053253

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

线性代数|机器学习-P36在图中找聚类

文章目录 1. 常见图结构2. 谱聚类 感觉后面几节课的内容跨越太大,需要补充太多的知识点,教授讲得内容跨越较大,一般一节课的内容是书本上的一章节内容,所以看视频比较吃力,需要先预习课本内容后才能够很好的理解教授讲解的知识点。 1. 常见图结构 假设我们有如下图结构: Adjacency Matrix:行和列表示的是节点的位置,A[i,j]表示的第 i 个节点和第 j 个