iOS ------ 消息传递和消息转发

一,消息传递

在OC中,传递消息就是在对象上调用方法

相对于C语言的方法就“静态绑定”的函数,在编译器就决定了运行时所要调用的函数。在OC中,如果向某对象传递消息,就会使用动态绑定机制来决定需要调用那个方法。调用那个方法完全取决于运行期决定,甚至可以在程序运行时改变。编译时并不能确定方法有没有对应的实现,没有写方法的具体实现也不会报错。

给对象发送消息可以这样来写:

id returnValue = [someObject messageName:parameter];

本例中,someObject叫做“接收者”(receiver),messageName叫做“选择子”(selector)。选择子与参数合起来称作“消息”(message)。编译器将其转换为C语言函数调用objc_msgSend

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

这个函数将消息接受者,选择子和参数作为主要参数,其原型如下:

objc_msgSend(receiver, selector)                    // 不带参数
objc_msgSend(receiver, selector, arg1, arg2,...)    // 带参数

消息传递的关键在于objc_class结构体有两个关键的字段:

  • isa 指向父类的指针
  • methodLists 类的方法分发表(dispatch table)

其中对象的isa指针让对象可以访问类和类的继承链。

消息传递的过程:

  • 当消息传递给一个对象时,首先从运行时系统缓存objc_cache中进行查找。如果找到则执行,否则执行下面的步骤
  • objc_msgSend通过isa指针获取类的结构体,然后通过选择子作为“键”在方法分发表methodLists查找应该执行的方法,实际上查找的就是相应方法的IMP函数指针,Dispatch table 是一张SELIMP的对应表。也就是说方法编号SEL最后还要通过Dispatch table表找到对应的IMPIMP是一个函数指针,然后去执行这个方法
  • 如果未找到,通过isa找到父类并在父类的分发表中查找,一直沿着类的继承链找到NSObject类,一旦找到则传入相应的参数来执行方法的具体实现,并将该方法加入到本类的方法缓存objc_cache。如果一直未找到方法的实现那么消息发送阶段结束,进入动态解析阶段,解析到就结束。如果未解析到,则会进入消息转发流程。

在这里插入图片描述

消息传递分为三个阶段:

  • 消息发送阶段
  • 动态解析阶段
  • 消息转发阶段

方法查找的核心函数就是 _class_lookupMethodAndLoadCache3 函数,接下来重点分析 _class_lookupMethodAndLoadCache3 函数内的源码。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward 函数

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver)
{// initialize = YES , cache = NO , resolver = YESIMP imp = nil;bool triedResolver = NO;runtimeLock.assertUnlocked();// 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找, 因为在汇编语言中CacheLookup已经查找过// Optimistic cache lookupif (cache) {imp = cache_getImp(cls, sel);if (imp) return imp;}// runtimeLock is held during isRealized and isInitialized checking// to prevent races against concurrent realization.// runtimeLock is held during method search to make// method-lookup + cache-fill atomic with respect to method addition.// Otherwise, a category could be added but ignored indefinitely because// the cache was re-filled with the old value after the cache flush on// behalf of the category.runtimeLock.lock();checkIsKnownClass(cls);if (!cls->isRealized()) {realizeClass(cls);}if (initialize  &&  !cls->isInitialized()) {runtimeLock.unlock();_class_initialize (_class_getNonMetaClass(cls, inst));runtimeLock.lock();// If sel == initialize, _class_initialize will send +initialize and // then the messenger will send +initialize again after this // procedure finishes. Of course, if this is not being called // from the messenger then it won't happen. 2778172}retry:    runtimeLock.assertLocked();// Try this class's cache.// 防止动态添加方法,缓存会变化,再次查找缓存。imp = cache_getImp(cls, sel);// 如果查找到imp, 直接调用done, 返回方法地址if (imp) goto done;// 查找方法列表, 传入类对象和方法名// Try this class's method lists.{// 根据sel去类对象里面查找方法Method meth = getMethodNoSuper_nolock(cls, sel);if (meth) {// 如果方法存在,则缓存方法log_and_fill_cache(cls, meth->imp, sel, inst, cls);// 方法缓存之后, 取出imp, 调用done返回impimp = meth->imp;goto done;}}// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法// Try superclass caches and method lists.{unsigned attempts = unreasonableClassCount();for (Class curClass = cls->superclass;curClass != nil;curClass = curClass->superclass){// Halt if there is a cycle in the superclass chain.if (--attempts == 0) {_objc_fatal("Memory corruption in class list.");}// 查找父类的缓存// Superclass cache.imp = cache_getImp(curClass, sel);if (imp) {if (imp != (IMP)_objc_msgForward_impcache) {// 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中// Found the method in a superclass. Cache it in this class.					log_and_fill_cache(cls, imp, sel, inst, curClass);goto done;}else {// Found a forward:: entry in a superclass.// Stop searching, but don't cache yet; call method // resolver for this class first.break;}}// 查找父类的方法列表// Superclass method list.Method meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {// 同样拿到方法, 在本类进行缓存log_and_fill_cache(cls, meth->imp, sel, inst, curClass);imp = meth->imp;goto done;}}}// ---------------- 消息发送阶段完成 ---------------------// ---------------- 进入动态解析阶段 ---------------------// 上述列表中都没有找到方法实现, 则尝试解析方法// No implementation found. Try method resolver once.if (resolver  &&  !triedResolver) {runtimeLock.unlock();_class_resolveMethod(cls, sel, inst);runtimeLock.lock();// Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead.triedResolver = YES;goto retry;}// ---------------- 动态解析阶段完成 ---------------------// ---------------- 进入消息转发阶段 ---------------------// No implementation found, and method resolver didn't help. // Use forwarding.imp = (IMP)_objc_msgForward_impcache;cache_fill(cls, sel, imp, inst);done:runtimeLock.unlock();return imp;
} 

方法缓存

在进行查找时,OC会运行时会利用缓存机制来提高查找的速度,在方法查找中,他会将最近使用过的方法实现存储在缓存中下次调用相同的方法就可以直接在缓存中获取实现,避免了反复查找的过程。

类缓存(objc_cache)

struct objc_cache {unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;unsigned int occupied                                    OBJC2_UNAVAILABLE;Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};
  • mask: 指定分配的缓存 bucket 的总数。,所以缓存的 size(total)是 mask+1。

  • occupied: 指定实际占用的缓存bucket的总数。

  • buckets: 指向 Method 数据结构指针的数组。

为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在 objc_cache 中,所以在实际运行中,大部分常用的方法都是会被缓存起来的。

SEL和IMP

IMP是OC方法实现代码块的地址,可以通过它像C语言函数一样直接调用方法实现。

typedef id (&IMP)(id,SEL,...);

IMP是一个函数指针,这个被指向的函数包含一个接搜消息的对象id(self指针),调用方法的选择子SEL(方法名),以及不定个数的方法参数,并返回一个id.

SEL是OC中表示方法名的数据类型,在运行时有编译器生成的唯一标识符用于在对象查找并调用相应的方法。

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

OC编译时会根据方法的名字包括参数序列,生成区分这个方法的唯一ID,不管是子类还是父类,只要方法的名字包括参数序列相同,它们的ID就相同。

二,消息转发

当一个对象能够接收一个消息时,会走完正常的消息传递过程。弱无法接受会发生什么呢?

  • 默认情况下,如果以[object message] 的形式调用方法,如果object无法响应message消息时,编译器会报错
  • 如果是以performselector 的形式调用方法,则需要等到运行时才能确定object是否能接受message消息,则程序崩溃

当不确定一个对象是否能接受某个消息时,可以调用respondsToSelector:来进行判断

if ([self respondsToSelector:@selector(method)]) {[self performSelector:@selector(method)];
}

当一个对象无法接受莫哥消息时,就会启动“消息转发”机制。通过学习转发机制可以告诉对象如何处理未知的消息。

消息转发机制分为三个阶段:

  • 动态方法解析
  • 备用接受者
  • 完整消息转发

在这里插入图片描述

1,动态方法解析

// No implementation found. Try method resolver once.
//未找到实现。尝试一次方法解析器if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}

如果没找到方法则尝试调用resolveMethod_locked动态解析,只会执行一次:

/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{lockdebug::assert_locked(&runtimeLock);ASSERT(cls->isRealized());runtimeLock.unlock();//判断是不是元类if (! cls->isMetaClass()) {// try [cls resolveInstanceMethod:sel]resolveInstanceMethod(inst, sel, cls);}else {// try [nonMetaClass resolveClassMethod:sel]// and [cls resolveInstanceMethod:sel]resolveClassMethod(inst, sel, cls);if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using itreturn lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

主要用的的方法如下

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//其中参数sel为未处理的方法

上述代码的大致流程:

  • 先检查进行解析的是否是元类
  • 如果不是元类,则调用resolveInstanceMethod:进行对象方法动态调用
  • 如果是元类,则调用resolveClassMethod:进行类方法动态解析,完成类方法动态解析后,再次查询cls中的imp,如果没有找到,则进行一次对象方法动态解析

两个方法resolveInstanceMethodresolveClassMethod则称为方法的动态决议。

执行完上述代码后返回lookUpImpOrForwardTryCache

IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{return _lookUpImpTryCache(inst, sel, cls, behavior);
}

这个方法调用的是_lookUpImpTryCache方法:

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{lockdebug::assert_unlocked(&runtimeLock);if (slowpath(!cls->isInitialized())) {// see comment in lookUpImpOrForwardreturn lookUpImpOrForward(inst, sel, cls, behavior);}IMP imp = cache_getImp(cls, sel);if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHESif (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);}
#endifif (slowpath(imp == NULL)) {return lookUpImpOrForward(inst, sel, cls, behavior);}done:if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {return nil;}return imp;
}

可以看到这里有cache_getImp;也就是说在进行一次动态决议之后,还会通过cache_getImp从cache里找一遍方法的sel。

#endifif (slowpath(imp == NULL)) {return lookUpImpOrForward(inst, sel, cls, behavior);}

如果还是没找到(imp == NULL),也就是无法通过动态添加方法的话,还会执行一次lookUpImpOrForward,这时候进lookUpImpOrForward方法,这里behavior传的值会发生变化。

第二次进入lookUpImpOrForward方法后,执行到if (slowpath(behavior & LOOKUP_RESOLVER))这个判断时

// 这里就是消息转发机制第一层的入口if (slowpath(behavior & LOOKUP_RESOLVER)) {behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}

根据变化后的behavior值和LOOKUP_RESOLVER值之间的关系导致该if语句内部只能进入第一次。解释了为什么开头说的该动态解析resolveMethod_locked为什么只执行一次次

具体实现

+(BOOL)resolveInstanceMethod:(SEL)sel {if ([NSStringFromSelector(sel) isEqualToString:@"instanceMethodTest:"]) {Method method = class_getInstanceMethod([self class], @selector(addDynamicInstanceMethod:));IMP methodIMP = method_getImplementation(method);const char * types = method_getTypeEncoding(method);class_addMethod([self class], sel, methodIMP, types);return YES;}return [super resolveInstanceMethod:sel];
}+(BOOL)resolveClassMethod:(SEL)sel {if ([NSStringFromSelector(sel) isEqualToString:@"classMethodTest:"]) {// 类方法都是存在元类中,所以添加方法需要往元类上添加Class metaClass = object_getClass([self class]);Method method = class_getClassMethod([self class], @selector(addDynamicClassMethod:));IMP methodIMP = method_getImplementation(method);const char * types = method_getTypeEncoding(method);class_addMethod(metaClass, sel, methodIMP, types);return YES;}return [super resolveClassMethod:sel];
}-(void)addDynamicInstanceMethod:(NSString *)value {NSLog(@"addDynamicInstanceMethod value = %@",value);
}+(void)addDynamicClassMethod:(NSString *)value {NSLog(@"addDynamicClassMethod value = %@",value);
}

2,备用接受者(快速转发)

当cache中没有找到imp,猎类的继承链里的方法列表都没有找到imp,并且resolve InstanceMethod / resolveClassMethod返回NO就进入快速消息转发。也就是本类没有能力去处理这个消息,那么就交给其他类去处理。

done:if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {return nil;}return imp;

从imp == (IMP)_objc_msgForward_impcache进入消息转发机制。
查看一下这个方法:
竟然是汇编实现的这就又印证了汇编速度更快的结论

	STATIC_ENTRY __objc_msgForward_impcache// No stret specialization.b	__objc_msgForwardEND_ENTRY __objc_msgForward_impcacheENTRY __objc_msgForwardadrp	x17, __objc_forward_handler@PAGEldr	p17, [x17, __objc_forward_handler@PAGEOFF]TailCallFunctionPointer x17END_ENTRY __objc_msgForward

具体实现

-(id)forwardingTargetForSelector:(SEL)aSelector {if ([NSStringFromSelector(aSelector) isEqualToString:@"instanceMethodTestFastForwarding:"]) {SubFromView * subFromView = [[SubFromView alloc]init];if ([subFromView respondsToSelector:aSelector]) {return  subFromView;}}return [super forwardingTargetForSelector:aSelector];
}+(id)forwardingTargetForSelector:(SEL)aSelector {if ([NSStringFromSelector(aSelector) isEqualToString:@"classMethodTestFastForwarding:"]) {if ([SubFromView respondsToSelector:aSelector]) {return  [SubFromView class];}}return [super forwardingTargetForSelector:aSelector];
}

我们在新建的SubFromView完成相应方法的实现,然后就将消息最终转发给了su bFromview实现。

3,完整消息转发(慢速转发)

//封装方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {NSString *method = NSStringFromSelector(aSelector);if ([method isEqualToString:@"sendMessage:"]) {//把这个方法存起来return [NSMethodSignature signatureWithObjCTypes:"v@:@"];}return [super methodSignatureForSelector:aSelector];}- (void)forwardInvocation:(NSInvocation *)anInvocation {//获得方法编号SEL sel = [anInvocation selector];//还来找备胎SpareWheel *sp = [SpareWheel new];//判断能否响应方法if ([sp respondsToSelector:sel]) {anInvocation.target = sp;}else {[super forwardInvocation:anInvocation];}
}

慢速转发需要同时实现methodSignatureForSelector和forwardInvocation两个函数,相当于是重新给该消息进行签名,然后调用forwardInvocation转发。[NSMethodSignature signatureWithObjCTypes:“v@😡”];,这里的"v@😡"是苹果官方的类型定义,

快速转发和慢速转发都会将消息转发给别的对象,它们的区别是什么?

  • 慢速转发可以转发给多个对象,而快速转发最多只能转发一个
  • 快速转发需要实现forwardingTargetForSelector这个方法,但是慢速必须同时实现methodSignatureForSelectorforwardInvocation方法。
  • 块速转发必须指定转发对象或者进行快速转发,而慢速转发作为最终步骤,可以不指定转发对象,也可以控制是否调用doesNotRecognizeSelector来控制抛异常。所以慢速转发可以避免闪退,如果最终没有可转发的对象,可以进行错误提示,提高用户体验。

总结:

  • 动态方法解析不处理,会进入消息转发流程

  • 消息转发流程有快速转发和慢速转发

  • 如果消息转发阶段,快速转发和慢速转发不处理,就进入doesNotRecognizeSelector默认抛出异常信息

在这里插入图片描述

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

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

相关文章

全球风味:红酒中的地域风情与特色

在红酒的世界里,每一滴琼浆玉液都承载着地域的风情与特色。它们不仅仅是葡萄酒,更是大自然的恩赐,是时间的馈赠,是人类智慧的结晶。今天,就让我们一起走进红酒的世界,感受那些来自不同地域的风情与魅力。 …

ROS2入门到精通—— 2-6 ROS2实战:可调节纯跟踪算法(局部规划)

1 Regulated Pure Pursuit 纯追踪算法变体:调节纯追踪算法 将自适应纯追踪(Adaptive Pure Pursuit)算法的特性与围绕线性速度的规则相结合,重点关注消费类、工业和服务型机器人的需求。我们还实现了几种常识性的安全机制&#xf…

业务终端动态分配IP-DHCP技术、DHCP中继技术

一、为什么需要DHCP? 1、许多设备(主机、无线WiFi终端等)需要动态地址的分配; 2、人工手工配置任务繁琐、容易出错,比如:IP地址冲突; 3、网络规模扩大、复杂度提高,网络配置越来越复杂,计算机的位置变化和数量超过可分配IP地址的数量,造成IP地址变法频繁以及IP地址…

Monaco 使用 DocumentHighlightProvider

Monaco 中有一个文字高亮的功能,就是选中一个单词,会高亮文字文档中所有出现该单词的位置,效果如下: Monaco 默认就有这个功能,可以根据具体需求进行定制。通过 registerDocumentHighlightProvider 进行注册 实现 pro…

【Java数据结构】初始线性表之一:链表

为什么要有链表 上一节我们描述了顺序表:【Java数据结构】初识线性表之一:顺序表-CSDN博客 并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素。 由于其底层是一段连续空间,当在ArrayList任意位置插入或者…

代码随想录二刷复习(二分法)

二分法模板: 1:左闭右闭区间写法 第一种写法,我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] (这个很重要非常重要)。 区间的定义这就决定了二分法的代码应该如何写,因为定…

vue 给特定满足条件的表单数据添加背景颜色,组件的 row-class-name

1、:row-class-name"tableRowClassName" 可为表格每行根据后面的函数绑定class名 <!-- 列表框 --><div class"tableList"><el-table :data"teamModelListTable" style"width: 100%"selection-change"handleSele…

el-table表格操作列错行处理

解决方法&#xff1a; <style>::v-deep .el-table th.el-table__cell > .cell {white-space: nowrap !important;} </style>

不想填邀请码?Xinstall来帮你,一键安装无忧愁

在这个快节奏的时代&#xff0c;每一个点击都承载着用户的期待与耐心。然而&#xff0c;在下载App的过程中&#xff0c;繁琐的邀请码填写往往成为了用户体验的一大障碍。你是否也曾经因为不愿填写邀请码而放弃了一款心仪的App&#xff1f;今天&#xff0c;就让我们一起走进Xins…

镜像与容器

Docker Image (镜像) Docker 镜像概念 Docker iamge 本质上是一个 read-only 只读文件&#xff0c;这个文件包含了文件系统、源码、库文件、依赖、工具等一些运行 application 所必需的文件。 可以把 Docker image 理解成一个模板&#xff0c;可以通过这个模板实例化出来很多…

【Datawhale AI 夏令营】入门lightgbm及特征工程进行实践

文章目录 1. LightGBM简介2. 导入必要的库3. 加载数据集4. 数据可视化4.1 不同类型对应目标值的柱状图4.2 特定ID的目标值折线图 5. 特征工程5.1 合并训练集和测试集并进行排序5.2 历史平移5.3 窗口统计5.4 数据切分5.5 确定输入特征 6. 模型训练与评估7. 结果展示 1. LightGBM…

一文看懂JTAG基本知识

文章目录 1、JTAG是什么?1.1边界扫描2、JTAG如何起作用?2.1 PC控制JTAG2.2 并行端口2.3 JTAG TAP控制器2.4 计算JTAG链中元件个数2.5 获得JTAG链上芯片的ID3、边界扫描3.1、SAMPLE3.2、边界扫描寄存器3.3、JTAG还可以做什么?参考资料:1、JTAG是什么? JTAG是20世纪80年代开…

云监控(华为) | 实训学习day1(10)

云监控&#xff1a;确保服务器高效运行 在当今的数字化时代&#xff0c;服务器的稳定运行对于任何企业都至关重要。为了确保服务器的 CPU、内存和硬盘等资源的合理运行&#xff0c;云监控成为了一项不可或缺的技术。本文将详细介绍云监控的基本概念、所需软件、配置方法以及如何…

JDBC 技术 | Java连接MySQL数据库(四万字零基础保姆级超全详解)

文章目录 前言一. JDBC概述1. JDBC 概念2. JDBC 本质3. JDBC 的好处 二. JDBC 快速入门1. 编写Java 程序步骤2. 在IDEA 中的操作流程3. 正式编写 Java程序 三. JDBC API详解1. DriverManager 类2. Connection 接口2.1 获取执行SQL语句的对象 3 .ResultSet 类3.1 概述3.2 代码实…

Cadence23学习笔记(四)

这个人讲cadence也很不错&#xff1a; 73、创建Power NetClass[Cadence Allegro132讲视频教程字幕版]_哔哩哔哩_bilibili 上位机开发&#xff1a; MFC 最详细入门教程-CSDN博客 Board Geometry — Design_Outline 板框 Etch — Top 走线 Pin — Top 焊盘 …

探索APP开发中的主流版式设计与应用实践

在当今移动互联网高速发展的时代&#xff0c;APP已成为人们日常生活中不可或缺的一部分。无论是社交娱乐、购物支付还是工作学习&#xff0c;各类APP都以其独特的界面设计和用户体验赢得了用户的青睐。而APP开发的版式设计和页面规范&#xff0c;则是决定用户体验好坏的关键因素…

记录些MySQL题集(6)

MySQL 单表为什么不要超过 2000W 行&#xff1f; 数据持久化在磁盘中&#xff0c;磁盘的最小单元是扇区&#xff0c;一个扇区 0.5 KB&#xff0c;而由 8 个扇区可以构成一个文件系统块&#xff08;4K&#xff09;&#xff0c;以 InnoDB 存储引擎为例&#xff0c;一个数据页的大…

打卡第15天------二叉树

最近公司给我派活儿太多了,要干好多活儿,好多工作任务要处理,我都没时间刷题了。leetcode上的题目通过数量一直停留在原地不动,我真的很着急呀,我现在每天过的都有一种紧迫感,很着急,有一种与时间赛跑的感觉,真的时间过的太快了,没有任何人能够阻挡住时间的年轮向前推…

【异步爬虫:利用异步协程抓取一部电影】

利用异步协程抓取一部电影 我们把目光转向wbdy. 目前该案例还是可以用的. 我们想要抓取网上的视频资源就必须要了解我们的视频网站是如何工作的. 这里我用91看剧来做举例. 其他网站的原理是一样的. 1.视频网站是如何工作的 假设, 你现在想要做一个视频网站. 也有很多的UP猪…

【BUG】已解决:java.lang.IllegalStateException: Duplicate key

已解决&#xff1a;java.lang.IllegalStateException: Duplicate key 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市…